← writing

An arbitrage bot with no slippage is a sandwich

A flash-loan arbitrage contract is a neat trick: borrow a pile of money you don’t have, use it to buy an asset cheap on one DEX and sell it dear on another, pay the loan back in the same transaction, and keep the difference. If the trade isn’t profitable, the whole thing reverts and you’re out nothing but gas. I had a 2021 hackathon skeleton of exactly this — an Aave v1 contract in Solidity 0.5 with the borrowing wired up and no actual arb — and rebuilt it into a real Aave v3 strategy.

The contract gets the genuinely hard part right. The part it left open is the one an arbitrage bot, of all things, should know about.

The hard part it got right

The dangerous surface of a flash-loan receiver is the callback. Aave hands control to your executeOperation in the middle of its own transaction, holding your borrowed funds. Two things have to be true or you’re drained:

require(msg.sender == address(POOL_CONTRACT), "caller not pool");
require(initiator == address(this),          "bad initiator");

The first stops anyone but Aave from calling the callback directly. The second is subtler and more important: without it, I could call flashLoanSimple and name your contract as the receiver, and Aave would dutifully invoke your executeOperation with my parameters — running your logic with attacker-chosen routers. Binding initiator == address(this) means the loan had to originate from this contract’s own executeArb. Both guards are there, and the repayment is enforced before any profit is swept. That’s the stuff that usually gets these contracts emptied, and it was correct.

The part it didn’t

The swaps had no slippage protection. Each leg just took whatever the router gave:

router.swapExactTokensForTokens(amountIn, 0, path, ...);  // amountOutMin = 0

Here’s why that’s the bug, and why it’s almost funny that it’s this contract: an arbitrage transaction sits in the public mempool announcing “I am about to do two profitable swaps.” A searcher reads it, front-runs to move the pool against you, lets your swaps execute at the worse price, and back-runs to collect. The contract built to harvest a price spread becomes the perfect victim of one — it broadcasts its intent and then accepts any fill. The arb bot is the sandwich filling.

And the minProfit check at the end doesn’t save you, which is the trap. Profit is measured after both swaps, so a sandwich that degrades each leg can still leave you above a loose minProfit while a searcher pockets the difference — or it pushes you to a tiny revert with nothing gained. You need a floor on each swap (amountOutMin) plus a deadline, not just a check on the final tally.

A flash-loan arb cycle, and where an unprotected swap gets sandwiched borrow (flash) swap A (buy) amountOutMin = 0 swap B (sell) amountOutMin = 0 repay + profit? front-run back-run searcher's sandwich → With amountOutMin = 0, each public swap accepts any fill — the searcher moves the price, your swaps execute worse, and the final minProfit check can still pass. Bound each leg.
The callback guards (only-pool, initiator-bound) were correct; the unprotected swaps weren't. A per-leg amountOutMin + deadline is the fix — a trailing minProfit isn't enough.

Who found it

I didn’t, on the first pass. The rebuild came out of a model-tiered agent workflow — one model writes the contract, a stronger one audits it before anything ships. The builder got the callback guards right and left the slippage at zero; the auditor flagged it medium-severity (“an arb with no amountOutMin is a free sandwich”), and the fix — per-leg minOut1/minOut2 and a deadline — went in with a regression test. Then I re-ran all 17 tests myself and read the callback by hand, because a green check from the fleet is a claim, not a result.

The contract, the mocks, and the tests are public. It’s tested against mock Aave and mock DEXes, not live ones — so it’s a faithful model of the strategy, not a deployed bot. But the lesson generalizes past flash loans: the protections you’re sure you don’t need are the ones for the attack your own code is designed around. An arb contract is a price-spread predator that forgot it trades in public.