At a glance
- The setup
- Pending transactions are public, and block builders order transactions largely by who pays most, so ordering is for sale
- Front-running
- An observer sees your profitable transaction waiting and pays to have theirs executed first
- The sandwich
- Around your swap: a buy just before to push the price up, and a sell just after to pocket the difference, at your expense
- The fix in the contract
- A minimum-output limit and a deadline, so a manipulated price makes your transaction revert instead of settling at a loss
- The rule
- On-chain there are no secrets and no guaranteed order. Anything profitable to reorder, will be
Ordering is for sale
Two facts about Ethereum combine into a whole economy. First, a transaction is public in the mempool before it is mined. Second, the party building the block chooses the order of transactions in it, and is paid to prefer the ones that tip highest. Put those together and a searcher can watch for a transaction that will move a price or unlock a reward, and pay to be placed in front of it, or on both sides of it. The value they extract by doing so has a name: MEV, maximal extractable value.
This is not a bug in a specific contract. It is a property of a transparent, openly-ordered ledger. You cannot patch it away, but you can write contracts that give an attacker nothing worth reordering, and that is the whole of the defence.
The sandwich attack
The cleanest example is a swap on an automated market maker, where the price moves as the pool's balances change. Suppose you swap a large amount of USDC for a token, and your transaction does not insist on a minimum amount of token in return.
// Accepts whatever the pool gives back, at whatever the price is when it runs.
function swap(uint256 amountIn) external {
uint256 out = pool.swapExactIn(amountIn); // no minimum, no deadline
token.transfer(msg.sender, out);
}
A searcher sees it waiting and wraps it in two of their own transactions. First they buy the same token, pushing its price up. Then your transaction runs and buys at that inflated price, getting fewer tokens than you should. Then they sell what they bought, into the demand your trade created, at the higher price. You paid the difference; they kept it. Your trade was the filling in the sandwich.
It is not only swaps
Any transaction whose profit a stranger can capture by going first is exposed. A claim that pays the first caller: a bot copies your call with a higher tip and takes it. A liquidation bounty: bots race to be the one that fires. A puzzle contract that pays for the right answer, submitted in plain view: the answer is copied out of your pending transaction and submitted ahead of you. And a special case worth naming on its own: there are no secrets on-chain. A "commit" that reveals its value in the same transaction, or a password stored in a contract's state, is visible to everyone the moment it touches the chain.
The defences
The first defence is the most important and the cheapest: a minimum output and a deadline. If your swap insists on receiving at least a sensible amount, and only within a short window, then a sandwich that worsens the price makes your transaction revert rather than settle at a loss. The attack stops being profitable.
function swap(uint256 amountIn, uint256 minOut, uint256 deadline) external {
require(block.timestamp <= deadline, "expired");
uint256 out = pool.swapExactIn(amountIn);
require(out >= minOut, "slippage: price moved against me"); // revert, do not settle
token.transfer(msg.sender, out);
}
Where a value genuinely must stay secret until it is acted on, use commit-reveal: first submit a hash of your value plus a random salt, and only later reveal the value itself. The chain sees the commitment but not its contents, so no one can front-run what they cannot read.
// Step 1, in one block: publish only a hash. Reveals nothing.
function commit(bytes32 commitment) external { commits[msg.sender] = commitment; }
// Step 2, in a later block: prove the value matched the commitment.
function reveal(uint256 value, bytes32 salt) external {
require(commits[msg.sender] == keccak256(abi.encodePacked(value, salt)), "no match");
// ... act on value, now that front-running it is impossible ...
}
Beyond the contract, trades can be routed through private order flow so they never sit in the public mempool, which denies searchers the preview the whole attack depends on. Contract-level protection and private routing are complementary: the first makes an attack unprofitable, the second makes it invisible.
Treat every on-chain value as published. Contract storage marked private is private only to other contracts' code, not to human observers, who can read any slot directly. Randomness from block.timestamp or blockhash can be seen and sometimes nudged by the block's builder. If secrecy or unpredictability matters, it cannot come from data that lives on-chain in the clear.
The short version. The mempool is public and block ordering is sold to the highest tip, so any transaction worth reordering will be. The sandwich is the classic case: a swap with no minimum output is bought around and settles at a worse price. Defend at the contract with a minimum output and a deadline so a moved price reverts instead of paying out, use commit-reveal when a value must stay hidden until acted on, and route sensitive trades privately. And never keep a secret on-chain, because there is no such thing.
References & further reading
- Ethereum community, Maximal extractable value (MEV). How ordering becomes a market, and where the value comes from.
- Ethereum community, Secrets, commitments and signatures. Why on-chain data is never hidden.
- Uniswap, Slippage. Minimum output and deadlines as protection against a moved price.
- Flashbots, Protect and private order flow. Keeping a transaction out of the public mempool.