DEV Community

ohmygod
ohmygod

Posted on

Solana MEV Defense in 2026: How Sandwich Bots Extracted $500M — And the 6 Protocol-Level Defenses That Actually Work

Sandwich bots extracted between $370M and $500M from Solana users over the past 16 months. One single program — vpeNALD…Noax38b — accounts for nearly half of all sandwich attacks on the network, executing 51,600 transactions daily with an 88.9% success rate and pocketing ~2,200 SOL ($450K) per day.

And it's getting worse. Wide (multi-slot) sandwich attacks now account for 93% of all sandwich activity, extracting over 529,000 SOL in the past year alone. These attacks span multiple validator slots, making them nearly invisible to traditional detection methods.

This article breaks down exactly how Solana sandwich attacks work in 2026, why Firedancer's arrival changes the MEV landscape, and the 6 protocol-level defenses that actually protect your users' trades.


How Solana Sandwich Attacks Actually Work

Unlike Ethereum's mempool-based MEV, Solana sandwich attacks exploit a different architectural property: the relationship between validators and transaction ordering within blocks.

The Classic Sandwich

Block N:
  tx[0]: Attacker buys TOKEN (front-run)
  tx[1]: Victim swaps SOL → TOKEN (user's trade)
  tx[2]: Attacker sells TOKEN (back-run)
Enter fullscreen mode Exit fullscreen mode

The attacker's front-run pushes the price up. The victim buys at an inflated price. The attacker's back-run captures the difference. The victim gets fewer tokens than they should.

The Wide Sandwich (93% of attacks in 2025-2026)

Slot N (Attacker-Controlled Validator):
  tx[last]: Attacker buys TOKEN (front-run)

Slot N+1 (Any Validator):
  tx[mid]: Victim's swap executes at inflated price

Slot N+2 (Attacker-Controlled Validator):
  tx[0]: Attacker sells TOKEN (back-run)
Enter fullscreen mode Exit fullscreen mode

Wide sandwiches split the front-run and back-run across different slots. This has two advantages for attackers:

  1. Harder to detect — the three transactions don't appear in the same block
  2. Validator collusion — attackers only need to control one slot to front-run, then wait for the natural price impact

The Jito Bundle Attack Vector

With 92% of Solana validators running the Jito-Solana client, the Jito block engine has become the primary MEV marketplace. Searchers submit bundles — atomic groups of transactions — with tips to validators for priority inclusion.

# Simplified searcher logic
def sandwich_opportunity(pending_tx):
    """Detect and exploit a sandwichable swap."""
    token_in = pending_tx.token_in
    token_out = pending_tx.token_out
    amount = pending_tx.amount
    slippage = pending_tx.max_slippage  # Usually 0.5-3%

    # Calculate if the price impact from front-running
    # stays within victim's slippage tolerance
    price_impact = estimate_impact(token_out, amount)

    if price_impact < slippage:
        # Profitable sandwich possible
        front_run = build_buy_tx(token_out, optimal_amount)
        back_run = build_sell_tx(token_out, optimal_amount)

        bundle = JitoBundle([front_run, pending_tx, back_run])
        bundle.tip = calculate_optimal_tip(expected_profit)
        submit_bundle(bundle)
Enter fullscreen mode Exit fullscreen mode

The critical insight: the victim's slippage tolerance is their vulnerability. A 1% slippage setting on a $10,000 swap means an attacker can extract up to $100. Across millions of daily transactions, this adds up fast.


Why Firedancer Changes Everything (And Not How You'd Expect)

Firedancer, Jump Crypto's independently developed Solana validator client, is rolling out through 2026 with dramatic performance improvements — up to 1M TPS in testing. But its MEV implications are complex:

Shorter Block Times = Shorter Attack Windows

Firedancer's optimized block packing means transactions spend less time in any "pending" state. This theoretically shrinks the window for sandwich detection and execution.

But: Firedancer also supports a "Revenue Mode" that explicitly prioritizes MEV capture. Validators running Firedancer in revenue mode may actually be more efficient at extracting MEV, not less.

More Transactions = More Opportunities

Higher throughput means more swaps per block, which means more sandwich opportunities. The raw volume of extractable value goes up even as individual attack windows shrink.

The Verification Lag Problem

Firedancer's dynamic block sizing and the "Skip-Vote Phenomenon" create verification lag — a gap between when a transaction is included in a block and when it's confirmed by the network. For time-sensitive operations like DEX swaps, this lag can be exploited:

// VULNERABLE: No slot-awareness in price check
pub fn execute_swap(ctx: Context<Swap>, amount: u64, min_out: u64) -> Result<()> {
    let price = get_oracle_price(&ctx.accounts.oracle)?;
    // Price might be stale by 1-2 slots due to verification lag
    let output = calculate_output(amount, price);
    require!(output >= min_out, SwapError::SlippageExceeded);
    // ... execute
    Ok(())
}

// FIXED: Explicit slot freshness check
pub fn execute_swap(ctx: Context<Swap>, amount: u64, min_out: u64) -> Result<()> {
    let clock = Clock::get()?;
    let oracle_data = get_oracle_data(&ctx.accounts.oracle)?;

    // Reject if oracle update is more than 2 slots old
    require!(
        clock.slot - oracle_data.last_update_slot <= 2,
        SwapError::StaleOracle
    );

    let output = calculate_output(amount, oracle_data.price);
    require!(output >= min_out, SwapError::SlippageExceeded);
    // ... execute
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Defense #1: Jito's dontfront — First-Index Guarantee

Jito's dontfront feature ensures your transaction is placed at index 0 within any bundle, preventing front-running within the same block.

How to Use It

import { Connection, Transaction } from '@solana/web3.js';

// Submit through Jito's block engine with dontfront
const JITO_BLOCK_ENGINE = 'https://mainnet.block-engine.jito.wtf/api/v1/transactions';

async function submitProtectedTx(tx: Transaction, keypair: Keypair) {
    const serialized = tx.serialize();

    const response = await fetch(JITO_BLOCK_ENGINE, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            jsonrpc: '2.0',
            id: 1,
            method: 'sendTransaction',
            params: [
                Buffer.from(serialized).toString('base64'),
                {
                    encoding: 'base64',
                    // dontfront: transaction gets index 0 priority
                    skipPreflight: true,
                    maxRetries: 3
                }
            ]
        })
    });

    return response.json();
}
Enter fullscreen mode Exit fullscreen mode

