MEV as a Security Threat: Static Analysis Approaches to Transaction Ordering Vulnerabilities

MEV — Maximal Extractable Value — is usually framed as a consensus layer problem. Validators and block builders sequence transactions; searchers compete to position themselves favorably within that sequence; users get sandwiched or front-run. The standard mitigation advice points to private mempools and MEV-share protocols.

That framing misses the underlying issue. MEV opportunities are not imposed on contracts from outside — they are created by contract code. A swap function that reads a price and executes a transfer creates a sandwichable window. A liquidation function that any caller can invoke creates a race condition bots will exploit. The mempool is where extraction happens; the contract is where the opportunity originates.

Reframing MEV as a bytecode property makes it detectable before deployment.

What Creates MEV Exposure

The common thread across MEV-vulnerable patterns is a function that reads external state, then makes a state change based on that reading, with no constraint on transaction ordering.

Price-dependent swap:

function swap(uint amountIn, uint minOut) external {
    uint price = oracle.getPrice();
    uint amountOut = amountIn * price / PRECISION;
    require(amountOut >= minOut, "Slippage");
    tokenIn.transferFrom(msg.sender, address(this), amountIn);
    tokenOut.transfer(msg.sender, amountOut);
}

The interval between when oracle.getPrice() is read and when tokenOut.transfer() executes is the sandwich window. A bot that can execute transactions before and after this one can manipulate what oracle.getPrice() returns (for AMM-based oracles) or simply exploit a fixed price oracle’s staleness.

At the bytecode level, the pattern is:

STATICCALL → oracle
MUL/DIV operations on result
CALL → token.transfer

Any function where a value returned from an external call flows through arithmetic into a token transfer is a candidate for sandwich analysis.

First-come-first-served mechanism:

function claimAirdrop(bytes32[] proof) external {
    require(!claimed[msg.sender], "Already claimed");
    require(merkleVerify(proof, msg.sender), "Invalid proof");
    claimed[msg.sender] = true;
    token.transfer(msg.sender, AIRDROP_AMOUNT);
}

This function is frontrunnable. The proof is visible in the mempool. A bot can copy the proof, replace msg.sender with their own address, and submit with higher gas. The pattern in bytecode:

SLOAD (claimed[sender]) → ISZERO check
[Merkle verification]
SSTORE (claimed = true)
CALL (token.transfer)

Any check-then-act pattern where the check uses public state and the action creates economic benefit is frontrunnable.

Liquidation race:

function liquidate(address user) external {
    require(getHealthFactor(user) < 1e18, "Healthy");
    uint bonus = collaterals[user] * LIQUIDATION_BONUS / 100;
    debts[user] = 0;
    collaterals[user] = 0;
    debtToken.transferFrom(msg.sender, address(this), debts[user]);
    collateralToken.transfer(msg.sender, collaterals[user] + bonus);
}

When a position becomes liquidatable, every call to liquidate() with that address succeeds until the first one confirms. MEV bots monitor health factors off-chain and submit liquidation transactions with high gas the moment a position crosses the threshold.

Static Analysis Techniques

Taint Analysis for Price Flows

Track oracle values through the instruction sequence:

Taint sources:
  - STATICCALL to known oracle addresses
  - Storage reads from price slots (identified by access patterns)

Propagation:
  - MUL, DIV, ADD, SUB on tainted values propagate taint
  - GT, LT, EQ comparison results carry taint forward

Sinks:
  - CALL with ETH value
  - token transfer calls (identified by function selector)
  - SSTORE to balance mappings

When a tainted value reaches a sink, the function has price-derived state changes. Combined with the absence of commit-reveal or batching mechanisms, this is a sandwich-vulnerable pattern.

Slippage Parameter Analysis

For swap functions specifically, the analysis checks whether minimum output parameters are actually enforced:

Function: swap(amountIn, minAmountOut)

Check 1: Is minAmountOut constrained to a non-zero value at any callsite?
Check 2: Does the require() check execute before the token transfer?
Check 3: Is minAmountOut derived from a TWAP or from spot price?

Finding: Slippage protection present but caller can pass 0
Risk: Protocols that set minAmountOut = 0 in automation scripts

The key distinction is between functions that structurally forbid zero slippage and functions that allow callers to shoot themselves. Both pattern classes appear in production; they warrant different severity scores.

Cross-Function MEV Detection

Some MEV opportunities span function boundaries:

contract LendingPool {
    function deposit(uint amount) external { ... }
    function borrow(uint amount) external { ... }
    function liquidate(address user) external { ... }
}

The MEV in this contract isn’t visible from liquidate() alone — it requires understanding that deposit() and borrow() create the health factor state that liquidate() checks. Static analysis builds the inter-function dependency graph:

  1. Identify functions that modify health-factor-relevant state (deposits, borrows, price changes)
  2. Identify functions that check health factor as an access condition
  3. Flag the check functions as MEV entry points
  4. Trace which external events (oracle price changes) can trigger liquidatability

