MEV: Maximal Extractable Value and Protection

Understanding MEV, its types, Flashbots, arbitrage mechanisms, and front-running prevention.

Advanced⏱️ 55 min📚 Prerequisites: 2

MEV: Maximal Extractable Value and Protection

MEV (Maximal Extractable Value) is profit extracted by reordering, including, or excluding transactions in blocks.

What is MEV?

MEV represents the maximum value that can be extracted from block production by manipulating transaction order.

Sources of MEV

  1. Arbitrage: Price differences across DEXs
  2. Liquidation: Liquidating undercollateralized positions
  3. Front-running: Seeing pending transactions and executing first
  4. Sandwich Attacks: Placing orders before and after target transaction
  5. Back-running: Following profitable transactions

Types of MEV

1. Arbitrage

RUST
struct ArbitrageOpportunity {
    dex_a: String,
    dex_b: String,
    token: String,
    price_a: u64,
    price_b: u64,
    profit: u64,
}

struct ArbitrageBot {
    dexes: Vec<String>,
    min_profit: u64,
}

impl ArbitrageBot {
    fn find_opportunity(&self, token: &str) -> Option<ArbitrageOpportunity> {
        // Compare prices across DEXs
        // Find profitable arbitrage
        
        // Example: Buy on DEX A, sell on DEX B
        let price_a = self.get_price("dex_a", token);
        let price_b = self.get_price("dex_b", token);
        
        if price_b > price_a {
            let profit = price_b - price_a - self.calculate_fees(price_a);
            if profit > self.min_profit {
                return Some(ArbitrageOpportunity {
                    dex_a: String::from("dex_a"),
                    dex_b: String::from("dex_b"),
                    token: token.to_string(),
                    price_a,
                    price_b,
                    profit,
                });
            }
        }
        
        None
    }
    
    fn execute_arbitrage(&self, opportunity: &ArbitrageOpportunity) -> Result<u64, String> {
        // Execute buy on DEX A
        // Execute sell on DEX B
        // Return profit
        Ok(opportunity.profit)
    }
}

2. Front-Running

RUST
// Front-running: See pending transaction, execute similar one first
struct PendingTransaction {
    hash: String,
    from: String,
    to: String,
    data: Vec<u8>,
    gas_price: u64,
}

struct FrontRunner {
    mempool: Vec<PendingTransaction>,
}

impl FrontRunner {
    fn detect_opportunity(&self, tx: &PendingTransaction) -> bool {
        // Detect if transaction is profitable (e.g., large DEX swap)
        // If yes, front-run it with higher gas price
        
        // Check if it's a DEX swap
        if self.is_dex_swap(tx) {
            // Estimate price impact
            let impact = self.estimate_price_impact(tx);
            return impact > self.min_profit_threshold;
        }
        
        false
    }
    
    fn front_run(&self, target_tx: &PendingTransaction) -> PendingTransaction {
        // Create similar transaction with higher gas price
        PendingTransaction {
            hash: String::from("front_run_tx"),
            from: String::from("attacker"),
            to: target_tx.to.clone(),
            data: self.modify_data(&target_tx.data),
            gas_price: target_tx.gas_price + 1, // Higher gas to get included first
        }
    }
}

3. Sandwich Attack

RUST
struct SandwichAttack {
    front_tx: PendingTransaction,
    target_tx: PendingTransaction,
    back_tx: PendingTransaction,
}

impl SandwichAttack {
    fn create_sandwich(&self, target: &PendingTransaction) -> SandwichAttack {
        // Front-run: Buy before target
        let front_tx = self.create_buy_tx(target);
        
        // Target transaction (victim's swap)
        let target_tx = target.clone();
        
        // Back-run: Sell after target (at better price)
        let back_tx = self.create_sell_tx(target);
        
        SandwichAttack {
            front_tx,
            target_tx,
            back_tx,
        }
    }
    
    fn execute(&self) -> Result<u64, String> {
        // Execute in order: front -> target -> back
        // Profit from price manipulation
        Ok(0)
    }
}

MEV Protection

1. Private Mempools (Flashbots)

Flashbots provides a private mempool to prevent front-running:

