What is MEV, and how do I protect users from sandwich attacks? · Jumpstart Blockchain
What is MEV, and how do I protect users from sandwich attacks?
MEV is profit from ordering transactions. A sandwich attack front-runs and back-runs a public swap, capped by its slippage. Use tight minimum outputs from fresh quotes and private RPCs.
MEV (maximal extractable value) is the profit available to whoever decides the order of transactions in a block. The best-known harmful kind is the sandwich attack. A searcher spots your pending swap, buys the same asset just before you to push the price up, lets your swap fill at the worse price, then sells right after you. Their profit comes out of your execution, and it is capped by your slippage tolerance. You protect users by keeping that cap tight and by keeping swaps out of the public mempool.
Where MEV comes from
On Ethereum, a pending transaction sent to a normal RPC is gossiped through the public mempool, where anyone can read it before it's included. Most blocks are now built through proposer-builder separation via MEV-Boost:
Searchers scan the mempool and state for opportunities and send bundles (ordered transaction lists) to builders.
Builders assemble the most profitable block they can from public transactions and bundles.
Relays pass the best block bids to the proposer (validator), who picks the highest-paying one.
Not all MEV is harmful. Arbitrage lines up prices across pools, and liquidations keep lending markets solvent. Sandwiches are different: they take value directly from a specific user.
Anatomy of a sandwich
Say a user swaps 5 ETH for USDC with 1% slippage. That means "fill me as long as I receive at least 99% of the quoted amount".
Block N:
tx 1 searcher: buy USDC with ETH -> pushes the ETH/USDC price against the user
tx 2 user: swap 5 ETH for USDC -> fills close to the 1% limit
tx 3 searcher: sell USDC for ETH -> captures the price difference
The searcher sizes tx 1 so the user's swap just clears amountOutMinimum. That is why slippage tolerance is effectively the most a sandwich can take from you. At 1% they can take up to about 1% (less pool fees and gas), and with amountOutMinimum = 0 they can take nearly everything. You can often spot a sandwich on a block explorer as two transactions from the same bot, trading the same pool, directly before and after the victim's swap.
Approve is safe; unlimited, forgotten allowances and unread permit signatures are not. Approve exact amounts, use permit with short deadlines, read every typed-data signature, and revoke old approvals.
Compute the minimum from a fresh off-chain quote just before sending, and set slippage to match the pool's liquidity and the trade size. Don't use a one-size-fits-all 5%.
Set a real deadline too, so a transaction that sits around for a while can't be filled later at a stale price.
Protection 2: don't broadcast swaps publicly
Send transactions through a private RPC that forwards them to builders without putting them in the public mempool. Searchers can't sandwich what they can't see. Two widely used options on Ethereum mainnet are Flashbots Protect (https://rpc.flashbots.net) and MEV Blocker (https://rpc.mevblocker.io). In a wallet, just add the RPC. In code, point the wallet client at it:
The trade-offs: you trust the RPC operator and the builders it shares with, and inclusion can take a little longer because only participating builders see your transaction. Private submission lowers the risk of a sandwich, but it doesn't replace protection 1.
Protection 3: execution that isn't a public market order
Batch auctions and intents. CoW Protocol settles orders in batches at a uniform clearing price, and UniswapX has fillers compete to fill signed orders. In both, the user signs an order with a minimum outcome instead of broadcasting a raw swap, and solvers or fillers compete to execute it.
Limit orders instead of market swaps when timing isn't urgent.
Split large trades, or route through deeper liquidity. Price impact is what makes a trade worth sandwiching.
If your contract swaps, you're a target too
Vaults, treasuries and keepers that swap on-chain get sandwiched constantly. The common bugs:
// Inside a vault using Uniswap V3's original SwapRouter.
// minOut and deadline are computed off-chain from a fresh quote.
function harvest(uint256 amountIn, uint256 minOut, uint256 deadline) external {
require(msg.sender == keeper, "not keeper");
router.exactInputSingle(ISwapRouter.ExactInputSingleParams({
tokenIn: reward,
tokenOut: want,
fee: 3000,
recipient: address(this),
deadline: deadline, // not block.timestamp, which always passes
amountIn: amountIn,
amountOutMinimum: minOut, // never 0
sqrtPriceLimitX96: 0
}));
}
amountOutMinimum: 0 hands the whole trade to anyone who sandwiches it.
Computing the minimum on-chain from the current spot price doesn't help either: the attacker has already moved that price earlier in the same block. Get the bound from the caller, from an off-chain quote, or from a manipulation-resistant oracle (a TWAP or a price feed).
deadline: block.timestamp always passes, so it offers no protection.
On rollups with a single sequencer and no public mempool, classic mempool sandwiching is harder, but the same slippage discipline still applies.
Checklist
Never ship amountOutMinimum = 0. Derive it from a fresh quote with slippage suited to the trade.
Use real deadlines, not block.timestamp.
Default users to a private RPC (Flashbots Protect, MEV Blocker) or intent-based execution.
In contracts, take minimum outputs from callers or robust oracles, never from spot price.
Warn users about large trades in thin pools, and offer limit orders or splitting.
Storage dominates gas costs: pack variables, cache storage reads, use constant, immutable, calldata and custom errors, emit events for off-chain data, and measure every change with forge snapshot.
It's usually a max fee below the base fee, a low tip, or an earlier stuck nonce. Speed up by resending with the same nonce and higher fees; cancel by sending 0 ETH to yourself with that nonce.