This contract is deployed on mainnet right now, processing swaps:
function buyTokens(uint256 amountIn, address[] calldata path) external {
IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn);
IERC20(path[0]).approve(router, amountIn);
IUniswapV2Router02(router).swapExactTokensForTokens(
amountIn,
0, // amountOutMin — accepts any output
path,
msg.sender,
block.timestamp + 300
);
}
The amountOutMin of zero means the caller will accept whatever output the swap produces, no matter how bad. A sandwich bot watching the mempool sees this transaction, front-runs it with a buy, lets the victim's swap execute at the inflated price, then immediately sells. The victim gets fewer tokens. The bot keeps the spread. The contract code is syntactically correct and passes compilation. Nothing reverts.
MEV — Maximal Extractable Value — does not require a bug in your code. It requires a predictable on-chain pattern and a public mempool. Understanding MEV means understanding how your contract looks to a searcher running a simulation loop against every pending transaction.
This guide covers six patterns that reduce MEV exposure, with Solidity examples for each.
What MEV Is and Why Developers Need to Think About It
MEV is the profit available to whoever controls transaction ordering within a block. Post-Merge, that's validators and the block builders they delegate to. In practice, specialized bots — searchers — monitor the public mempool, simulate pending transactions, and submit competing transactions to capture value.
Searchers are not guessing. They run your contract's logic against a simulation environment, compute exact profit, and decide in milliseconds whether to front-run, back-run, or sandwich.
The patterns they exploit are not exotic:
- Swaps with no minimum output (
amountOutMin = 0) - Auctions where bids are visible before execution
- Contracts that use
block.timestampas a randomness source - Limit orders priced at spot rather than time-averaged rates
- Transactions submitted to the public mempool that should have been private
None of these are obvious bugs. All of them are common in deployed contracts.
Pattern 1: Slippage Protection
The vulnerability
Zero slippage is the most common MEV entry point in DeFi integrations. When amountOutMin is zero, the swap accepts any output — including output that has been driven down by the bot's front-run trade. There is no floor price; the sandwich bot can extract up to the full trade value.
// VULNERABLE: zero slippage = infinite sandwich opportunity
router.swapExactTokensForTokens(
amountIn,
0, // accepts any output — no protection
path,
recipient,
block.timestamp + 300
);
The fix: basis point slippage calculation
Calculate amountOutMin from the expected output with a tolerance expressed in basis points (1 bp = 0.01%). A standard default is 50 bp (0.5%). Volatile or low-liquidity pairs may need 100 bp (1%).
// SECURE: slippage tolerance enforced on-chain
function swapWithSlippage(
address router,
uint256 amountIn,
address[] calldata path,
uint256 slippageBps // 50 = 0.5%, 100 = 1.0%
) external returns (uint256[] memory amounts) {
// getAmountsOut for quote only — not used as an oracle
uint256[] memory expected = IUniswapV2Router02(router).getAmountsOut(amountIn, path);
uint256 expectedOut = expected[expected.length - 1];
// Apply basis point tolerance
uint256 amountOutMin = (expectedOut * (10_000 - slippageBps)) / 10_000;
IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn);
IERC20(path[0]).approve(router, amountIn);
amounts = IUniswapV2Router02(router).swapExactTokensForTokens(
amountIn,
amountOutMin, // revert if output falls below this floor
path,
msg.sender,
block.timestamp + 300
);
}
getAmountsOut() reads spot reserves — it is not a trusted oracle. It is used here purely to estimate expected output for slippage math. Never pipe its output into protocol-level decisions.
Pattern 2: Commit-Reveal Scheme
The vulnerability
Auctions, randomized NFT mints, price bids, and any scenario where a transaction's value depends on its content being unknown before execution are all front-running targets. The moment a bid is in the mempool, it is public.
// VULNERABLE: bid amount visible in mempool before execution
function bid(uint256 amount) external payable {
require(msg.value == amount, "Value mismatch");
require(amount > highestBid, "Bid too low");
highestBid = amount;
highestBidder = msg.sender;
}
A bot watching the mempool sees amount, computes amount + 1 wei, and submits a higher-gas transaction that lands first.
The fix: two-phase commit-reveal
Split the interaction into two transactions separated by a time window. The first transaction commits a hash of the action and a secret salt. The second reveals the preimage. During the commit phase, bots see only the hash — front-running a hash is useless because they do not know the underlying value.
// SECURE: commit-reveal auction
contract CommitRevealAuction {
struct Commitment {
bytes32 hash;
bool revealed;
}
mapping(address => Commitment) public commitments;
mapping(address => uint256) public bids;
uint256 public immutable commitDeadline;
uint256 public immutable revealDeadline;
address public winner;
uint256 public winningBid;
constructor(uint256 commitWindow, uint256 revealWindow) {
commitDeadline = block.timestamp + commitWindow;
revealDeadline = commitDeadline + revealWindow;
}
// Phase 1: submit hash — amount and salt stay off-chain until reveal
function commit(bytes32 commitment) external {
require(block.timestamp < commitDeadline, "Commit phase closed");
require(commitments[msg.sender].hash == bytes32(0), "Already committed");
commitments[msg.sender].hash = commitment;
}
// Phase 2: reveal preimage — contract verifies it matches the stored hash
function reveal(uint256 amount, bytes32 salt) external payable {
require(block.timestamp >= commitDeadline, "Commit phase still open");
require(block.timestamp < revealDeadline, "Reveal phase closed");
Commitment storage c = commitments[msg.sender];
require(c.hash != bytes32(0), "No commitment found");
require(!c.revealed, "Already revealed");
require(msg.value == amount, "Value must match bid");
require(
keccak256(abi.encodePacked(amount, salt, msg.sender)) == c.hash,
"Hash mismatch"
);
c.revealed = true;
bids[msg.sender] = amount;
if (amount > winningBid) {
winningBid = amount;
winner = msg.sender;
}
}
}
Callers generate the commitment off-chain: keccak256(abi.encodePacked(amount, salt, msg.sender)). The salt must be secret — without it, an attacker can brute-force the hash to recover the bid. Commit-reveal applies equally to on-chain randomness generation and sealed-bid pricing for any order mechanism.
Pattern 3: Private Mempool via Flashbots Protect
The most complete MEV defense at the infrastructure layer is routing transactions through a private mempool. If a transaction is never visible before it lands, it cannot be front-run.
Flashbots Protect is an RPC endpoint that submits transactions privately to Flashbots block builders, bypassing the public mempool entirely. From the DApp perspective, the integration is one config change — swap the provider URL. No contract changes required.
// ethers.js — route transactions through Flashbots Protect
const provider = new ethers.JsonRpcProvider("https://rpc.flashbots.net");
const signer = new ethers.Wallet(privateKey, provider);
const tx = await contract.connect(signer).swap(amountIn, amountOutMin, path);
Flashbots Protect also supports bundle submission — multiple transactions that execute atomically or not at all, useful for multi-step operations that should not be partially filled.
For ERC-4337 account abstraction, UserOperation bundlers serve the same role: they aggregate user operations off-chain and submit them as a single transaction, giving users effective mempool privacy without a custom RPC setup.
Private mempool routing does not eliminate all MEV — bots can still react to confirmed on-chain state. It eliminates the most profitable category: reacting to pending transactions.
Pattern 4: Deadline and Slippage Combined
Slippage alone does not prevent a delayed sandwich. A transaction can sit in the mempool for minutes or hours — especially during gas price spikes — and execute when market conditions have shifted. UniswapV2's swapExactTokensForTokens is the canonical model for combining both defenses:
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin, // revert if output below this
address[] calldata path,
address to,
uint deadline // revert if executed after this timestamp
) external returns (uint[] memory amounts);
Both parameters are required. amountOutMin bounds the price impact from sandwich insertion. deadline prevents stale execution of a transaction that was meant to be time-sensitive.
// SECURE: both defenses active
uint256 expectedOut = getExpectedOutput(amountIn, path); // from off-chain or TWAP
uint256 amountOutMin = (expectedOut * 9950) / 10000; // 0.5% slippage
uint256 deadline = block.timestamp + 180; // 3-minute window
router.swapExactTokensForTokens(
amountIn,
amountOutMin,
path,
msg.sender,
deadline
);
Setting deadline = type(uint256).max or deadline = block.timestamp + 365 days disables the protection. Passing block.timestamp directly is equally useless — the transaction executes in the current block, so it never restricts anything. Pass a future timestamp computed at submission time.
Pattern 5: TWAP Pricing for On-Chain Orders
Spot price is manipulable in a single block via flash loan. Any limit order or conditional execution mechanism that reads spot price to determine whether to fill can be triggered by a price manipulation.
// VULNERABLE: limit order filled based on manipulable spot price
function fillLimitOrder(uint256 orderId) external {
Order storage order = orders[orderId];
(uint112 r0, uint112 r1,) = IUniswapV2Pair(order.pair).getReserves();
uint256 spotPrice = (uint256(r1) * 1e18) / uint256(r0);
// Attacker flash-loans into pool, pushes spotPrice above order.limitPrice, triggers fill
require(spotPrice >= order.limitPrice, "Price not reached");
_executeFill(order);
}
Price orders against a TWAP oracle with at minimum a one-hour window — preferably 24 hours for low-liquidity pairs. Moving a TWAP meaningfully requires sustained capital across many blocks, making flash loan manipulation economically impractical.
// SECURE: limit order priced against TWAP — immune to single-block manipulation
function fillLimitOrder(uint256 orderId) external {
Order storage order = orders[orderId];
// consult() returns time-averaged price — flash loans cannot move it
uint256 twapPrice = IUniswapV2Oracle(twapOracle).consult(order.tokenIn, 1e18);
require(twapPrice >= order.limitPrice, "TWAP price not reached");
_executeFill(order);
}
Chainlink price feeds are a production-grade alternative, updated by independent node operators with built-in deviation circuit breakers. For assets without a Chainlink feed, deploy a UniswapV2OracleSimple with a 24-hour window.
Pattern 6: Batch Auctions
The CoW Protocol (Coincidence of Wants) and 1inch Fusion architecture solve MEV at a structural level. Instead of executing individual swaps through the public mempool, they aggregate orders over a time window, find a clearing price off-chain, and settle all matched orders atomically in a single transaction.
The on-chain settlement contract receives a clearing price and a set of pre-signed orders, verifies each against the clearing price, and executes transfers atomically:
// Simplified batch settlement — all orders fill at the same clearing price
function settle(
Order[] calldata orders,
uint256 clearingPrice, // determined off-chain by solver
bytes calldata solverSig
) external {
require(isTrustedSolver(msg.sender, solverSig), "Unauthorized solver");
for (uint256 i = 0; i < orders.length; i++) {
Order calldata order = orders[i];
require(clearingPrice >= order.limitPrice, "Order not fillable");
uint256 outAmount = (order.inAmount * clearingPrice) / 1e18;
IERC20(order.tokenIn).transferFrom(order.trader, address(this), order.inAmount);
IERC20(order.tokenOut).transfer(order.trader, outAmount);
}
}
Two properties make batch auctions MEV-resistant: all orders see the same clearing price so there is no marginal order for a sandwich bot to target, and off-chain solvers compete on execution quality rather than block position. The value that would go to sandwich bots goes to users as price improvement instead.
Protocols building DEX integrations at scale should evaluate whether their flow can route through a batch auction system rather than direct router calls.
What Not to Do
Do not depend on transaction ordering for correctness. If your contract's security requires transaction A to always execute before transaction B, a sequencer or validator can break that assumption. Write contracts safe under any ordering.
Do not use block.timestamp as a randomness source. Within the same block, block.timestamp is identical for every transaction and can be nudged by validators. For on-chain randomness, use Chainlink VRF or a commit-reveal scheme with the reveal separated by at least one block.
Do not assume a pending transaction is private. Any transaction sent to a public RPC node — Infura, Alchemy, or your own — enters the public mempool and is visible to the entire Ethereum p2p network within seconds. Use Flashbots Protect or a private bundle for transactions that should not be front-runnable.
Do not use tx.origin to identify users. Smart wallets, meta-transaction relayers, and ERC-4337 paymasters all change tx.origin without changing the effective user. Use msg.sender with explicit access control.
What ContractScan Detects
| Pattern | Detection |
|---|---|
amountOutMin = 0 in router calls |
AST pattern match on zero literal in swap call arguments |
Missing deadline or deadline = type(uint256).max |
Literal value detection in router call deadline arguments |
block.timestamp used as randomness input |
Data flow analysis tracing timestamp into keccak or modular arithmetic |
Spot price from getReserves() used in protocol logic |
Static analysis flagging reserve ratio in pricing or conditional branches |
getAmountsOut() output used in protocol decisions |
Call graph analysis tracing output into conditional or pricing branches |
| Transaction order dependence (SWC-114) | Control flow analysis: state reads followed by state writes in separate transactions |
| Auction bid visible before execution without commit-reveal | Pattern detection: public bid(uint256) without corresponding commitment phase |
ContractScan runs these detectors as part of every scan. Findings link to the specific line and recommended remediation. MEV-adjacent issues — missing slippage, missing deadlines, spot price oracle reliance — appear under the DeFi security category in the report.
Audit Your Contract for MEV Vulnerabilities
MEV patterns are common, often missed in manual review, and frequently exploitable at scale. Slippage and deadline issues show up in forks of established protocols because the integration parameters are easy to get wrong and the defaults are often unsafe.
Check your contract for MEV vulnerabilities at https://contract-scanner.raccoonworld.xyz
Related Reading
- Front-Running and MEV: What Solidity Developers Need to Know — MEV mechanics, sandwich anatomy, and JIT liquidity attacks
- AMM and DEX Security: Price Manipulation, Sandwich Attacks, and Liquidity Vulnerabilities — spot price oracle abuse, K-value manipulation, reentrancy via token callbacks, and
getAmountsOut()misuse
This post is for educational purposes. It does not constitute financial, legal, or investment advice. Always conduct a professional audit before deploying smart contracts to production.
Important Notes
This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.