← Back to Blog

MEV Protection Patterns: Commit-Reveal, Private Mempools, and Slippage Defense

2026-04-18 mev front-running commit-reveal flashbots slippage solidity security 2026

This contract is deployed on mainnet right now, processing swaps:

function buyTokens(uint256 amountIn, address[] calldata path) external {
    IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn);
    IERC20(path[0]).approve(router, amountIn);

    IUniswapV2Router02(router).swapExactTokensForTokens(
        amountIn,
        0,                    // amountOutMin — accepts any output
        path,
        msg.sender,
        block.timestamp + 300
    );
}

The amountOutMin of zero means the caller will accept whatever output the swap produces, no matter how bad. A sandwich bot watching the mempool sees this transaction, front-runs it with a buy, lets the victim's swap execute at the inflated price, then immediately sells. The victim gets fewer tokens. The bot keeps the spread. The contract code is syntactically correct and passes compilation. Nothing reverts.

MEV — Maximal Extractable Value — does not require a bug in your code. It requires a predictable on-chain pattern and a public mempool. Understanding MEV means understanding how your contract looks to a searcher running a simulation loop against every pending transaction.

This guide covers six patterns that reduce MEV exposure, with Solidity examples for each.


What MEV Is and Why Developers Need to Think About It

MEV is the profit available to whoever controls transaction ordering within a block. Post-Merge, that's validators and the block builders they delegate to. In practice, specialized bots — searchers — monitor the public mempool, simulate pending transactions, and submit competing transactions to capture value.

Searchers are not guessing. They run your contract's logic against a simulation environment, compute exact profit, and decide in milliseconds whether to front-run, back-run, or sandwich.

The patterns they exploit are not exotic:

None of these are obvious bugs. All of them are common in deployed contracts.


Pattern 1: Slippage Protection

The vulnerability

Zero slippage is the most common MEV entry point in DeFi integrations. When amountOutMin is zero, the swap accepts any output — including output that has been driven down by the bot's front-run trade. There is no floor price; the sandwich bot can extract up to the full trade value.

// VULNERABLE: zero slippage = infinite sandwich opportunity
router.swapExactTokensForTokens(
    amountIn,
    0,                  // accepts any output — no protection
    path,
    recipient,
    block.timestamp + 300
);

The fix: basis point slippage calculation

Calculate amountOutMin from the expected output with a tolerance expressed in basis points (1 bp = 0.01%). A standard default is 50 bp (0.5%). Volatile or low-liquidity pairs may need 100 bp (1%).

// SECURE: slippage tolerance enforced on-chain
function swapWithSlippage(
    address router,
    uint256 amountIn,
    address[] calldata path,
    uint256 slippageBps    // 50 = 0.5%, 100 = 1.0%
) external returns (uint256[] memory amounts) {
    // getAmountsOut for quote only — not used as an oracle
    uint256[] memory expected = IUniswapV2Router02(router).getAmountsOut(amountIn, path);
    uint256 expectedOut = expected[expected.length - 1];

    // Apply basis point tolerance
    uint256 amountOutMin = (expectedOut * (10_000 - slippageBps)) / 10_000;

    IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn);
    IERC20(path[0]).approve(router, amountIn);

    amounts = IUniswapV2Router02(router).swapExactTokensForTokens(
        amountIn,
        amountOutMin,     // revert if output falls below this floor
        path,
        msg.sender,
        block.timestamp + 300
    );
}

getAmountsOut() reads spot reserves — it is not a trusted oracle. It is used here purely to estimate expected output for slippage math. Never pipe its output into protocol-level decisions.


Pattern 2: Commit-Reveal Scheme

The vulnerability

Auctions, randomized NFT mints, price bids, and any scenario where a transaction's value depends on its content being unknown before execution are all front-running targets. The moment a bid is in the mempool, it is public.

// VULNERABLE: bid amount visible in mempool before execution
function bid(uint256 amount) external payable {
    require(msg.value == amount, "Value mismatch");
    require(amount > highestBid, "Bid too low");
    highestBid = amount;
    highestBidder = msg.sender;
}

A bot watching the mempool sees amount, computes amount + 1 wei, and submits a higher-gas transaction that lands first.

The fix: two-phase commit-reveal

Split the interaction into two transactions separated by a time window. The first transaction commits a hash of the action and a secret salt. The second reveals the preimage. During the commit phase, bots see only the hash — front-running a hash is useless because they do not know the underlying value.