Limitations

  • Only protects within a single block — wide sandwiches across slots are unaffected
  • Requires submitting through Jito's endpoint
  • Costs additional tips for priority placement
  • 92% validator adoption means 8% of slots have no Jito protection

Defense #2: Private Transaction Relays

Private relays encrypt transactions until execution, eliminating the observation window that sandwich bots need.

Astralane's Encrypted Channel

// Submit through Astralane's private relay
const ASTRALANE_RELAY = 'https://relay.astralane.io/v1/submit';

async function submitPrivate(tx: Transaction, keypair: Keypair) {
    const signed = await signTransaction(tx, keypair);

    // Transaction is encrypted in transit
    // Only becomes visible at execution time
    const response = await fetch(ASTRALANE_RELAY, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-API-Key': process.env.ASTRALANE_KEY
        },
        body: JSON.stringify({
            transaction: Buffer.from(signed.serialize()).toString('base64'),
            options: {
                mevProtection: true,
                maxRetries: 5,
                priorityFee: 'auto'
            }
        })
    });

    return response.json();
}
Enter fullscreen mode Exit fullscreen mode

BloXroute's Leader-Aware Routing

BloXroute takes a different approach — scoring upcoming slot leaders for sandwich risk and routing around dangerous validators:

// BloXroute scores validators and avoids known sandwich-colluding ones
const BLOXROUTE_API = 'https://solana.trader-api.bloxroute.com/v1';

