Front-Running & MEV
Every transaction you submit to Ethereum sits in a public waiting room called the mempool before it's included in a block. Anyone — including sophisticated bots — can see what you're about to do. Front-running exploits this transparency: an attacker copies your profitable transaction, submits it with higher gas, and their copy gets executed first. This is not a bug that can be patched — it's a fundamental property of transparent blockchains. But it can be mitigated at the application level.
MEV (Maximal Extractable Value) extraction is estimated at over $1 billion per year on Ethereum alone. Every function whose output depends on timing or transaction ordering is a potential MEV target. Sandwich attacks on DEX swaps cost retail users hundreds of millions in additional slippage annually.
The Mempool: Public Transaction Queue
When you submit a transaction, it enters the mempool — a distributed database of pending transactions visible to all nodes. Validators pick transactions from the mempool when building blocks, typically ordering by gas price (highest first). This ordering can be manipulated.
// Transaction journey:
// 1. User signs and broadcasts tx → enters mempool (public!)
// 2. MEV bot detects profitable opportunity in mempool
// 3. Bot submits similar tx with higher gas price
// 4. Validator includes bot's tx FIRST (higher gas = higher priority)
// 5. Bot's tx executes, captures value
// 6. User's tx executes in modified state — receives worse outcome
// Mempool visibility:
// - All pending transactions are public before inclusion
// - Gas price determines ordering within a block
// - Validators can reorder, include, or exclude transactions freely
// - Private mempools (Flashbots) bypass this public exposure
// What bots can see in your pending transaction:
{
"to": "0xUniswapRouter",
"data": "swapETHForTokens(1000 USDC, minOut=950, ...)", // ← visible!
"value": "1 ETH", // ← visible!
"gasPrice": "20 gwei" // ← bot will use 21+ gwei
}Sandwich Attack: Front-Run + Back-Run
The sandwich attack is the most common MEV extraction on DEXes. The bot spots your swap, manipulates the price by front-running, then sells into the price impact you created. You end up buying at a worse price, and the bot extracts the difference.
// Victim submits: swap 10 ETH for USDC, accepts up to 2% slippage
// Pool state: 1000 ETH + 2,000,000 USDC (price: 2000 USDC/ETH)
// STEP 1: Bot front-runs with larger buy (same token, higher gas)
// Bot buys 100 ETH worth of USDC first
// Pool after bot's buy: ~1100 ETH + ~1,818,182 USDC
// New price: ~1818 USDC/ETH (price moved up — victim will pay more)
// STEP 2: Victim's tx executes at the new (worse) price
// Victim buys 10 ETH worth of USDC at ~1818 USDC/ETH
// Victim receives ~18,182 USDC instead of expected ~20,000 USDC
// Victim lost ~1818 USDC to slippage manipulation
// STEP 3: Bot immediately back-runs — sells USDC back for ETH
// Bot sells its USDC back at the price after victim's purchase
// Bot profits from the round-trip price manipulation
// The vulnerability: no minimum output protection
function swapVulnerable(uint256 amountIn) external {
// ❌ No minAmountOut parameter — attacker can reduce output to near zero
uniswap.swapExactTokensForTokens(amountIn, 0, path, msg.sender, deadline);
// ↑ 0 = accept any amount
}</code></div>
Specific Front-Running Vulnerability Patterns
⚠️ Pattern 1: ERC20 Approval Race Condition
// Classic ERC20 approval vulnerability:
// Alice approves Bob for 100 tokens. Later she wants to change to 50.
// State: Alice has approved Bob for 100 tokens
// Alice submits: approve(Bob, 50) ← reduces allowance
// Bob sees this in the mempool and front-runs:
// Bob calls: transferFrom(Alice, Bob, 100) ← uses full old approval
// Then Alice's tx: approve(Bob, 50) ← sets new allowance
// Bob calls: transferFrom(Alice, Bob, 50) ← uses new approval too!
// Result: Bob stole 150 tokens instead of the intended 50 allowance
// ✅ FIX: Use increaseAllowance/decreaseAllowance pattern
// Or set to 0 first, then set to new value
// Step 1: Set approval to 0
token.approve(bob, 0);
// Step 2: Then set to desired amount
token.approve(bob, 50);
// EIP-2612 permit() is also vulnerable to front-running!
// Anyone can front-run a permit() call to execute it first,
// making the original caller's use fail (but funds not stolen)
⚠️ Pattern 2: NFT Mint Sniping
// Vulnerable NFT mint with predictable rare trait assignment
contract VulnerableNFT {
uint256 public nextTokenId;
mapping(uint256 => uint256) public traits;
// ❌ Anyone can calculate which tokenId will have rare trait
// Bots watch the pending tx, front-run with same mint call
function mint() external payable {
require(msg.value >= 0.1 ether);
uint256 tokenId = nextTokenId++;
// Trait determined by block data — predictable before inclusion
traits[tokenId] = uint256(keccak256(abi.encodePacked(block.timestamp, tokenId))) % 100;
}
}
// ✅ Mitigation: Commit-reveal or VRF-based trait assignment
// Assign traits after mint using Chainlink VRF — bot can't know traits before minting
🔒 Defense 1: Minimum Output Protection
contract ProtectedDEX {
// ✅ Always require a minimum output amount
// Sandwich attacks become unprofitable if slippage tolerance is tight
function swapWithProtection(
uint256 amountIn,
uint256 minAmountOut, // ← User-specified minimum
uint256 deadline // ← Transaction must include by this timestamp
) external {
require(block.timestamp <= deadline, "Transaction expired");
uint256 amountOut = _executeSwap(amountIn);
// ✅ Revert if output is below user's acceptable minimum
// Sandwich bot can't profitably move price enough without triggering this
require(amountOut >= minAmountOut, "Insufficient output amount");
}
function _executeSwap(uint256 amountIn) internal returns (uint256) {
/* swap implementation */
return 0;
}
}
🔒 Defense 2: Commit-Reveal Scheme
contract CommitReveal {
mapping(address => bytes32) public commitments;
mapping(address => uint256) public commitBlock;
// Phase 1: User commits to an action without revealing details
// What you commit: keccak256(action + secret)
// The action (e.g., "buy 100 tokens") is hidden inside the hash
function commit(bytes32 commitment) external {
commitments[msg.sender] = commitment;
commitBlock[msg.sender] = block.number;
}
// Phase 2: After at least 1 block, reveal the action
// Front-running the reveal is too late — the commit already locked the intent
function reveal(uint256 action, bytes32 secret) external {
require(block.number > commitBlock[msg.sender], "Too early");
require(
commitments[msg.sender] == keccak256(abi.encodePacked(action, secret)),
"Commitment mismatch"
);
_executeAction(action);
}
function _executeAction(uint256 action) internal { /* ... */ }
// ⚠️ LIMITATION: If reveal is front-run (transaction copied),
// original user's reveal fails. Attacker can grief but not profit.
// Commit-reveal does not prevent denial-of-service griefing.
}
MEV Infrastructure: Flashbots and Private Mempools
📖 How Flashbots Works
Flashbots allows users and searchers to send transaction bundles directly to validators, bypassing the public mempool. This protects users from front-running because their transactions are never publicly visible before inclusion. MEV Blocker is a free RPC endpoint that routes transactions through private mempool infrastructure by default.
Defense Protects Against Trade-offs Implementation
minAmountOut parameter Sandwich attacks User must set reasonable slippage Require check in swap function
Deadline parameter Stale transaction reuse Expired txs must be resubmitted require(block.timestamp <= deadline)
Commit-reveal Front-running of specific actions Two-transaction UX friction Hash commitment stored on-chain
Private mempool (Flashbots) All mempool-based front-running Depends on infrastructure Use Flashbots Protect RPC endpoint
TWAP oracle Oracle manipulation front-running Delayed price response Uniswap V3 oracle or Chainlink
Real-World Exploits
Protocol Year Loss Attack Type
Uniswap (ongoing) 2020–present $100M+/year Sandwich attacks on swaps with loose slippage
OpenSea 2022 $8M+ NFT bid sniping — bots front-run low-price listings
Synthetix 2019 $37M (recovered) Oracle front-running — bots traded ahead of price updates
Various DEXes 2021 Ongoing JIT liquidity provision — bots provide and remove LP in same block
Key Takeaways
- The mempool is public. Any transaction visible in the mempool can be front-run. Design protocols assuming all pending transactions are observable by adversaries.
- Minimum output amounts are mandatory on DEX integrations. Any swap with a zero or very loose minAmountOut is a sandwich target. The slippage tolerance should be the tightest the user can accept.
- Deadline parameters prevent stale transaction reuse. Without a deadline, a transaction submitted during normal gas conditions can be held and executed during favorable price conditions.
- TWAP oracles resist price manipulation better than spot prices. A flash loan cannot move a TWAP significantly — it requires sustained capital over many blocks.
- Any function whose output depends on ordering is an MEV target. Liquidations, auctions, governance votes — all require careful design to prevent ordering exploitation.
ToolFlashbots — MEV Infrastructure
→
AnalyticsEigenPhi — MEV Analytics Dashboard
→
DocumentationEthereum.org MEV Documentation
→
Foundry Test: Front-Running Protection
🧪 Foundry Test — Slippage Protection and Deadline Enforcement
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
contract FrontRunningTest is Test {
ProtectedDEX public dex;
function setUp() public {
dex = new ProtectedDEX();
}
/// @notice Expired transactions revert
function test_expiredDeadlineReverts() public {
// Warp time past deadline
uint256 deadline = block.timestamp + 60;
vm.warp(block.timestamp + 120); // Move 2 minutes forward
vm.expectRevert("Transaction expired");
dex.swapWithProtection(1 ether, 900, deadline);
}
/// @notice Insufficient output reverts (slippage protection)
function test_slippageProtectionReverts() public {
uint256 minOutput = 2000; // Expects 2000 tokens minimum
uint256 deadline = block.timestamp + 1 days;
// DEX returns only 1500 (simulated sandwich attack impact)
vm.mockCall(
address(dex),
abi.encodeWithSelector(ProtectedDEX._executeSwap.selector),
abi.encode(uint256(1500))
);
vm.expectRevert("Insufficient output amount");
dex.swapWithProtection(1 ether, minOutput, deadline);
}
/// @notice Fuzz: deadline in the past always reverts
function testFuzz_pastDeadlineAlwaysReverts(uint256 secondsPast) public {
vm.assume(secondsPast > 0 && secondsPast < 365 days);
uint256 deadline = block.timestamp;
vm.warp(block.timestamp + secondsPast);
vm.expectRevert("Transaction expired");
dex.swapWithProtection(1 ether, 0, deadline);
}
}
MEV as a Protocol Design Consideration
Modern protocol design must account for MEV from the beginning — not as an afterthought. Here is a framework for evaluating MEV risk in any protocol function:
Function Type MEV Risk Attack Vector Recommended Defense
DEX swap Critical Sandwich attack minAmountOut + deadline + private mempool
Liquidation High Competing bots race Partial liquidation auctions, Dutch auction pricing
NFT mint High Sniping rare items Chainlink VRF for post-mint trait assignment
Governance vote Medium Flash loan governance Snapshot at proposal creation, timelock delay
Oracle update High Front-run price update TWAP, commit-reveal, Chainlink
Limit order execution Medium Competing executors First-come-first-served or randomized selection
📖 MEV and Proposer-Builder Separation (PBS)
After Ethereum's Merge to Proof-of-Stake, MEV extraction became more structured through Proposer-Builder Separation (PBS). Block builders specialize in constructing maximally profitable blocks, while validators (proposers) simply choose the highest-paying block. Flashbots' MEV-Boost relayer infrastructure now processes over 90% of Ethereum blocks, meaning most MEV is extracted through organized builder competition rather than individual validator manipulation.
Liquidation Frontrunning: Multiple Bots Competing
Liquidations in lending protocols are open to anyone — a position below the health factor can be liquidated by any address that calls the liquidate function. This creates a competitive race among bots. The winning bot earns the liquidation bonus (typically 5-15%). When a large position becomes liquidatable, dozens of bots may submit the same liquidation simultaneously.
⚠️ Liquidation Race — All Bots Compete Simultaneously
// When a $5M position becomes liquidatable with a 10% bonus:
// Potential reward = $500,000
// Hundreds of MEV bots see it instantly
// Each submits the same liquidation transaction
// Winner: whoever has the fastest latency + highest gas price
// Losers: gas cost but no reward — reverted transactions
// Total gas wasted by losing bots: potentially millions of gas units
contract LiquidationRace {
mapping(address => uint256) public collateral;
mapping(address => uint256) public debt;
uint256 public constant LIQUIDATION_BONUS = 1100; // 10% bonus
// ❌ First-come-first-served liquidations create maximum MEV extraction
function liquidate(address borrower) external {
require(_isUnderwater(borrower), "Position is healthy");
uint256 seized = (debt[borrower] * LIQUIDATION_BONUS) / 1000;
collateral[borrower] -= seized;
debt[borrower] = 0;
// Transfer seized collateral to liquidator
(bool ok,) = msg.sender.call{value: seized}("");
require(ok);
}
// ✅ Alternative: Dutch auction liquidation
// Bonus starts high and decreases over time
// First legitimate liquidator wins at current bonus level
// Less MEV extraction — no need to outgas everyone else
mapping(address => uint256) public liquidationStart;
function liquidateDutchAuction(address borrower) external {
require(_isUnderwater(borrower), "Position is healthy");
uint256 startTime = liquidationStart[borrower];
if (startTime == 0) liquidationStart[borrower] = block.timestamp;
// Bonus decreases from 15% to 5% over 30 minutes
uint256 elapsed = block.timestamp - liquidationStart[borrower];
uint256 bonus = elapsed > 30 minutes ? 1050 : 1150 - (elapsed * 100 / (30 minutes));
uint256 seized = (debt[borrower] * bonus) / 1000;
// First liquidator at current bonus wins — no gas war needed
collateral[borrower] -= seized;
debt[borrower] = 0;
}
function _isUnderwater(address borrower) internal view returns (bool) {
return collateral[borrower] < debt[borrower];
}
}
EIP-2612 permit() and Front-Running
⚠️ permit() Front-Running — Griefing Pattern
// EIP-2612 permit() allows gasless approvals via a signed message
// The signed permit can be submitted by ANYONE — not just the token owner
// GRIEFING ATTACK on permit-based deposits:
// 1. User submits permit + deposit in one transaction
// 2. Bot sees the permit signature in the mempool
// 3. Bot front-runs and calls token.permit(user, spender, amount, v, r, s)
// 4. Permit is now used — nonce incremented
// 5. User's combined permit+deposit transaction arrives and fails
// 6. User is griefed — no funds moved but they have to retry
// NOTE: This is usually griefing, not theft — no funds stolen
// But it can cause denial-of-service for permit-based interactions
// ✅ FIX: Wrap permit in try/catch so deposit still works if permit already used
function depositWithPermit(
uint256 amount,
uint256 deadline,
uint8 v, bytes32 r, bytes32 s
) external {
// ✅ Try permit — if already used (front-run), we continue anyway
try token.permit(msg.sender, address(this), amount, deadline, v, r, s) {}
catch {} // Silently ignore permit failure — allowance may already be set
// Proceed with transfer — works whether permit just ran or was pre-set
token.transferFrom(msg.sender, address(this), amount);
}
JIT Liquidity MEV
Just-in-Time (JIT) liquidity is an MEV strategy unique to Uniswap V3's concentrated liquidity model. A searcher observes a large pending swap in the mempool, adds a massive concentrated liquidity position in the exact price range of the swap just before it executes, collects the fee from the swap, then removes the liquidity in the next block. The original LP earns almost nothing from that trade despite having capital deployed. JIT is not directly harmful to traders but it creates an unequal playing field for LPs.
JIT Liquidity Attack Sequence
// Block N (pending):
// User tx: Swap 1,000 ETH → USDC at ~$3,000/ETH price range
//
// Searcher sees mempool, builds bundle:
// Tx 1 (first in block): Add 50,000,000 USDC + 50,000 ETH liquidity
// in narrow tick range [$2,999–$3,001]
// Tx 2 (original user): Swap 1,000 ETH → USDC
// → Searcher LP position captures ~99% of the fee
// Tx 3 (last in block): Remove all liquidity + collect fees
//
// Result:
// User: Got USDC at fair price (no harm to trader)
// Regular LPs: Lost most of the fee income from this large trade
// Searcher: Earned LP fees with zero price exposure (in/out same block)
// Defense for protocols: minimum liquidity hold duration
mapping(uint256 => uint256) public positionAddedAt;
modifier holdRequired(uint256 tokenId) {
require(
block.number > positionAddedAt[tokenId] + MIN_HOLD_BLOCKS,
"Must hold position for minimum blocks"
);
_;
}
function removeLiquidity(uint256 tokenId, ...)
external
holdRequired(tokenId)
{
// Remove liquidity only after hold period
}
Proposer-Builder Separation and MEV Infrastructure
Ethereum's PBS (Proposer-Builder Separation) post-Merge fundamentally changed MEV dynamics. Block builders compete to construct the most profitable block; the winning builder's block is proposed by the validator who wins the PBS auction. Flashbots MEV-Boost enables validators to outsource block building to specialized builders, resulting in a market where MEV extraction is highly professionalized.
🏗 The MEV Supply Chain
Searchers detect profitable opportunities (arbitrage, liquidations, sandwich attacks) and submit bundles to block builders via MEV-Share or private RPC endpoints. Builders assemble the most profitable block ordering and bid for inclusion by proposers (validators) via relays (MEV-Boost). Validators accept the highest-paying block header without seeing the full block content until after they sign. This separation means validators earn more (higher tips) but have less direct MEV control.
MEV Actor Role Tool / Interface Revenue Source
Searcher Find profitable tx sequences Flashbots Protect, MEV-Share Arbitrage/liquidation profits
Builder Assemble optimal block MEV-Boost builder API Fee from searcher bundles
Relay Escrow block bids, deliver to proposer MEV-Boost relay (Flashbots, Ultra Sound) Trust intermediary
Proposer Select highest-paying block MEV-Boost consensus client plugin Block bid payment from builder
User Submit tx, often MEV victim Flashbots Protect RPC to hide from searchers N/A — pays MEV cost
Anti-MEV Protocol Design Principles
Protocols can reduce MEV exposure through architecture choices rather than hoping users take defensive measures. The following design patterns significantly reduce profitable MEV opportunities against your users:
Dutch Auction Order Matching — Reducing Sandwich Attack Surface
contract DutchAuctionSwap {
struct Order {
address token;
uint256 amountIn;
uint256 startPrice; // High starting price (unfavorable to filler)
uint256 endPrice; // Low ending price (limit price for user)
uint256 startTime;
uint256 duration; // Price decays over this window
address user;
bytes32 orderHash;
}
/// @notice Current price decays from startPrice to endPrice over duration
/// Fillers compete to fill at best price — naturally finds market price
/// No slippage tolerance needed — price is deterministic at fill time
function getCurrentPrice(Order calldata order)
public view
returns (uint256)
{
uint256 elapsed = block.timestamp - order.startTime;
if (elapsed >= order.duration) return order.endPrice;
uint256 priceDrop = order.startPrice - order.endPrice;
uint256 dropSoFar = (priceDrop * elapsed) / order.duration;
return order.startPrice - dropSoFar;
}
/// Filler fills order at current decay price — MEV from sandwich is eliminated
/// because the price is deterministic regardless of block position
function fillOrder(Order calldata order, uint256 amountOut) external {
uint256 currentPrice = getCurrentPrice(order);
uint256 minOut = (order.amountIn * currentPrice) / 1e18;
require(amountOut >= minOut, "Insufficient output");
// Transfer tokens and settle
}
}
Key Takeaways
- Front-running exploits the mempool's transparency — any transaction visible before inclusion can be copied, displaced, or sandwiched by well-resourced searchers.
- Slippage tolerance (
minAmountOut) and deadline parameters are essential for every swap — they are the primary user-level defense against sandwich attacks.
- Commit-reveal schemes prevent front-running of sensitive operations (NFT minting, auctions, randomness) but introduce a two-transaction UX cost.
- Private mempools (Flashbots Protect, MEV Blocker) hide transactions from searchers but add latency and centralization risk.
- Protocol-level defenses (Dutch auctions, batch auctions, TWAP pricing, permit() griefing resistance) provide stronger guarantees than user-level defenses.
- MEV is unavoidable on transparent blockchains — the goal is to minimize harmful MEV (sandwiching) while tolerating or capturing beneficial MEV (arbitrage, liquidations).