Difficulty: Advanced
Maximal Extractable Value (MEV) refers to the maximum value that can be extracted from block production beyond the standard block reward and gas fees, by including, excluding, or reordering transactions within a block. Originally called Miner Extractable Value (when Ethereum used proof-of-work), the concept was rebranded after The Merge. MEV is a fundamental property of blockchains where transaction ordering matters: because pending transactions are visible in the public mempool, sophisticated actors can observe them and craft profitable ordering strategies. MEV is estimated to have extracted over $600 million from Ethereum users since 2020.
The most common forms of MEV are front-running, back-running, and sandwich attacks. Front-running occurs when a searcher sees a profitable pending transaction (like a large DEX swap that will move the price) and submits their own transaction with a higher gas price to execute first, profiting from the price impact. Back-running is the opposite: placing a transaction immediately after a price-moving event (like an oracle update or large trade) to capture the resulting arbitrage. Sandwich attacks combine both: the attacker places a buy order before a victim's large swap (front-run) and a sell order after it (back-run), profiting from the price impact the victim's trade creates while the victim gets a worse execution price.
Arbitrage is another major category of MEV and is generally considered beneficial. When the same token trades at different prices on different DEXes (Uniswap vs Sushiswap, for example), arbitrageurs equalize prices across markets by buying low and selling high in the same block. Liquidation MEV involves being the first to liquidate an undercollateralized position in lending protocols like Aave or Compound, earning the liquidation bonus. While arbitrage and liquidations serve useful functions (price efficiency and protocol solvency), front-running and sandwich attacks extract value directly from users.
Flashbots was created to address the negative externalities of MEV, particularly the priority gas auctions (PGAs) that were congesting the Ethereum network and the front-running that was harming regular users. Flashbots introduced several innovations: Flashbots Auction (now MEV-Boost) is a marketplace where searchers submit bundles of transactions to block builders, who construct optimal blocks and offer them to validators through MEV-Boost relays. This separates the roles of searching (finding MEV opportunities), building (constructing blocks), and proposing (validators who propose blocks). Flashbots Protect is an RPC endpoint that users can add to their wallets to send transactions directly to block builders, bypassing the public mempool entirely and preventing front-running.
The MEV supply chain in post-Merge Ethereum works as follows: Searchers identify MEV opportunities and create transaction bundles. Block Builders receive bundles from multiple searchers and assemble complete blocks that maximize total value. Relays act as trusted intermediaries between builders and validators, verifying block validity without revealing contents. Validators (proposers) select the most valuable block from the relay and propose it to the network. This Proposer-Builder Separation (PBS) is a key design principle that prevents validators from needing to run sophisticated MEV extraction software themselves, while still allowing them to capture MEV revenue through the auction mechanism.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/
* @title SandwichAttackDemo
* @notice Educational example showing how sandwich attacks work
* @dev DO NOT deploy this - for understanding purposes only
*
* Attack flow:
* 1. Victim submits: swap 10 ETH -> TOKEN on Uniswap (5% slippage)
* 2. Attacker front-runs: buy TOKEN with ETH (pushes price up)
* 3. Victim's swap executes at a worse price (higher TOKEN price)
* 4. Attacker back-runs: sell TOKEN for ETH (profit from inflated price)
*/
contract SandwichAttackDemo {
IUniswapV2Router02 public immutable router;
constructor(address _router) {
router = IUniswapV2Router02(_router);
}
/
* @dev Step 1 (Front-run): Buy token before victim's large swap
* This pushes the token price UP, making victim pay more
*/
function frontRun(
address tokenOut,
uint256 amountOutMin
) external payable {
address[] memory path = new address[](2);
path[0] = router.WETH();
path[1] = tokenOut;
router.swapExactETHForTokens{value: msg.value}(
amountOutMin,
path,
address(this), // Tokens come to this contract
block.timestamp
);
}
/
* @dev Step 2: Victim's swap executes at inflated price
* (This is the victim's normal Uniswap swap)
* The victim set 5% slippage, so the trade still executes
* but they get fewer tokens than they would have without the attack
*/
/
* @dev Step 3 (Back-run): Sell tokens after victim's swap
* The victim's large buy further pushed price up,
* so attacker sells at the peak
*/
function backRun(
address tokenIn,
uint256 amountIn,
uint256 amountOutMin
) external {
IERC20(tokenIn).approve(address(router), amountIn);
address[] memory path = new address[](2);
path[0] = tokenIn;
path[1] = router.WETH();
router.swapExactTokensForETH(
amountIn,
amountOutMin,
path,
msg.sender, // ETH profit goes to attacker
block.timestamp
);
}
}
// === Profit Calculation Example ===
// Pool: 1000 ETH / 1,000,000 TOKEN (price = 0.001 ETH/TOKEN)
// Constant product: k = 1000 * 1,000,000 = 1,000,000,000
//
// 1. Attacker buys with 5 ETH:
// New ETH reserve = 1005
// New TOKEN reserve = k / 1005 = 995,024.87
// Attacker gets: 4,975.12 TOKEN
// New price = 0.001005 ETH/TOKEN (0.5% higher)
//
// 2. Victim swaps 10 ETH -> TOKEN:
// New ETH reserve = 1015
// New TOKEN reserve = k / 1015 = 985,221.67
// Victim gets: 9,803.20 TOKEN (instead of ~9,950 without attack)
// Victim lost: ~147 TOKEN worth of value (~0.15 ETH)
// New price = 0.001015 ETH/TOKEN
//
// 3. Attacker sells 4,975.12 TOKEN:
// Attacker receives: ~5.025 ETH
// Profit: ~0.025 ETH (minus gas)
//
// The attacker profits from the price impact of the victim's trade.
This educational contract shows the mechanics of a sandwich attack on Uniswap V2. The attacker monitors the mempool for large swaps, places a buy order immediately before (front-run) and a sell order immediately after (back-run) the victim's transaction. The profit comes from the price impact created by the victim's trade. In practice, MEV bots use Flashbots bundles to guarantee atomic execution of all three transactions in the exact order needed.
// === Protecting Users from MEV ===
// JavaScript/TypeScript example using ethers.js with Flashbots Protect
import { ethers } from 'ethers';
// Option 1: Flashbots Protect RPC (simplest - just change the RPC URL)
// Users can add this to MetaMask as a custom RPC:
// Network: Flashbots Protect
// RPC URL: https://rpc.flashbots.net
// Chain ID: 1
// This sends transactions directly to block builders, skipping the
// public mempool entirely. No front-running possible.
const flashbotsProvider = new ethers.JsonRpcProvider(
'https://rpc.flashbots.net'
);
// Transactions sent via this provider are private:
const wallet = new ethers.Wallet('0xPRIVATE_KEY', flashbotsProvider);
async function privateSwap() {
const uniswapRouter = new ethers.Contract(
'0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D',
['function swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable'],
wallet
);
// This swap is invisible in the public mempool
// No sandwich attack possible!
const tx = await uniswapRouter.swapExactETHForTokens(
ethers.parseUnits('1000', 18), // amountOutMin
['0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0xTOKEN'], // WETH -> TOKEN
wallet.address,
Math.floor(Date.now() / 1000) + 300,
{ value: ethers.parseEther('1.0') }
);
console.log('Private tx hash:', tx.hash);
// Transaction goes directly to Flashbots block builders
// If not included within ~6 blocks, it expires (no stuck txs)
}
// Option 2: Smart contract level protection
// Using commit-reveal to hide trade parameters
// Option 3: Slippage protection (defense in depth)
async function safeSwapWithTightSlippage() {
// Get the expected output from a quote first
const quoter = new ethers.Contract(
'0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6',
['function quoteExactInputSingle(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn, uint160 sqrtPriceLimitX96) view returns (uint256 amountOut)'],
flashbotsProvider
);
const expectedOut = await quoter.quoteExactInputSingle(
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
3000, // 0.3% fee tier
ethers.parseEther('1.0'),
0
);
// Set tight slippage: 0.5% instead of default 5%
// This limits the profit a sandwich attacker can extract
const minOut = expectedOut * 995n / 1000n; // 0.5% slippage tolerance
console.log(`Expected: ${expectedOut}, Min accepted: ${minOut}`);
// With 0.5% slippage, sandwich profit is capped to ~0.5%
// Most attackers won't bother if gas costs exceed potential profit
}
privateSwap().catch(console.error);
Flashbots Protect is the simplest MEV protection: just send transactions through the Flashbots RPC endpoint instead of the public mempool. Transactions are forwarded directly to block builders, making them invisible to mempool scanners and sandwich bots. Combined with tight slippage tolerances (0.5% instead of the default 5%), users can significantly reduce MEV extraction. The tradeoff is that private transactions may take slightly longer to be included since they are only seen by Flashbots-connected builders.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/
* @title SimpleArbitrage
* @notice Executes cross-DEX arbitrage in a single transaction
* @dev Educational - production bots use assembly and Flashbots bundles
*
* Profitable when: priceA < priceB for the same token pair
* Buy cheap on DEX A, sell expensive on DEX B, keep the difference
*/
interface IUniswapV2Pair {
function getReserves() external view returns (
uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast
);
function swap(
uint256 amount0Out, uint256 amount1Out,
address to, bytes calldata data
) external;
function token0() external view returns (address);
function token1() external view returns (address);
}
contract SimpleArbitrage {
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor() {
owner = msg.sender;
}
/
* @notice Execute arbitrage between two Uniswap V2 pairs
* @param pair0 The pair to buy from (cheaper price)
* @param pair1 The pair to sell to (higher price)
* @param inputToken Token we start and end with (e.g., WETH)
* @param targetToken Token we're arbitraging (e.g., USDC)
* @param amountIn Amount of inputToken to trade
* @param minProfit Minimum profit required (reverts if not met)
*/
function executeArbitrage(
address pair0,
address pair1,
address inputToken,
address targetToken,
uint256 amountIn,
uint256 minProfit
) external onlyOwner {
uint256 balanceBefore = IERC20(inputToken).balanceOf(address(this));
// Step 1: Swap inputToken -> targetToken on pair0 (buy cheap)
uint256 targetAmount = _swap(pair0, inputToken, targetToken, amountIn);
// Step 2: Swap targetToken -> inputToken on pair1 (sell expensive)
uint256 outputAmount = _swap(pair1, targetToken, inputToken, targetAmount);
// Step 3: Verify profit
uint256 balanceAfter = IERC20(inputToken).balanceOf(address(this));
uint256 profit = balanceAfter - balanceBefore;
require(profit >= minProfit, "Insufficient profit");
}
function _swap(
address pair,
address tokenIn,
address tokenOut,
uint256 amountIn
) internal returns (uint256 amountOut) {
IUniswapV2Pair uniPair = IUniswapV2Pair(pair);
(uint112 reserve0, uint112 reserve1, ) = uniPair.getReserves();
bool isToken0 = uniPair.token0() == tokenIn;
(uint112 reserveIn, uint112 reserveOut) = isToken0
? (reserve0, reserve1)
: (reserve1, reserve0);
// Calculate output using Uniswap V2 formula
// amountOut = (amountIn * 997 * reserveOut) / (reserveIn * 1000 + amountIn * 997)
uint256 amountInWithFee = amountIn * 997;
amountOut = (amountInWithFee * reserveOut) /
(uint256(reserveIn) * 1000 + amountInWithFee);
// Transfer input token to the pair
IERC20(tokenIn).transfer(pair, amountIn);
// Execute the swap
if (isToken0) {
uniPair.swap(0, amountOut, address(this), "");
} else {
uniPair.swap(amountOut, 0, address(this), "");
}
}
/
* @notice Check if arbitrage is profitable
* @dev Call this off-chain to find opportunities
*/
function checkProfitability(
address pair0,
address pair1,
address tokenIn,
uint256 amountIn
) external view returns (uint256 profit, bool isProfitable) {
// Get pair0 reserves
(uint112 r0_0, uint112 r0_1, ) = IUniswapV2Pair(pair0).getReserves();
bool isToken0_p0 = IUniswapV2Pair(pair0).token0() == tokenIn;
(uint256 resIn0, uint256 resOut0) = isToken0_p0
? (uint256(r0_0), uint256(r0_1))
: (uint256(r0_1), uint256(r0_0));
// Calculate output from pair0
uint256 midAmount = (amountIn * 997 * resOut0) /
(resIn0 * 1000 + amountIn * 997);
// Get pair1 reserves
(uint112 r1_0, uint112 r1_1, ) = IUniswapV2Pair(pair1).getReserves();
bool isToken0_p1 = IUniswapV2Pair(pair1).token0() != tokenIn; // selling targetToken
(uint256 resIn1, uint256 resOut1) = isToken0_p1
? (uint256(r1_0), uint256(r1_1))
: (uint256(r1_1), uint256(r1_0));
// Calculate output from pair1
uint256 finalAmount = (midAmount * 997 * resOut1) /
(resIn1 * 1000 + midAmount * 997);
if (finalAmount > amountIn) {
profit = finalAmount - amountIn;
isProfitable = true;
}
}
/// @notice Withdraw tokens (profit extraction)
function withdraw(address token) external onlyOwner {
uint256 balance = IERC20(token).balanceOf(address(this));
IERC20(token).transfer(owner, balance);
}
receive() external payable {}
}
This educational arbitrage contract buys a token on one DEX pair (where it is cheaper) and sells it on another (where it is more expensive) in a same transaction, atomically capturing the price difference as profit. The checkProfitability function can be called off-chain to scan for opportunities. In practice, production MEV bots use highly optimized assembly code, flash loans for capital-free arbitrage, and Flashbots bundles for guaranteed atomic execution without the risk of partial execution or front-running by other searchers.
MEV, Front-running, Sandwich Attacks, Flashbots Protect, Block Building