DEV Community

ohmygod
ohmygod

Posted on • Originally published at dreamworksecurity.hashnode.dev

MEV Protection on Solana: A Developer's Guide to Defending DeFi Protocols Against Sandwich Attacks

TL;DR

Solana's high-throughput architecture doesn't make your protocol MEV-proof. Validators running custom block builders can reorder, insert, and delay transactions to extract value from your users. This guide covers practical defense patterns — from slippage-aware instruction design to Jito bundle integration — that every Solana DeFi developer should implement in 2026.


The Myth: "Solana Doesn't Have MEV"

Early Solana marketing positioned the chain as MEV-resistant. No mempool, deterministic execution order, 400ms slots — what's to front-run?

Reality told a different story. By 2024, Jito Labs had built an entire MEV infrastructure with tip-based transaction ordering. Validators running Jito's modified client earned millions in priority fees and tips. Sandwich bots became the most profitable participants on Solana, extracting an estimated $300M+ from DEX traders in 2025 alone.

The fundamental economics haven't changed: if a transaction reveals profitable information before execution, someone will exploit it. Solana just made the game faster.

How Sandwich Attacks Work on Solana

A sandwich attack on Solana follows the same three-step pattern as on Ethereum, adapted for Solana's architecture:

Step 1: Detection

The attacker monitors pending transactions through:

  • Jito Block Engine: Validators share transaction bundles with searchers
  • RPC endpoints: Public RPCs expose sendTransaction before inclusion
  • Staked connections: Validators sell direct access to upcoming transactions
User submits: Swap 1000 USDC → SOL on Raydium (slippage: 1%)
Attacker sees: Profitable sandwich opportunity
Enter fullscreen mode Exit fullscreen mode

Step 2: Front-run

The attacker submits a transaction with higher priority that executes before the user's:

Attacker buys: 50,000 USDC → SOL (pushes price up)
User's swap: 1000 USDC → SOL (gets fewer SOL due to moved price)
Attacker sells: SOL → USDC (captures the price difference)
Enter fullscreen mode Exit fullscreen mode

Step 3: Bundle Submission

On Solana, attackers use Jito bundles to atomically submit all three transactions:

// Pseudo-code for a sandwich bundle
let bundle = vec![
    front_run_tx,   // Buy before victim
    victim_tx,       // Victim's original swap
    back_run_tx,     // Sell after victim
];
jito_client.send_bundle(bundle).await?;
Enter fullscreen mode Exit fullscreen mode

The bundle guarantees atomic execution — all three transactions land in the same slot in the specified order, or none do.

Defense Pattern 1: Slippage-Aware Instruction Design

The most fundamental protection is ensuring your protocol enforces minimum output amounts at the instruction level — not just at the UI level.

Bad: Trust the Frontend