RUST
struct FlashbotsBundle {
    transactions: Vec<PendingTransaction>,
    block_number: u64,
    min_timestamp: Option<u64>,
    max_timestamp: Option<u64>,
}

struct FlashbotsProtection {
    bundles: Vec<FlashbotsBundle>,
}

impl FlashbotsProtection {
    fn create_bundle(&mut self, txs: Vec<PendingTransaction>) -> FlashbotsBundle {
        FlashbotsBundle {
            transactions: txs,
            block_number: 0,
            min_timestamp: None,
            max_timestamp: None,
        }
    }
    
    fn submit_bundle(&self, bundle: &FlashbotsBundle) -> Result<(), String> {
        // Submit to Flashbots relay
        // Transactions are private until included in block
        // Prevents front-running
        Ok(())
    }
}

2. Commit-Reveal Scheme

RUST
struct CommitReveal {
    commitment: Vec<u8>, // hash(secret + transaction_data)
    reveal_block: u64,
}

impl CommitReveal {
    fn commit(&self, secret: &[u8], tx_data: &[u8]) -> Vec<u8> {
        // Hash secret + transaction data
        // Submit commitment first
        // Reveal later
        vec![]
    }
    
    fn reveal(&self, secret: &[u8], tx_data: &[u8], commitment: &[u8]) -> bool {
        // Verify commitment matches
        // Execute transaction if valid
        true
    }
}

3. Time-Delayed Execution

RUST
struct TimeDelayedTx {
    transaction: PendingTransaction,
    execute_after: u64, // Block number
}

impl TimeDelayedTx {
    fn create_delayed_tx(tx: PendingTransaction, delay: u64) -> Self {
        TimeDelayedTx {
            transaction: tx,
            execute_after: current_block() + delay,
        }
    }
    
    fn can_execute(&self, current_block: u64) -> bool {
        current_block >= self.execute_after
    }
}

4. Slippage Protection

RUST
struct SwapWithSlippage {
    amount_in: u64,
    min_amount_out: u64, // Minimum acceptable output
    max_slippage: u8,    // Percentage (e.g., 1 = 1%)
}

impl SwapWithSlippage {
    fn execute_swap(&self, pool: &AMMPool) -> Result<u64, String> {
        let amount_out = pool.calculate_output(self.amount_in);
        
        // Check slippage
        let expected_out = self.amount_in; // Simplified
        let slippage = ((expected_out - amount_out) * 100) / expected_out;
        
        if slippage > self.max_slippage as u64 {
            return Err(String::from("Slippage too high"));
        }
        
        if amount_out < self.min_amount_out {
            return Err(String::from("Output below minimum"));
        }
        
        Ok(amount_out)
    }
}

Real-World Examples

  • Flashbots: Private mempool, MEV protection
  • CowSwap: Batch auctions, MEV protection
  • 1inch: DEX aggregator with MEV protection
  • Uniswap V3: Concentrated liquidity, reduced MEV

Best Practices

  1. Use Private Mempools: Flashbots for sensitive transactions
  2. Set Slippage Limits: Protect against sandwich attacks
  3. Batch Transactions: Reduce MEV opportunities
  4. Time Delays: Delay execution to prevent front-running
  5. Commit-Reveal: Hide transaction details until execution

Code Examples

Arbitrage Detection

Simple arbitrage opportunity detection

RUST
struct DEXPrice {
    dex: String,
    price: u64,
}

struct ArbitrageBot {
    min_profit: u64,
    fee_percentage: u8,
}

impl ArbitrageBot {
    fn new(min_profit: u64) -> Self {
        ArbitrageBot {
            min_profit,
            fee_percentage: 3, // 0.3%
        }
    }
    