Mitigation Patterns (and How to Verify Them)

Commit-Reveal

Commit-reveal schemes prevent frontrunning by separating the intent signal (visible to bots) from the execution parameters (hidden until reveal):

function commitSwap(bytes32 hash) external {
    commits[msg.sender] = Commit(hash, block.number);
}

function revealSwap(uint amountIn, uint minOut, bytes32 salt) external {
    require(keccak256(abi.encode(amountIn, minOut, salt))
            == commits[msg.sender].hash);
    require(block.number > commits[msg.sender].blockNumber + DELAY);
    // Execute swap
}

Verification checks: Does the commit store block.number? Does the reveal require block.number > commit.blockNumber + DELAY? Is the hash binding over all execution parameters?

A commit-reveal without delay is weak — a bot that sees the commit can still front-run the reveal by observing the parameters when the reveal is submitted.

Time-Weighted Price Oracles

For oracle-dependent logic, using a TWAP instead of spot price narrows the sandwich window:

function getPrice() internal view returns (uint) {
    return oracle.consult(token, TWAP_PERIOD);  // 30-minute TWAP
}

Verification checks: Is the oracle call using a time-weighted function rather than getReserves()? Is the TWAP period long enough (below ~10 minutes, manipulation becomes economically viable at scale)?

A TWAP does not eliminate sandwich vulnerability in the strict sense — it raises the capital cost of oracle manipulation. Combined with a meaningful minimum output parameter, it adequately constrains most realistic attack scenarios.

Batch Auctions

Protocols that accumulate orders before execution break the per-transaction MEV model:

function submitOrder(uint amountIn, uint minOut) external {
    epochOrders[currentEpoch].push(Order(msg.sender, amountIn, minOut));
}

function settleEpoch() external {
    require(block.timestamp >= epochEnd[currentEpoch]);
    // All orders execute at same clearing price
}

When all orders in an epoch execute at the same price, there is no per-transaction ordering advantage to extract. Verification checks: Are orders accumulated in state before execution? Is there a time delay between submission and settlement? Do all orders receive the same price?

The Intentional Case: Uniswap V2

Not all MEV-vulnerable patterns are bugs. Consider Uniswap V2 pairs:

Function: swap(uint amount0Out, uint amount1Out, address to, bytes data)

Price-dependent execution: reserves read before transfer
No commit-reveal: direct execution
Slippage protection: absent from pair contract, delegated to router

MEV classification: INHERENT BY DESIGN

Uniswap pairs are designed so that arbitrageurs correct price deviations. The MEV extracted from pairs is the mechanism by which prices converge across venues. Flagging this as a vulnerability would be incorrect — the correct output is noting that users must interact through the Router (which enforces slippage) and that the pair itself is MEV-intentional.

This distinction matters for useful analysis output. The classification should be “MEV-intentional design” rather than “vulnerable,” with a note that user-facing contracts should not call swap() directly without adding slippage protection at the caller level.

The Direction of Contract Design

A trend worth tracking is MEV internalization — protocols capturing the value that would otherwise go to external searchers.

One approach: the protocol calculates expected MEV and reduces user output by that amount, adding it to a protocol fee pool rather than letting bots extract it. Another: order flow auctions where bots bid for the right to execute a user’s swap and the winning bid goes to the user or protocol. These mechanisms are detectable at the bytecode level by looking for the characteristic patterns: MEV estimation logic before transfers, auction state for order execution rights.

Encrypted mempool integration (threshold decryption, time-locked encryption) represents the furthest end of this spectrum. These designs require transaction contents to be unknown until execution, eliminating front-running at the infrastructure level. Static analysis must evolve to recognize contracts designed for encrypted execution environments.

Summary

MEV exposure is a property of contract logic, not just mempool infrastructure. Contracts that read prices and make state changes, implement first-come-first-served mechanics, or expose callable liquidation functions create extractable value regardless of which mempool they’re submitted to.

Static analysis identifies MEV-vulnerable patterns by tracing oracle value flows to state-changing sinks, analyzing slippage enforcement, and mapping health-factor dependencies across function boundaries. It can also verify that mitigations — commit-reveal, TWAP oracles, batch auctions — are correctly implemented rather than present in name only.

The limits are real: multi-protocol MEV chains and validator-dependent ordering attacks require dynamic simulation that goes beyond static analysis. But for the common classes of sandwichable swaps, frontrunnable claims, and liquidation races, the vulnerability is encoded in bytecode and detectable before deployment.

References

  1. Flash Boys 2.0 — Daian et al. (2019)
  2. MEV — ethereum.org
  3. Flashbots MEV-Explore
  4. CoW Protocol — Batch Auction Design
  5. Uniswap V2 Core Whitepaper