async function submitWithLeaderAwareness(tx: Transaction) {
    const response = await fetch(`${BLOXROUTE_API}/submit`, {
        method: 'POST',
        headers: {
            'Authorization': process.env.BLOXROUTE_AUTH,
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            transaction: serializeTx(tx),
            submitStrategy: {
                // Route through multiple channels for redundancy
                multipath: true,
                // Score leaders and avoid high-risk validators
                leaderAwareMevProtection: true,
                // Use Jito, Paladin, and bloXroute simultaneously
                channels: ['jito', 'paladin', 'bloxroute']
            }
        })
    });

    return response.json();
}
Enter fullscreen mode Exit fullscreen mode

Defense #3: Program-Level Slippage with TWAP Validation

The most robust defense lives in the smart contract itself. Instead of trusting user-provided min_out alone, validate against a TWAP (Time-Weighted Average Price):

use anchor_lang::prelude::*;

#[program]
pub mod protected_swap {
    use super::*;

    pub fn swap_with_twap_guard(
        ctx: Context<SwapGuarded>,
        amount_in: u64,
        min_amount_out: u64,
        max_price_deviation_bps: u16,  // e.g., 100 = 1%
    ) -> Result<()> {
        let oracle = &ctx.accounts.oracle;
        let clock = Clock::get()?;

        // 1. Get spot price
        let spot_price = oracle.get_spot_price()?;

        // 2. Get TWAP over last 30 seconds
        let twap_price = oracle.get_twap(
            clock.unix_timestamp - 30,
            clock.unix_timestamp
        )?;

        // 3. Reject if spot deviates from TWAP beyond threshold
        // This catches sandwich-inflated prices
        let deviation = if spot_price > twap_price {
            ((spot_price - twap_price) * 10_000) / twap_price
        } else {
            ((twap_price - spot_price) * 10_000) / twap_price
        };

        require!(
            deviation <= max_price_deviation_bps as u64,
            SwapError::PriceDeviationTooHigh
        );

        // 4. Execute swap with validated price
        let output = execute_swap_internal(
            &ctx.accounts.pool,
            amount_in,
            spot_price
        )?;

        require!(output >= min_amount_out, SwapError::SlippageExceeded);

        // 5. Post-swap sanity check
        let post_price = ctx.accounts.pool.get_current_price()?;
        let impact = ((spot_price - post_price) * 10_000) / spot_price;
        require!(
            impact <= max_price_deviation_bps as u64 * 2,
            SwapError::ExcessivePriceImpact
        );

        Ok(())
    }
}

#[derive(Accounts)]
pub struct SwapGuarded<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(mut)]
    pub pool: Account<'info, LiquidityPool>,
    /// CHECK: Validated by oracle program CPI
    pub oracle: AccountInfo<'info>,
    pub oracle_program: Program<'info, PythOracle>,
}
Enter fullscreen mode Exit fullscreen mode

Why this works: A sandwich attack temporarily inflates the spot price. But the TWAP — averaged over 30 seconds of trading — won't spike from a single front-run transaction. If spot diverges from TWAP by more than 1%, the swap reverts, and the attacker's front-run transaction costs them gas with zero profit.


Defense #4: Commit-Reveal Swap Pattern

Split swaps into two transactions to make front-running impossible:

#[program]
pub mod commit_reveal_swap {
    use super::*;

    /// Phase 1: Commit — user locks funds and submits hash of swap params
    pub fn commit_swap(
        ctx: Context<CommitSwap>,
        commitment_hash: [u8; 32],  // hash(amount, min_out, nonce)
        deposit_amount: u64,
    ) -> Result<()> {
        let commitment = &mut ctx.accounts.commitment;
        let clock = Clock::get()?;

        // Lock funds in escrow
        transfer_tokens(
            &ctx.accounts.user_token_account,
            &ctx.accounts.escrow,
            deposit_amount,
            &ctx.accounts.user,
        )?;

        commitment.user = ctx.accounts.user.key();
        commitment.hash = commitment_hash;
        commitment.amount = deposit_amount;
        commitment.slot = clock.slot;
        commitment.executed = false;

        Ok(())
    }