    fn find_arbitrage(&self, prices: &[DEXPrice]) -> Option<(String, String, u64)> {
        if prices.len() < 2 {
            return None;
        }
        
        // Find best buy and sell prices
        let mut best_buy: Option<&DEXPrice> = None;
        let mut best_sell: Option<&DEXPrice> = None;
        
        for price in prices {
            if best_buy.is_none() || price.price < best_buy.unwrap().price {
                best_buy = Some(price);
            }
            if best_sell.is_none() || price.price > best_sell.unwrap().price {
                best_sell = Some(price);
            }
        }
        
        if let (Some(buy), Some(sell)) = (best_buy, best_sell) {
            if buy.dex == sell.dex {
                return None; // Same DEX, no arbitrage
            }
            
            // Calculate profit after fees
            let price_diff = sell.price - buy.price;
            let fees = (buy.price * self.fee_percentage as u64) / 1000 +
                      (sell.price * self.fee_percentage as u64) / 1000;
            let profit = price_diff.saturating_sub(fees);
            
            if profit > self.min_profit {
                return Some((buy.dex.clone(), sell.dex.clone(), profit));
            }
        }
        
        None
    }
}

fn main() {
    let bot = ArbitrageBot::new(100); // Min profit: 100
    
    let prices = vec![
        DEXPrice { dex: String::from("Uniswap"), price: 2000 },
        DEXPrice { dex: String::from("SushiSwap"), price: 2010 },
        DEXPrice { dex: String::from("Curve"), price: 1995 },
    ];
    
    if let Some((buy_dex, sell_dex, profit)) = bot.find_arbitrage(&prices) {
        println!("Arbitrage opportunity found!");
        println!("Buy on: {}, Sell on: {}", buy_dex, sell_dex);
        println!("Expected profit: {}", profit);
    } else {
        println!("No arbitrage opportunity");
    }
}

Explanation:

Arbitrage bots find price differences across DEXs. They buy on the cheaper DEX and sell on the more expensive one, profiting from the difference. This is a legitimate form of MEV that helps keep prices consistent.

Slippage Protection

Protect against MEV with slippage limits

RUST
struct SwapRequest {
    amount_in: u64,
    min_amount_out: u64,
    max_slippage_percent: u8,
}

struct AMMPool {
    reserve_in: u64,
    reserve_out: u64,
}

impl AMMPool {
    fn calculate_output(&self, amount_in: u64) -> u64 {
        // Constant product formula: x * y = k
        let k = self.reserve_in * self.reserve_out;
        let new_reserve_in = self.reserve_in + amount_in;
        let new_reserve_out = k / new_reserve_in;
        self.reserve_out - new_reserve_out
    }
}

impl SwapRequest {
    fn execute_swap(&self, pool: &AMMPool) -> Result<u64, String> {
        let amount_out = pool.calculate_output(self.amount_in);
        
        // Calculate expected output (simplified: assume 1:1 ratio)
        let expected_out = self.amount_in;
        
        // Calculate slippage percentage
        if expected_out == 0 {
            return Err(String::from("Invalid amount"));
        }
        
        let slippage = if amount_out < expected_out {
            ((expected_out - amount_out) * 100) / expected_out
        } else {
            0
        };
        
        // Check slippage limit
        if slippage > self.max_slippage_percent as u64 {
            return Err(format!("Slippage too high: {}% (max: {}%)", 
                             slippage, self.max_slippage_percent));
        }
        
        // Check minimum output
        if amount_out < self.min_amount_out {
            return Err(format!("Output {} below minimum {}", 
                             amount_out, self.min_amount_out));
        }
        
        Ok(amount_out)
    }
}

fn main() {
    let pool = AMMPool {
        reserve_in: 1000000,
        reserve_out: 2000000,
    };
    
    let swap = SwapRequest {
        amount_in: 10000,
        min_amount_out: 19000, // Minimum acceptable
        max_slippage_percent: 5, // Max 5% slippage
    };
    
    match swap.execute_swap(&pool) {
        Ok(amount_out) => {
            println!("Swap successful! Output: {}", amount_out);
        }
        Err(e) => {
            println!("Swap failed: {}", e);
            println!("This protects against sandwich attacks!");
        }
    }
}

Explanation:

Slippage protection prevents MEV attacks by rejecting transactions if the price moves too much. Users set a maximum acceptable slippage percentage and minimum output amount. This protects against sandwich attacks.

Exercises

Arbitrage Detection

Create a function that detects arbitrage opportunities!

Medium

Starter Code:

RUST
struct Price {
    dex: String,
    price: u64,
}

fn find_arbitrage(prices: &[Price]) -> Option<(String, String)> {
    // Find best buy and sell prices
    // Return (buy_dex, sell_dex) if profitable
}