What is MEV (Maximal Extractable Value)?
MEV refers to the maximum value that can be extracted from block production beyond the standard block reward and gas fees. This is done by including, excluding, or reordering transactions within a block.
🎯 Who Extracts MEV?
🤖 Searchers
Bots that scan the mempool for profitable opportunities and submit transactions to extract value.
🏗️ Builders
Entities that construct blocks by ordering transactions to maximize extracted value.
✅ Validators
Receive bids from builders and propose the most profitable block to the network.
The Dark Forest
How Front-Running Works
Front-running occurs when an attacker observes a pending transaction in the mempool and submits their own transaction with a higher gas price to get included before the victim's transaction.
Victim Submits Transaction
User submits a DEX swap for 10 ETH → USDC at market price
Attacker Detects in Mempool 👁️
MEV bot sees the pending tx and calculates profit opportunity
Front-Run Transaction 💥
Bot submits same trade with higher gas to execute first, moving the price
Victim Gets Worse Price
User's trade executes at a worse rate due to price impact
Attacker Profits
Bot may also back-run (sell after) to capture the price difference
Types of MEV Attacks
Sandwich Attacks
The attacker places a buy order before and a sell order after a victim's large trade, profiting from the price movement they cause.
Liquidation Front-Running
Bots race to liquidate undercollateralized positions on lending protocols, earning liquidation bonuses (typically 5-10%).
DEX Arbitrage
Exploiting price differences between DEXs by buying low on one and selling high on another within the same block.
NFT Sniping
Front-running NFT mints or underpriced listings to acquire valuable assets before regular users.
Back-Running
Placing a transaction immediately after a target transaction to profit from its effects (e.g., after oracle updates or large trades).
Time-Bandit Attacks
Re-mining past blocks to capture MEV that was already extracted. Rare but theoretically possible with sufficient hashpower.
Sandwich Attack Deep Dive
Sandwich attacks are the most common form of MEV extraction affecting regular users. Let's understand exactly how they work:
🥪 Anatomy of a Sandwich Attack
Real Example: Uniswap Sandwich
1// Victim wants to swap 10 ETH for USDC
2// Current price: 1 ETH = 2000 USDC
3// Slippage tolerance: 1%
4
5// === ATTACKER FRONT-RUN ===
6// Attacker buys 50 ETH with USDC
7// New price: 1 ETH = 2050 USDC (price pushed up)
8
9// === VICTIM TRANSACTION ===
10// Victim's 10 ETH swap executes
11// Expected: 20,000 USDC (at 2000)
12// Actual: 19,500 USDC (at 1950 effective due to combined impact)
13// Loss: 500 USDC (2.5% worse)
14
15// === ATTACKER BACK-RUN ===
16// Attacker sells 50 ETH for USDC
17// Receives: 102,500 USDC
18// Original: 100,000 USDC spent
19// Profit: 2,500 USDC - gas costs ≈ 2,400 USDCWho Gets Sandwiched?
The MEV Ecosystem
Understanding the players and infrastructure helps you protect yourself:
⚡ Flashbots
A research organization that created infrastructure to democratize MEV extraction and reduce its negative externalities.
- • MEV-Boost: PBS implementation for validators
- • Flashbots Protect: RPC for sandwich protection
- • MEV-Share: Redistributes MEV to users
🔒 Private Mempools
Submit transactions privately to avoid public mempool exposure.
- • Flashbots Protect RPC
- • MEV Blocker (by CoW Protocol)
- • Blocknative Private Pool
- • Eden Network
🏗️ Block Builders
Specialized entities that construct blocks to maximize value extraction.
- • Flashbots Builder
- • BloXroute
- • Builder0x69
- • Beaver Build
📊 MEV Dashboards
Track MEV extraction and analyze blockchain activity.
- •
mevboost.pics - •
eigenphi.io - •
zeromev.org - •
flashbots-data.flashbots.net
Vulnerable Code Patterns
❌ Pattern 1: No Slippage Protection
1// 🚨 VULNERABLE: No minimum output specified
2contract VulnerableSwap {
3 IUniswapV2Router public router;
4
5 function swapExactETHForTokens(address token) external payable {
6 address[] memory path = new address[](2);
7 path[0] = router.WETH();
8 path[1] = token;
9
10 // amountOutMin = 0 means accept ANY output!
11 // Attacker can sandwich for maximum extraction
12 router.swapExactETHForTokens{value: msg.value}(
13 0, // 💥 No slippage protection!
14 path,
15 msg.sender,
16 block.timestamp
17 );
18 }
19}❌ Pattern 2: Public Commit Without Protection
1// 🚨 VULNERABLE: Reveals intent before execution
2contract VulnerableAuction {
3 mapping(address => uint256) public pendingBids;
4
5 // Step 1: User reveals their max bid publicly
6 function commitBid(uint256 amount) external {
7 pendingBids[msg.sender] = amount;
8 // 💥 Anyone can see this in mempool and front-run!
9 }
10
11 // Step 2: Execute bid
12 function executeBid() external {
13 uint256 bid = pendingBids[msg.sender];
14 // Attacker already front-ran with bid + 1
15 _processBid(msg.sender, bid);
16 }
17}❌ Pattern 3: Predictable Liquidation Triggers
1// 🚨 VULNERABLE: Predictable liquidation conditions
2contract VulnerableLending {
3 function liquidate(address user) external {
4 require(isUnderwater(user), "Not liquidatable");
5
6 // Anyone can see when positions become liquidatable
7 // Bots race to call this function first
8 uint256 bonus = calculateBonus(user);
9 _executeLiquidation(user, msg.sender, bonus);
10 }
11
12 // 💥 Problem: No mechanism to fairly distribute liquidations
13 // or prevent front-running of liquidation transactions
14}Protection Strategies
Private Transaction RPCs
Use Flashbots Protect, MEV Blocker, or other private mempools to hide transactions from searchers until they're included in a block.
Tight Slippage Settings
Set appropriate slippage tolerance (0.1-0.5% for stables, 0.5-1% for volatile pairs). Reject transactions that exceed your tolerance.
Commit-Reveal Schemes
Hide transaction details with hashed commits, then reveal after inclusion. Prevents front-running by hiding intent.
Batch Auctions (CoW Protocol)
Use protocols that batch orders off-chain and settle at uniform clearing prices, eliminating ordering-based MEV.
Time-Delayed Execution
Add mandatory delays between intent declaration and execution. Makes front-running less predictable and profitable.
MEV-Share Rebates
Use MEV-Share to receive a portion of the MEV your transactions generate back as a rebate.
Using Flashbots Protect
The easiest way to protect yourself from sandwich attacks is to use Flashbots Protect RPC:
🔧 Setup Instructions
MetaMask
- Settings → Networks → Add Network
- Network Name:
Flashbots Protect - RPC URL:
https://rpc.flashbots.net - Chain ID:
1 - Symbol:
ETH
With MEV-Share Rebates
https://rpc.flashbots.net?hint=hash&hint=logs&hint=default_logs&hint=special_logs&hint=calldata&hint=contract_address&hint=function_selector&builder=flashbots&builder=f1b.ioProgrammatic Usage
1// Using ethers.js with Flashbots Protect
2import { ethers } from 'ethers';
3
4// Connect to Flashbots Protect RPC
5const provider = new ethers.JsonRpcProvider(
6 'https://rpc.flashbots.net'
7);
8
9// Your transactions are now private!
10const wallet = new ethers.Wallet(privateKey, provider);
11
12// Regular transaction - but submitted privately
13const tx = await wallet.sendTransaction({
14 to: uniswapRouter,
15 data: swapCalldata,
16 value: ethers.parseEther("1"),
17 // Transactions go to Flashbots builders, not public mempool
18});
19
20// Wait for inclusion
21const receipt = await tx.wait();
22console.log("Protected swap executed:", receipt.hash);MEV Blocker Alternative
rpc.mevblocker.io) offers similar protection with additional refund mechanisms. Try both and see which works better for your use case.Contract-Level Defenses
✅ Pattern 1: Enforce Slippage in Contract
1// ✅ SECURE: Contract enforces slippage protection
2contract SecureSwap {
3 IUniswapV2Router public router;
4
5 function swapWithProtection(
6 address tokenOut,
7 uint256 minAmountOut, // User specifies minimum
8 uint256 deadline
9 ) external payable {
10 require(minAmountOut > 0, "Must specify min output");
11 require(deadline > block.timestamp, "Expired");
12
13 address[] memory path = new address[](2);
14 path[0] = router.WETH();
15 path[1] = tokenOut;
16
17 // ✅ Reverts if output is below user's minimum
18 router.swapExactETHForTokens{value: msg.value}(
19 minAmountOut,
20 path,
21 msg.sender,
22 deadline
23 );
24 }
25
26 // Helper: Calculate minimum with slippage
27 function getMinOutput(
28 uint256 amountIn,
29 address tokenOut,
30 uint256 slippageBps // e.g., 50 = 0.5%
31 ) external view returns (uint256) {
32 address[] memory path = new address[](2);
33 path[0] = router.WETH();
34 path[1] = tokenOut;
35
36 uint256[] memory amounts = router.getAmountsOut(amountIn, path);
37 uint256 expectedOut = amounts[1];
38
39 // Apply slippage tolerance
40 return expectedOut * (10000 - slippageBps) / 10000;
41 }
42}✅ Pattern 2: Commit-Reveal Scheme
1// ✅ SECURE: Two-phase commit-reveal auction
2contract SecureAuction {
3 struct Commit {
4 bytes32 hash;
5 uint256 commitBlock;
6 }
7
8 mapping(address => Commit) public commits;
9 uint256 public constant REVEAL_DELAY = 10; // blocks
10
11 // Phase 1: Commit hash of bid (hides amount)
12 function commitBid(bytes32 bidHash) external {
13 commits[msg.sender] = Commit({
14 hash: bidHash,
15 commitBlock: block.number
16 });
17 // Attacker can't see bid amount - only hash!
18 }
19
20 // Phase 2: Reveal actual bid after delay
21 function revealBid(uint256 amount, bytes32 salt) external payable {
22 Commit memory commit = commits[msg.sender];
23
24 // Verify delay has passed
25 require(
26 block.number >= commit.commitBlock + REVEAL_DELAY,
27 "Too early to reveal"
28 );
29
30 // Verify hash matches
31 bytes32 expectedHash = keccak256(
32 abi.encodePacked(msg.sender, amount, salt)
33 );
34 require(commit.hash == expectedHash, "Invalid reveal");
35 require(msg.value == amount, "Amount mismatch");
36
37 // Process bid - front-running is now pointless
38 _processBid(msg.sender, amount);
39
40 delete commits[msg.sender];
41 }
42}✅ Pattern 3: Fair Ordering with Chainlink
1// ✅ SECURE: Using Chainlink's Fair Sequencing Service (FSS)
2// Note: FSS is still in development - this shows the concept
3
4contract FairOrderedContract {
5 // Chainlink FSS will ensure fair transaction ordering
6 // based on received time, not gas price
7
8 address public sequencer;
9
10 modifier fairOrdered(bytes memory proof) {
11 // Verify transaction was ordered fairly by sequencer
12 require(
13 _verifySequencerProof(proof),
14 "Invalid sequencer proof"
15 );
16 _;
17 }
18
19 function executeOrder(
20 bytes calldata orderData,
21 bytes calldata sequencerProof
22 ) external fairOrdered(sequencerProof) {
23 // Transaction order guaranteed by Chainlink FSS
24 // Front-running is not possible
25 _processOrder(orderData);
26 }
27}Testing for Front-Running Vulnerabilities
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.19;
3
4import "forge-std/Test.sol";
5import "../src/SecureSwap.sol";
6
7contract FrontRunningTest is Test {
8 SecureSwap swap;
9 address attacker = makeAddr("attacker");
10 address victim = makeAddr("victim");
11
12 function setUp() public {
13 swap = new SecureSwap(address(router));
14 vm.deal(victim, 10 ether);
15 vm.deal(attacker, 100 ether);
16 }
17
18 // ✅ Test: Slippage protection prevents sandwich
19 function test_SlippageProtectionPreventsLoss() public {
20 // Get expected output
21 uint256 minOut = swap.getMinOutput(1 ether, USDC, 50); // 0.5%
22
23 // Attacker front-runs to move price
24 vm.prank(attacker);
25 router.swapExactETHForTokens{value: 50 ether}(
26 0, path, attacker, block.timestamp
27 );
28
29 // Victim's transaction should revert due to slippage
30 vm.prank(victim);
31 vm.expectRevert("UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT");
32 swap.swapWithProtection{value: 1 ether}(USDC, minOut, block.timestamp);
33 }
34
35 // ✅ Test: Zero slippage is rejected
36 function test_RevertWhen_NoSlippageSet() public {
37 vm.prank(victim);
38 vm.expectRevert("Must specify min output");
39 swap.swapWithProtection{value: 1 ether}(USDC, 0, block.timestamp);
40 }
41
42 // ✅ Test: Commit-reveal prevents front-running
43 function test_CommitRevealPreventsInformationLeak() public {
44 // Victim commits hidden bid
45 bytes32 salt = keccak256("secret");
46 bytes32 commitHash = keccak256(
47 abi.encodePacked(victim, 5 ether, salt)
48 );
49
50 vm.prank(victim);
51 auction.commitBid(commitHash);
52
53 // Attacker cannot determine bid amount from hash
54 // (Would need to brute force, impractical for large range)
55
56 // Fast forward past reveal delay
57 vm.roll(block.number + 11);
58
59 // Victim reveals
60 vm.prank(victim);
61 auction.revealBid{value: 5 ether}(5 ether, salt);
62 }
63
64 // ✅ Fuzz: Slippage always enforced
65 function testFuzz_SlippageEnforced(
66 uint256 tradeAmount,
67 uint256 frontRunAmount
68 ) public {
69 tradeAmount = bound(tradeAmount, 0.1 ether, 5 ether);
70 frontRunAmount = bound(frontRunAmount, 1 ether, 50 ether);
71
72 uint256 minOut = swap.getMinOutput(tradeAmount, USDC, 100); // 1%
73
74 // Simulate front-run
75 vm.prank(attacker);
76 router.swapExactETHForTokens{value: frontRunAmount}(
77 0, path, attacker, block.timestamp
78 );
79
80 vm.prank(victim);
81 // Either succeeds within slippage or reverts - never worse
82 try swap.swapWithProtection{value: tradeAmount}(
83 USDC, minOut, block.timestamp
84 ) {
85 uint256 received = IERC20(USDC).balanceOf(victim);
86 assertGe(received, minOut, "Received less than minimum");
87 } catch {
88 // Reverted due to slippage - this is expected protection
89 }
90 }
91}🛠️ MEV Detection Tools
EigenPhi
Analyze MEV transactions and identify sandwich attacks on your protocol.
Flashbots Explorer
View MEV bundles and track searcher activity.
ZeroMEV
Free tool to check if your past transactions were sandwiched.
Tenderly
Simulate transactions to see MEV exposure before execution.
✅ MEV Protection Checklist
📚 Quick Reference
Private RPCs
- • rpc.flashbots.net
- • rpc.mevblocker.io
- • protect.eden.network
MEV-Resistant DEXs
- • CoW Protocol (batch auction)
- • 1inch Fusion
- • Hashflow (RFQ)
Slippage Guidelines
- • Stablecoins: 0.1-0.3%
- • Major pairs: 0.5-1%
- • Volatile: 1-3%