// SECURE: commit-reveal auction
contract CommitRevealAuction {
    struct Commitment {
        bytes32 hash;
        bool revealed;
    }

    mapping(address => Commitment) public commitments;
    mapping(address => uint256) public bids;

    uint256 public immutable commitDeadline;
    uint256 public immutable revealDeadline;

    address public winner;
    uint256 public winningBid;

    constructor(uint256 commitWindow, uint256 revealWindow) {
        commitDeadline = block.timestamp + commitWindow;
        revealDeadline = commitDeadline + revealWindow;
    }

    // Phase 1: submit hash — amount and salt stay off-chain until reveal
    function commit(bytes32 commitment) external {
        require(block.timestamp < commitDeadline, "Commit phase closed");
        require(commitments[msg.sender].hash == bytes32(0), "Already committed");
        commitments[msg.sender].hash = commitment;
    }

    // Phase 2: reveal preimage — contract verifies it matches the stored hash
    function reveal(uint256 amount, bytes32 salt) external payable {
        require(block.timestamp >= commitDeadline, "Commit phase still open");
        require(block.timestamp < revealDeadline, "Reveal phase closed");

        Commitment storage c = commitments[msg.sender];
        require(c.hash != bytes32(0), "No commitment found");
        require(!c.revealed, "Already revealed");
        require(msg.value == amount, "Value must match bid");
        require(
            keccak256(abi.encodePacked(amount, salt, msg.sender)) == c.hash,
            "Hash mismatch"
        );

        c.revealed = true;
        bids[msg.sender] = amount;

        if (amount > winningBid) {
            winningBid = amount;
            winner = msg.sender;
        }
    }
}

Callers generate the commitment off-chain: keccak256(abi.encodePacked(amount, salt, msg.sender)). The salt must be secret — without it, an attacker can brute-force the hash to recover the bid. Commit-reveal applies equally to on-chain randomness generation and sealed-bid pricing for any order mechanism.


Pattern 3: Private Mempool via Flashbots Protect

The most complete MEV defense at the infrastructure layer is routing transactions through a private mempool. If a transaction is never visible before it lands, it cannot be front-run.

Flashbots Protect is an RPC endpoint that submits transactions privately to Flashbots block builders, bypassing the public mempool entirely. From the DApp perspective, the integration is one config change — swap the provider URL. No contract changes required.

// ethers.js — route transactions through Flashbots Protect
const provider = new ethers.JsonRpcProvider("https://rpc.flashbots.net");
const signer = new ethers.Wallet(privateKey, provider);

const tx = await contract.connect(signer).swap(amountIn, amountOutMin, path);

Flashbots Protect also supports bundle submission — multiple transactions that execute atomically or not at all, useful for multi-step operations that should not be partially filled.

For ERC-4337 account abstraction, UserOperation bundlers serve the same role: they aggregate user operations off-chain and submit them as a single transaction, giving users effective mempool privacy without a custom RPC setup.

Private mempool routing does not eliminate all MEV — bots can still react to confirmed on-chain state. It eliminates the most profitable category: reacting to pending transactions.


Pattern 4: Deadline and Slippage Combined

Slippage alone does not prevent a delayed sandwich. A transaction can sit in the mempool for minutes or hours — especially during gas price spikes — and execute when market conditions have shifted. UniswapV2's swapExactTokensForTokens is the canonical model for combining both defenses:

function swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,  // revert if output below this
    address[] calldata path,
    address to,
    uint deadline       // revert if executed after this timestamp
) external returns (uint[] memory amounts);

Both parameters are required. amountOutMin bounds the price impact from sandwich insertion. deadline prevents stale execution of a transaction that was meant to be time-sensitive.

// SECURE: both defenses active
uint256 expectedOut = getExpectedOutput(amountIn, path);   // from off-chain or TWAP
uint256 amountOutMin = (expectedOut * 9950) / 10000;       // 0.5% slippage
uint256 deadline = block.timestamp + 180;                   // 3-minute window

router.swapExactTokensForTokens(
    amountIn,
    amountOutMin,
    path,
    msg.sender,
    deadline
);

Setting deadline = type(uint256).max or deadline = block.timestamp + 365 days disables the protection. Passing block.timestamp directly is equally useless — the transaction executes in the current block, so it never restricts anything. Pass a future timestamp computed at submission time.


Pattern 5: TWAP Pricing for On-Chain Orders

Spot price is manipulable in a single block via flash loan. Any limit order or conditional execution mechanism that reads spot price to determine whether to fill can be triggered by a price manipulation.

// VULNERABLE: limit order filled based on manipulable spot price
function fillLimitOrder(uint256 orderId) external {
    Order storage order = orders[orderId];
    (uint112 r0, uint112 r1,) = IUniswapV2Pair(order.pair).getReserves();
    uint256 spotPrice = (uint256(r1) * 1e18) / uint256(r0);

    // Attacker flash-loans into pool, pushes spotPrice above order.limitPrice, triggers fill
    require(spotPrice >= order.limitPrice, "Price not reached");
    _executeFill(order);
}

