By Odiwuor Zacharia Omollo — AI Engineer & Full-Stack Developer, Nairobi, Kenya

Flash-loan arbitrage has the best pitch in DeFi: borrow millions with zero collateral, buy low on one exchange, sell high on another, repay the loan in the same transaction, keep the spread. If any step fails, the whole transaction reverts and you lose nothing but gas.

So I built one. A scanner that watches Arbitrum pools for price gaps, a route optimiser, and a Solidity executor contract. Then I did the part most bot tutorials skip: I checked, rigorously, whether it would actually make money.

It wouldn't. Here is the evidence, and what I changed because of it.

What I built

The system has three parts:

  • A read-only scanner (Python) that reads pool state on Arbitrum One — Uniswap V3 and Camelot pools — batched into a single pinned-block Multicall3 request, so every price in a scan comes from the same block. No private key is ever loaded; the scanner cannot sign or broadcast anything.
  • A profit calculator that does all arithmetic in raw integer token units, decomposing real gas costs through Arbitrum's NodeInterface precompile instead of guessing them.
  • A flash-loan executor contract (FlashLoanArbitrage.sol) supporting Balancer, Morpho and Aave flash loans, with per-swap-type execution paths.

The first embarrassment: the contract never compiled

Before any profitability question mattered, the executor contract failed solc with a "Stack too deep" error at the Camelot V2 branch — one oversized function doing every swap type. Under a standard toolchain, the contract had never compiled at all.

Splitting it into one internal function per swap type (_swapV2Router, _swapV3Router, _swapDirectV2Pair, _swapCamelotV2Pair, _swapAlgebraRouter) and stripping revert strings brought it to 22,720 bytes deployed under solc 0.8.20 (via-IR, optimizer runs=1) — inside the 24,576-byte deploy limit, with 0 errors and 0 warnings.

Compiling is not deploying, and deploying is not an audit. But a bot whose contract cannot compile is not a bot; it is a story.

The fee wall: why the textbook route is structurally negative

The canonical beginner route — flash-borrow WETH or USDC, swap on one DEX, swap back on another — has to clear roughly 0.15%–0.65% in combined fees (two swap fees, plus the flash-loan fee) before price impact. Meanwhile, the aggregate value available in cross-rollup arbitrage is only about 0.03%–0.05% of volume, and the market is contested: two firms won over 90% of Arbitrum's Timeboost express-lane auctions between April and July 2025.

The fee details matter at small scale:

  • Aave V3 charges 5 bps for a flash loan — that's $5 on a $10,000 loan, frequently the entire margin. Balancer V2 and Morpho Blue charge 0%, if their on-chain liquidity actually covers your size.
  • At the observed 6–12% success rate for L2 cyclic arbitrage probes, most attempts revert. A barely-positive trade is negative-EV once you price in the gas of the 88–94% that fail.

The live check: 33 routes, best result −$1.08

I scanned live routes across trade sizes. Zero of 33 route/size combinations were profitable. The best one lost $1.08.

My original sizing made it worse. The old code traded a fixed $1,000 per route — and in the deterministic test harness, fixed sizing lost roughly $9–$10 on every route, because profit is unimodal in trade size: too small and fees dominate, too large and your own price impact eats the spread. A fixed size is wrong in both directions.

What I changed

Three changes, each aimed at a measured failure rather than a hope:

  1. Optimal sizing — find_optimal_amount() ternary-searches trade size against the exact-input quote function, scoring every candidate with full fee accounting. If no size is net-positive, the route is explicitly marked non-executable.
  2. Zero-fee flash-loan selection — cheapest_flash_provider() prefers Balancer (0%) → Morpho (0%) → Aave (0.05%), and only among providers whose verified on-chain liquidity covers the loan. Unknown liquidity is treated as unavailable, never assumed.
  3. An expected-value execution gate — passes_execution_gate() refuses to submit unless net profit is positive, clears a minimum threshold, and covers the expected revert-gas cost at the caller's attempts-per-success rate. Hope is not an input.

The full suite is 38/38 tests passing (31 pre-existing + 7 new for these changes), and the scanner stays read-only by design.

What would have to change for this to make money

Honestly stated:

  • A wider route universe. Three pools of major pairs is the worst-contested segment of the market. Long-tail pairs and low-fee 3-hop cycles are where a solo operator's edge could plausibly exist.
  • Fork and testnet testing. This has not been done yet. Simulation against pinned blocks is not execution.
  • An independent audit before any real funds touch the contract.

Until those happen, expected earnings from this system are $0 — and I'd rather publish that number than a fantasy.

The lesson

"The bot works" and "the bot makes money" are different claims, and only one of them survives measurement. Most arbitrage content online shows you the first claim and lets you assume the second.

Verification-first engineering — pinned-block reads, integer-exact accounting, an execution gate that can say no — is slower than optimism. It's also the only version of this project I'd put my name on.

The code is open source: arbitrum-flashloan-research. More of my work: GitHub · LinkedIn