🌳 Advanced ⏱️ 50 min

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.

🚨 Critical

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.

📋 Mempool Transaction Lifecycle
// 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.

🥩 Sandwich Attack Step-by-Step
// 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.

DefenseProtects AgainstTrade-offsImplementation
minAmountOut parameterSandwich attacksUser must set reasonable slippageRequire check in swap function
Deadline parameterStale transaction reuseExpired txs must be resubmittedrequire(block.timestamp <= deadline)
Commit-revealFront-running of specific actionsTwo-transaction UX frictionHash commitment stored on-chain
Private mempool (Flashbots)All mempool-based front-runningDepends on infrastructureUse Flashbots Protect RPC endpoint
TWAP oracleOracle manipulation front-runningDelayed price responseUniswap V3 oracle or Chainlink

Real-World Exploits

ProtocolYearLossAttack Type
Uniswap (ongoing)2020–present$100M+/yearSandwich attacks on swaps with loose slippage
OpenSea2022$8M+NFT bid sniping — bots front-run low-price listings
Synthetix2019$37M (recovered)Oracle front-running — bots traded ahead of price updates
Various DEXes2021OngoingJIT 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.

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 TypeMEV RiskAttack VectorRecommended Defense
DEX swapCriticalSandwich attackminAmountOut + deadline + private mempool
LiquidationHighCompeting bots racePartial liquidation auctions, Dutch auction pricing
NFT mintHighSniping rare itemsChainlink VRF for post-mint trait assignment
Governance voteMediumFlash loan governanceSnapshot at proposal creation, timelock delay
Oracle updateHighFront-run price updateTWAP, commit-reveal, Chainlink
Limit order executionMediumCompeting executorsFirst-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 ActorRoleTool / InterfaceRevenue Source
SearcherFind profitable tx sequencesFlashbots Protect, MEV-ShareArbitrage/liquidation profits
BuilderAssemble optimal blockMEV-Boost builder APIFee from searcher bundles
RelayEscrow block bids, deliver to proposerMEV-Boost relay (Flashbots, Ultra Sound)Trust intermediary
ProposerSelect highest-paying blockMEV-Boost consensus client pluginBlock bid payment from builder
UserSubmit tx, often MEV victimFlashbots Protect RPC to hide from searchersN/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).