Price orders against a TWAP oracle with at minimum a one-hour window — preferably 24 hours for low-liquidity pairs. Moving a TWAP meaningfully requires sustained capital across many blocks, making flash loan manipulation economically impractical.

// SECURE: limit order priced against TWAP — immune to single-block manipulation
function fillLimitOrder(uint256 orderId) external {
    Order storage order = orders[orderId];

    // consult() returns time-averaged price — flash loans cannot move it
    uint256 twapPrice = IUniswapV2Oracle(twapOracle).consult(order.tokenIn, 1e18);

    require(twapPrice >= order.limitPrice, "TWAP price not reached");
    _executeFill(order);
}

Chainlink price feeds are a production-grade alternative, updated by independent node operators with built-in deviation circuit breakers. For assets without a Chainlink feed, deploy a UniswapV2OracleSimple with a 24-hour window.


Pattern 6: Batch Auctions

The CoW Protocol (Coincidence of Wants) and 1inch Fusion architecture solve MEV at a structural level. Instead of executing individual swaps through the public mempool, they aggregate orders over a time window, find a clearing price off-chain, and settle all matched orders atomically in a single transaction.

The on-chain settlement contract receives a clearing price and a set of pre-signed orders, verifies each against the clearing price, and executes transfers atomically:

// Simplified batch settlement — all orders fill at the same clearing price
function settle(
    Order[] calldata orders,
    uint256 clearingPrice,     // determined off-chain by solver
    bytes calldata solverSig
) external {
    require(isTrustedSolver(msg.sender, solverSig), "Unauthorized solver");

    for (uint256 i = 0; i < orders.length; i++) {
        Order calldata order = orders[i];
        require(clearingPrice >= order.limitPrice, "Order not fillable");

        uint256 outAmount = (order.inAmount * clearingPrice) / 1e18;
        IERC20(order.tokenIn).transferFrom(order.trader, address(this), order.inAmount);
        IERC20(order.tokenOut).transfer(order.trader, outAmount);
    }
}

Two properties make batch auctions MEV-resistant: all orders see the same clearing price so there is no marginal order for a sandwich bot to target, and off-chain solvers compete on execution quality rather than block position. The value that would go to sandwich bots goes to users as price improvement instead.

Protocols building DEX integrations at scale should evaluate whether their flow can route through a batch auction system rather than direct router calls.


What Not to Do

Do not depend on transaction ordering for correctness. If your contract's security requires transaction A to always execute before transaction B, a sequencer or validator can break that assumption. Write contracts safe under any ordering.

Do not use block.timestamp as a randomness source. Within the same block, block.timestamp is identical for every transaction and can be nudged by validators. For on-chain randomness, use Chainlink VRF or a commit-reveal scheme with the reveal separated by at least one block.

Do not assume a pending transaction is private. Any transaction sent to a public RPC node — Infura, Alchemy, or your own — enters the public mempool and is visible to the entire Ethereum p2p network within seconds. Use Flashbots Protect or a private bundle for transactions that should not be front-runnable.

Do not use tx.origin to identify users. Smart wallets, meta-transaction relayers, and ERC-4337 paymasters all change tx.origin without changing the effective user. Use msg.sender with explicit access control.


What ContractScan Detects

Pattern Detection
amountOutMin = 0 in router calls AST pattern match on zero literal in swap call arguments
Missing deadline or deadline = type(uint256).max Literal value detection in router call deadline arguments
block.timestamp used as randomness input Data flow analysis tracing timestamp into keccak or modular arithmetic
Spot price from getReserves() used in protocol logic Static analysis flagging reserve ratio in pricing or conditional branches
getAmountsOut() output used in protocol decisions Call graph analysis tracing output into conditional or pricing branches
Transaction order dependence (SWC-114) Control flow analysis: state reads followed by state writes in separate transactions
Auction bid visible before execution without commit-reveal Pattern detection: public bid(uint256) without corresponding commitment phase

ContractScan runs these detectors as part of every scan. Findings link to the specific line and recommended remediation. MEV-adjacent issues — missing slippage, missing deadlines, spot price oracle reliance — appear under the DeFi security category in the report.


Audit Your Contract for MEV Vulnerabilities

MEV patterns are common, often missed in manual review, and frequently exploitable at scale. Slippage and deadline issues show up in forks of established protocols because the integration parameters are easy to get wrong and the defaults are often unsafe.

Check your contract for MEV vulnerabilities at https://contract-scanner.raccoonworld.xyz



This post is for educational purposes. It does not constitute financial, legal, or investment advice. Always conduct a professional audit before deploying smart contracts to production.

Important Notes

This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.

Scan your contract for this vulnerability
Free QuickScan — Unlimited quick scans. No signup required.. No signup required.
Scan a Contract →