// ❌ No minimum output check
pub fn swap(ctx: Context<Swap>, amount_in: u64) -> Result<()> {
    let amount_out = calculate_output(amount_in, &ctx.accounts.pool)?;
    transfer_tokens(ctx.accounts.user_token_out, amount_out)?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Good: Enforce On-Chain

// ✅ On-chain minimum output enforcement
pub fn swap(
    ctx: Context<Swap>, 
    amount_in: u64,
    minimum_amount_out: u64,
) -> Result<()> {
    let amount_out = calculate_output(amount_in, &ctx.accounts.pool)?;

    require!(
        amount_out >= minimum_amount_out,
        SwapError::SlippageExceeded
    );

    transfer_tokens(ctx.accounts.user_token_out, amount_out)?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Better: Time-Weighted Price Bounds

// ✅✅ TWAP-based slippage with staleness check
pub fn swap_with_oracle(
    ctx: Context<SwapWithOracle>,
    amount_in: u64,
    max_price_deviation_bps: u16,
) -> Result<()> {
    let oracle = &ctx.accounts.price_oracle;
    let clock = Clock::get()?;

    require!(
        clock.unix_timestamp - oracle.last_update < MAX_ORACLE_STALENESS,
        SwapError::StaleOracle
    );

    let spot_price = calculate_spot_price(&ctx.accounts.pool)?;
    let oracle_price = oracle.twap_price;

    let deviation = abs_diff(spot_price, oracle_price) * 10000 / oracle_price;
    require!(
        deviation <= max_price_deviation_bps as u64,
        SwapError::PriceDeviationTooHigh
    );

    let amount_out = calculate_output(amount_in, &ctx.accounts.pool)?;
    transfer_tokens(ctx.accounts.user_token_out, amount_out)?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

This pattern catches sandwich attacks mid-execution: if the front-run transaction has moved the price beyond the TWAP deviation threshold, the victim's transaction reverts.

Defense Pattern 2: Commit-Reveal Schemes

For high-value swaps, a commit-reveal pattern makes front-running structurally impossible:

// Phase 1: Commit (reveals nothing about swap direction or amount)
pub fn commit_swap(
    ctx: Context<CommitSwap>,
    commitment: [u8; 32],  // hash(amount, min_out, nonce, user_pubkey)
    deadline_slot: u64,
) -> Result<()> {
    let order = &mut ctx.accounts.pending_order;
    order.commitment = commitment;
    order.deadline_slot = deadline_slot;
    order.user = ctx.accounts.user.key();
    order.committed_slot = Clock::get()?.slot;
    Ok(())
}

// Phase 2: Reveal (must be in a later slot)
pub fn reveal_swap(
    ctx: Context<RevealSwap>,
    amount_in: u64,
    minimum_amount_out: u64,
    nonce: [u8; 32],
) -> Result<()> {
    let order = &ctx.accounts.pending_order;
    let clock = Clock::get()?;

    require!(clock.slot > order.committed_slot, SwapError::SameSlotReveal);
    require!(clock.slot <= order.deadline_slot, SwapError::OrderExpired);

    let expected = hash(&[
        &amount_in.to_le_bytes(),
        &minimum_amount_out.to_le_bytes(),
        &nonce,
        order.user.as_ref(),
    ]);
    require!(expected.0 == order.commitment, SwapError::InvalidReveal);

    execute_swap(ctx, amount_in, minimum_amount_out)?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Trade-off: Adds latency (~400ms minimum) and UX friction. Best for swaps above a threshold (e.g., >$10K).

Defense Pattern 3: Private Transaction Submission

Use private submission channels instead of broadcasting to public RPCs:

import { SearcherClient } from 'jito-ts/dist/sdk/searcher';

async function submitPrivateSwap(
    connection: Connection,
    transaction: Transaction,
    tipLamports: number
) {
    const searcher = SearcherClient.connect(JITO_BLOCK_ENGINE_URL, keypair);

    const tipIx = SystemProgram.transfer({
        fromPubkey: wallet.publicKey,
        toPubkey: JITO_TIP_ACCOUNT,
        lamports: tipLamports,
    });
    transaction.add(tipIx);

    const bundle = new Bundle([transaction]);
    await searcher.sendBundle(bundle);
}
Enter fullscreen mode Exit fullscreen mode

Defense Pattern 4: Batch Auctions

Batch all swaps within a time window and execute at a single clearing price — eliminates sandwich attacks entirely:

pub fn settle_auction(ctx: Context<SettleAuction>) -> Result<()> {
    let auction = &mut ctx.accounts.auction;
    let clearing_price = find_clearing_price(
        &auction.buy_orders, &auction.sell_orders,
    )?;

    for order in auction.buy_orders.iter().chain(auction.sell_orders.iter()) {
        if order_matches(order, clearing_price) {
            execute_fill(order, clearing_price)?;
        }
    }
    auction.settled = true;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Defense Pattern 5: Dynamic Fees with Entropy

Make transactions computationally unpredictable for sandwich bots:

pub fn swap_with_entropy(
    ctx: Context<Swap>,
    amount_in: u64,
    minimum_amount_out: u64,
) -> Result<()> {
    let clock = Clock::get()?;
    let entropy = hash(&[
        &clock.slot.to_le_bytes(),
        ctx.accounts.user.key.as_ref(),
    ]);

    let dynamic_fee = calculate_dynamic_fee(
        &ctx.accounts.pool, &entropy, clock.unix_timestamp,
    )?;

    let amount_out = calculate_output_with_fee(
        amount_in, &ctx.accounts.pool, dynamic_fee,
    )?;

    require!(amount_out >= minimum_amount_out, SwapError::SlippageExceeded);
    transfer_tokens(ctx.accounts.user_token_out, amount_out)?;
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

The Security Checklist

If you're building or auditing a Solana DeFi protocol, verify these MEV protections:

  • [ ] Slippage enforcement: Is minimum_amount_out checked on-chain?
  • [ ] Oracle integration: Does the protocol detect abnormal price deviations?
  • [ ] Private submission: Can users submit transactions privately?
  • [ ] Batch execution: Are high-value orders batched?
  • [ ] Dynamic fees: Do fees respond to volatility or MEV activity?
  • [ ] Commit-reveal: Is there a commit-reveal option for large swaps?
  • [ ] Priority fee guidance: Does the UI suggest appropriate priority fees?
  • [ ] Simulation resistance: Is there computational unpredictability?

Conclusion

MEV protection isn't a single feature — it's a design philosophy. The most secure Solana DeFi protocols in 2026 combine multiple layers:

  1. On-chain slippage enforcement (minimum viable protection)
  2. Oracle-aware price bounds (catches mid-attack manipulation)
  3. Private submission channels (reduces information leakage)
  4. Batch auctions for high-value operations (eliminates ordering exploitation)

Build for the adversary. Your users will thank you.


DreamWork Security researches smart contract vulnerabilities across Solana and EVM ecosystems. Follow for weekly deep dives into DeFi security, audit techniques, and security best practices.

Top comments (0)