    /// Phase 2: Reveal — user reveals params, swap executes
    pub fn reveal_and_swap(
        ctx: Context<RevealSwap>,
        amount: u64,
        min_out: u64,
        nonce: u64,
    ) -> Result<()> {
        let commitment = &mut ctx.accounts.commitment;
        let clock = Clock::get()?;

        // Must reveal within 10 slots of commit
        require!(
            clock.slot <= commitment.slot + 10,
            SwapError::CommitmentExpired
        );
        // Must wait at least 2 slots (prevents same-block reveal)
        require!(
            clock.slot >= commitment.slot + 2,
            SwapError::RevealTooEarly
        );

        // Verify commitment
        let expected_hash = hash(&[
            &amount.to_le_bytes(),
            &min_out.to_le_bytes(),
            &nonce.to_le_bytes(),
        ]);
        require!(
            expected_hash.to_bytes() == commitment.hash,
            SwapError::InvalidReveal
        );

        // Execute swap from escrow
        let output = execute_swap_from_escrow(
            &ctx.accounts.escrow,
            &ctx.accounts.pool,
            amount,
            min_out,
        )?;

        commitment.executed = true;

        Ok(())
    }
}
Enter fullscreen mode Exit fullscreen mode

Trade-off: Commit-reveal adds latency (~1-2 seconds for 2-slot wait) and requires two transactions. Best for large swaps where the MEV extraction would exceed the extra gas cost.


Defense #5: Dynamic Slippage Based on Trade Size

Most wallets and DEX frontends use a static slippage tolerance (0.5%, 1%, etc.). This is a gift to sandwich bots — they know exactly how much they can extract. Dynamic slippage scales the tolerance based on the trade's price impact:

// Frontend SDK for dynamic slippage calculation
function calculateDynamicSlippage(
    tradeSize: number,
    poolLiquidity: number,
    recentVolatility: number,  // 1-hour price volatility
    blockCongestion: number    // current slot utilization %
): number {
    // Base: expected price impact from trade size
    const expectedImpact = (tradeSize / poolLiquidity) * 100;

    // Volatility buffer: wider in volatile markets
    const volatilityBuffer = recentVolatility * 0.5;

    // Congestion premium: tighter when network is calm
    const congestionMultiplier = blockCongestion > 80 ? 1.5 : 1.0;

    // Dynamic slippage = impact + buffer, but never more than 2x impact
    const slippage = Math.min(
        (expectedImpact + volatilityBuffer) * congestionMultiplier,
        expectedImpact * 2
    );

    // Floor at 0.1%, cap at 3%
    return Math.max(0.1, Math.min(slippage, 3.0));
}

// Example results:
// $100 swap in $10M pool → 0.1% slippage (floor)
// $10K swap in $1M pool → 1.0% slippage (standard impact)
// $100K swap in $1M pool → 3.0% slippage (cap, warn user)
Enter fullscreen mode Exit fullscreen mode

Why this works: A $100 swap gets 0.1% slippage — only $0.10 extractable, less than the attacker's gas cost. A $100,000 swap still gets 3% maximum, but the user is warned and can use commit-reveal for additional protection.


Defense #6: On-Chain Sandwich Detection Circuit Breaker

For protocols managing significant TVL, implement an on-chain circuit breaker that detects sandwich patterns and pauses trading:

#[account]
pub struct TradingState {
    pub last_trade_direction: TradeDirection,  // Buy or Sell
    pub consecutive_reversals: u8,
    pub last_trade_slot: u64,
    pub last_trade_size: u64,
    pub circuit_breaker_active: bool,
    pub circuit_breaker_until: u64,
}

