# Sandwich Attacks and How to Protect Against MEV
When you swap tokens on an automated market maker (AMM), your transaction sits in the public mempool before it is mined. Anyone watching that mempool can read your intent: the amount, the token pair, and crucially your slippage tolerance. A sandwich attack exploits exactly this visibility. It is one of the most common forms of MEV (Maximal Extractable Value), and unlike a contract bug, it does not require a flaw in the code you call. It only requires that you broadcast a profitable trade in the clear.
This article explains the mechanics, why a loose slippage bound quietly hands money to a searcher, and the practical defenses you can apply both as a user and as a protocol designer.
How a sandwich attack works
An AMM like a constant-product pool prices a swap along the curve x * y = k. The larger your trade relative to the reserves, the more the price moves against you. That movement is price impact, and it is deterministic given the reserves.
A sandwich is three transactions ordered around yours:
The attacker profits from the price they moved, and you pay for it through worse execution. They do not need to predict the future. They only need your pending transaction and the ability to order their own around it.
A concrete example
Suppose a pool holds 1,000 ETH and 2,000,000 USDC. You want to buy ETH with 100,000 USDC. On the bare curve you would receive roughly 47.6 ETH. If the attacker first spends 200,000 USDC buying ETH, the pool shifts, and your same 100,000 USDC now buys noticeably less. The attacker then sells their ETH back into the inflated price. The gap between what you should have received and what you did receive is their margin, minus gas.
Why a loose slippage bound fails
Every serious swap function takes a minimum-output argument. In Uniswap V2 style routers it looks like this:
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
The amountOutMin is your protection: the transaction reverts if you would receive less. The problem is how much room you give it. Many front ends default to a comfortable buffer, sometimes 0.5 percent, 1 percent, or worse, an open-ended 49 percent in extreme cases. That buffer is not a safety margin for you. It is the maximum profit you have authorized the attacker to extract.
Think of it this way: a sandwich is profitable only up to the point where your trade still clears your amountOutMin. A tighter bound shrinks that window. Set it to a number that reflects real market conditions plus genuine volatility, not a lazy round number. If you allow 3 percent of slippage on a calm pool, you have invited a searcher to take most of it.
// Compute amountOutMin off the current quote with a tight tolerance.
// Example: 30 basis points (0.30%).
uint256 quoted = router.getAmountsOut(amountIn, path)[path.length - 1];
uint256 amountOutMin = quoted * 9970 / 10000;
A deadline matters too. A stale deadline lets a transaction linger and be executed at a future, manipulated price. Keep it short, on the order of a few minutes.
Protections for users
Tightening amountOutMin reduces the size of the prize but does not hide your trade. The stronger fix is to stop broadcasting in the clear.
- Private order flow / Flashbots Protect: instead of the public mempool, you submit through a private relay that sends the transaction directly to block builders. Searchers cannot see it before inclusion, so there is nothing to sandwich. This is the single most effective protection for an individual swapper.
- Tight slippage plus a short deadline: defense in depth even when you do use a private route, since not all paths are fully private.
- Split large trades: breaking a big swap into smaller pieces lowers per-transaction price impact, though it raises total gas. Use with care.
- Trade on pools with deep liquidity: the same notional trade moves price less when reserves are large, leaving a thinner margin for an attacker.
Protections for protocol designers
If you build a contract that swaps on behalf of users (a vault, a router, a rebalancer), the responsibility shifts to you. Never call a swap with amountOutMin = 0. That single line is a standing invitation to drain value on every interaction.
Pass real minimums through, do not hardcode zero
// Anti-pattern: the caller has no protection at all.
router.swapExactTokensForTokens(amountIn, 0, path, address(this), block.timestamp);
// Better: require the caller to specify, and enforce a freshness window.
function rebalance(uint256 amountIn, uint256 minOut, uint256 deadline) external {
require(deadline >= block.timestamp, "expired");
require(minOut > 0, "minOut required");
router.swapExactTokensForTokens(amountIn, minOut, path, address(this), deadline);
}
Be TWAP-aware, not spot-price-naive
If your contract reads a price to make a decision (collateral valuation, a fair-value check, a sanity bound on a swap), never trust the instantaneous spot price of an AMM. Spot price is exactly what a sandwich manipulates. Use a time-weighted average price (TWAP) over a window long enough that moving it for the duration would cost more than the attack yields.
Uniswap V3 exposes cumulative tick data you can read for a TWAP:
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 600; // 10 minutes ago
secondsAgos[1] = 0; // now
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
int56 tickDelta = tickCumulatives[1] - tickCumulatives[0];
int24 avgTick = int24(tickDelta / int56(uint56(600)));
// Convert avgTick to a price with the standard tick math, then use it.
A TWAP raises the cost of manipulation because the attacker must hold the price away from fair value across many blocks, not just one. It is not magic. A short window or a thin pool can still be moved, so size the window to the value at stake.
Commit-reveal for order intent
When the trade itself must be public on a single chain, you can hide intent in time. In a commit-reveal scheme, a user first submits a hash of their order (the commit) and only later submits the plaintext (the reveal). A searcher who sees the commit cannot front-run a trade whose direction and size are unknown.
mapping(address => bytes32) public commitments;
function commit(bytes32 orderHash) external {
commitments[msg.sender] = orderHash;
}
function reveal(uint256 amountIn, uint256 minOut, uint256 nonce) external {
bytes32 expected = keccak256(abi.encodePacked(amountIn, minOut, nonce, msg.sender));
require(commitments[msg.sender] == expected, "bad reveal");
delete commitments[msg.sender];
// execute the swap with the revealed parameters
}
Commit-reveal adds a transaction and latency, and it does not by itself solve back-running, so it fits batch auctions and order systems more than a single retail swap. Treat it as a building block, not a drop-in fix.
A short mental checklist
- Is my
amountOutMinderived from a fresh quote with a tight, realistic tolerance?
- Is my
deadlineshort?
- Am I broadcasting to the public mempool, or through a private relay?
- If a contract reads a price to make a decision, is it a TWAP and not a spot read?
- Does any swap path anywhere in my system pass
0as the minimum output?
Practice it hands-on
The fastest way to internalize this is to build the attack and then defend against it. On app.solingo-blockchain.xyz you can work through AMM mechanics, compute price impact on a constant-product curve, and write swap logic that sets a real amountOutMin and reads a TWAP instead of spot. Seeing your own loose slippage get sandwiched in an exercise tends to make the lesson stick.