pub fn check_sandwich_pattern(
    state: &mut TradingState,
    current_direction: TradeDirection,
    current_slot: u64,
    trade_size: u64,
) -> Result<()> {
    // Detect rapid buy-sell-buy or sell-buy-sell in same slot
    if current_slot == state.last_trade_slot 
        && current_direction != state.last_trade_direction 
    {
        state.consecutive_reversals += 1;

        // 3 reversals in same slot = likely sandwich
        if state.consecutive_reversals >= 3 {
            state.circuit_breaker_active = true;
            state.circuit_breaker_until = current_slot + 10;

            emit!(SandwichDetected {
                slot: current_slot,
                reversals: state.consecutive_reversals,
            });

            return Err(SwapError::CircuitBreakerTripped.into());
        }
    } else if current_slot > state.last_trade_slot {
        // New slot, reset counter
        state.consecutive_reversals = 0;
    }

    // Check if circuit breaker is still active
    if state.circuit_breaker_active && current_slot < state.circuit_breaker_until {
        return Err(SwapError::CircuitBreakerActive.into());
    } else {
        state.circuit_breaker_active = false;
    }

    state.last_trade_direction = current_direction;
    state.last_trade_slot = current_slot;
    state.last_trade_size = trade_size;

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

The Defense Matrix: Which Protection for Which Scenario

Defense Protects Against Latency Cost Best For
Jito dontfront Same-block sandwich None All trades via Jito
Private relays Observation-based attacks ~50ms High-value trades
TWAP validation Price manipulation None (on-chain) Protocol-level AMMs
Commit-reveal All sandwich types 1-2 seconds Trades > $10K
Dynamic slippage Over-extraction None Frontend/wallet UX
Circuit breaker Repeated attacks Pauses trading High-TVL pools

Layered Defense: The Recommended Stack

For maximum protection, layer multiple defenses:

User Intent (Frontend)
    │
    ├─ Dynamic slippage calculation
    │
    ▼
Transaction Submission
    │
    ├─ Private relay (Astralane/BloXroute)
    ├─ Jito dontfront for bundle protection
    │
    ▼
On-Chain Execution
    │
    ├─ TWAP deviation check
    ├─ Sandwich pattern detection
    ├─ Circuit breaker
    │
    ▼
Post-Trade Verification
    │
    └─ Price impact sanity check
Enter fullscreen mode Exit fullscreen mode

No single defense is sufficient. Jito's dontfront doesn't stop wide sandwiches. Private relays don't help if the validator itself is malicious. TWAP checks don't prevent extraction below the deviation threshold. Layer them.


The Firedancer Forecast: What Changes in Late 2026

When Firedancer reaches majority adoption:

  1. Block times shrink → Less time for searchers to analyze and submit bundles
  2. Throughput increases → More transactions to sandwich, but also more noise
  3. Revenue Mode validators → MEV extraction becomes more efficient, not less
  4. New attack surface → Different block scheduling between Firedancer and Jito-Solana creates arbitrage between validator implementations

My prediction: Total MEV extracted will increase with Firedancer, but per-transaction extraction will decrease. The winners will be protocols with on-chain defenses (TWAP checks, circuit breakers) that work regardless of the validator client.


Action Items

  1. Today: Check your DEX frontend's default slippage — if it's static 0.5%, you're donating to sandwich bots
  2. This week: Implement Jito dontfront or private relay submission for your protocol's frontend
  3. Before Firedancer mainnet: Add TWAP deviation checks to any swap instruction in your programs
  4. For high-TVL protocols: Implement the commit-reveal pattern for large trades and circuit breakers for repeated attack detection

The $500M extracted from Solana users isn't a technology failure — it's a defense failure. Every tool in this article exists today. The only question is whether you'll implement them before your users pay the tax.


This is part of the DeFi Security Research series. Previously: ZK Circuit Kill Chain, 5 Smart Contract Anti-Patterns.

Follow @ohmygod for weekly security research.

Top comments (0)