Front-Running & MEV in Solidity: The AMM Developer's Survival Guide

MEV bots extract over $700 million a year from naive Solidity contracts, and your AMM is probably feeding them. Front-running isn't a bug in your code — it's a property of the mempool. If you write swap functions, auctions, or liquidations without understanding sandwich attacks, commit-reveal…

Front-running in Solidity (SWC-114) is when an attacker observes a pending transaction in the public mempool and submits their own transaction with higher gas to execute first, profiting from the victim's intended state change. MEV (Maximal Extractable Value) generalizes this to any reorderable profit — sandwich attacks, arbitrage, liquidations. Mitigations include slippage checks, commit-reveal patterns, TWAP oracles, and private mempools like Flashbots Protect.
··8 min read

The Bug: Your Mempool Is a Public Auction

Every transaction you broadcast to Ethereum sits in the public mempool before it's mined. Bots watch that mempool 24/7. If your transaction is profitable to front-run, sandwich, or back-run, it will be. This isn't a theoretical attack — it's a multi-hundred-million-dollar industry that runs on autopilot.

SWC-114 formalizes this as *Transaction Order Dependence*: when your contract's outcome depends on the order transactions are mined, and an attacker can influence that order by paying more gas (or bribing a builder), you have a vulnerability.

The canonical case is the AMM swap. Naive users sign a swap that says "give me at least 0 tokens out." A bot sees it, front-runs with a buy that pumps the price, lets the victim execute at the inflated price, then dumps. The victim eats the entire price impact. This is a *sandwich attack*, and it happens thousands of times a day.

Vulnerable Code: The AMM That Hates Its Users

```solidity
contract NaiveAMM {
uint256 public reserveA;
uint256 public reserveB;

// No slippage protection. No deadline. Bot bait.
function swap(uint256 amountIn, bool aToB) external {
if (aToB) {
uint256 amountOut = (amountIn * reserveB) / (reserveA + amountIn);
reserveA += amountIn;
reserveB -= amountOut;
IERC20(tokenB).transfer(msg.sender, amountOut);
}
// ... mirror logic
}
}
```

Problems, in order of severity:

1. **No `minAmountOut`.** The user accepts any output, including zero. A sandwich bot will move the price until your slippage equals the bot's profit margin minus gas.
2. **No `deadline`.** A miner can hold this transaction for hours and execute it when prices have moved.
3. **Spot price is readable on-chain.** Any contract that uses `reserveB / reserveA` as a price oracle (looking at you, lending protocols circa 2020) can be flash-loan-manipulated in a single block.

The Fix: Slippage, Deadlines, and Realistic Threat Models

```solidity
contract HardenedAMM {
uint256 public reserveA;
uint256 public reserveB;

error SlippageExceeded(uint256 got, uint256 min);
error Expired();

function swap(
uint256 amountIn,
uint256 minAmountOut,
bool aToB,
uint256 deadline
) external {
if (block.timestamp > deadline) revert Expired();

uint256 amountOut = aToB
? (amountIn * reserveB) / (reserveA + amountIn)
: (amountIn * reserveA) / (reserveB + amountIn);

if (amountOut < minAmountOut) revert SlippageExceeded(amountOut, minAmountOut);

// ... update reserves, transfer
}
}
```

`minAmountOut` caps the sandwich profit at the slippage tolerance. `deadline` prevents miners from holding stale transactions. These are table stakes — every Uniswap V2 fork has them, and skipping them is malpractice.

But slippage limits don't eliminate MEV. They cap it. A 1% slippage tolerance on a $1M swap is a $10,000 sandwich budget. If you want to actually defeat MEV, you need stronger tools.

Beyond Slippage: Commit-Reveal

For anything sensitive to ordering — auctions, NFT mints with bonding curves, on-chain games — use commit-reveal:

```solidity
contract CommitRevealAuction {
mapping(address => bytes32) public commitments;
mapping(address => uint256) public revealed;
uint256 public commitDeadline;
uint256 public revealDeadline;

function commit(bytes32 hash) external {
require(block.timestamp < commitDeadline);
commitments[msg.sender] = hash;
}

function reveal(uint256 bid, bytes32 salt) external {
require(block.timestamp >= commitDeadline && block.timestamp < revealDeadline);
require(keccak256(abi.encodePacked(bid, salt, msg.sender)) == commitments[msg.sender]);
revealed[msg.sender] = bid;
}
}
```

Bots can't front-run what they can't read. The trade-off is UX: two transactions instead of one, plus a waiting period.

TWAP Oracles: Don't Trust Spot Prices

If your contract reads prices from an AMM, use a time-weighted average. Uniswap V2's `cumulativePrice` and V3's tick oracles exist for exactly this reason. The bZx hacks (~$1M, February 2020) and dozens since then all share one root cause: a protocol read spot price from a manipulable pool and treated it as truth.

```solidity
// Read Uniswap V3 TWAP over 30 minutes
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 1800;
secondsAgos[1] = 0;
(int56[] memory tickCumulatives,) = pool.observe(secondsAgos);
int24 avgTick = int24((tickCumulatives[1] - tickCumulatives[0]) / 1800);
```

A flash loan can move spot price 50% for one block. It cannot move a 30-minute TWAP without sustained capital and risk.

Real-World Damage

**Daily sandwich attacks**: EigenPhi and Flashbots dashboards routinely show $1–3M extracted per day on Ethereum mainnet alone, mostly from users routing through DEX aggregators without tight slippage.
- **bZx (Feb 2020)**: ~$1M lost to flash-loan-manipulated Uniswap V1 spot prices used as oracle input.
- **Inverse Finance (April 2022)**: $15.6M drained via TWAP manipulation on a low-liquidity Curve pool — proving even TWAPs need depth to be safe.
- **MEV-Boost since The Merge**: builders explicitly auction transaction ordering. Front-running is now infrastructure, not exploit.

What to Do Before You Deploy

At a minimum, every state-changing user-facing function should ask: *what happens if my transaction is reordered, delayed, or executed alongside an attacker's?* If you can't answer that crisply, run your contracts through our [free AI audit](https://www.cryptohawking.com/audit) to catch the obvious MEV exposures — missing slippage params, spot price reads, unbounded deadlines.

For protocols holding real TVL, the ordering attacks get subtle: just-in-time liquidity, oracle latency arbitrage, governance proposal sandwiching. Those require a human reading your code with a threat model in hand — that's what our [manual audit](https://www.cryptohawking.com/audit/manual) is for ($5,000, 3 business days, paid in ETH/SOL/USDT).

The Honest Summary

You cannot eliminate MEV on a public blockchain. You can make your contracts uneconomical to attack. That's the whole game: raise the bot's cost above its profit. Slippage limits, deadlines, commit-reveal, TWAPs, and private RPCs are the tools. Use them by default, not when you remember.

If your AMM ships without `minAmountOut`, you didn't write a DEX. You wrote a donation contract for bots.

FAQ

Does using Flashbots Protect eliminate front-running for my users?

It eliminates *public mempool* front-running for that specific user, because the transaction never enters the public mempool — it goes directly to block builders. But it doesn't protect against on-chain MEV that's visible after the transaction lands (back-running, JIT liquidity), and it doesn't protect users who don't use it. As a protocol developer, you can't force users to use Flashbots, so your contract still needs slippage and deadline protections at the smart contract layer. Treat private RPCs as defense in depth, not a substitute for hardened contract logic.

Why isn't a high slippage tolerance like 5% just fine?

Because slippage tolerance is the *maximum profit you're authorizing the bot to take*. A 5% slippage on a $100k swap is a $5,000 sandwich. The bot will move the price until it captures exactly that, every time. Slippage protection is a circuit breaker for catastrophic loss, not a defense against routine extraction. For tight execution, use 0.1–0.5% slippage on liquid pairs, route through aggregators that split orders, and consider RFQ-based DEXs (1inch Fusion, CoW Swap) that move execution off-chain entirely.

Can I prevent MEV by checking tx.origin or restricting EOAs?

No, and please don't. Restricting calls to EOAs (via `tx.origin == msg.sender`) blocks legitimate smart wallet users (Safe, account abstraction) without stopping MEV — bots run validators and builders directly, they don't need contracts to extract value. The right primitives are economic (slippage, deadlines) and cryptographic (commit-reveal, threshold encryption like Shutter Network). EOA-only checks are a 2018 anti-pattern that breaks composability and provides essentially zero security against modern MEV infrastructure.

How long should my TWAP window be?

It depends on pool depth and how much you trust the underlying liquidity. For deep Uniswap V3 pools (ETH/USDC, WBTC/ETH), 10–30 minutes is reasonable. For mid-cap pairs, 1–2 hours. For thin pools, don't use them as oracles at all — use Chainlink or a multi-source aggregator. Longer windows resist manipulation but lag real prices, which matters for liquidations. The Inverse Finance hack happened because the TWAP was technically present but the underlying pool was too shallow to make manipulation expensive. Depth × time = manipulation cost.

Is MEV a problem on L2s and alt-L1s?

Yes, just shaped differently. Centralized sequencers (Arbitrum, Optimism, Base today) order transactions first-come-first-served and don't run public mempools, so sandwich attacks are rare — but the sequencer itself could extract MEV, and they're moving toward decentralized sequencing where it returns. Solana has its own MEV ecosystem via Jito. Any chain with public state and economic actors has MEV. The mitigations (slippage, commit-reveal, TWAPs) are chain-agnostic and should be in your contracts regardless of deployment target.

One Solidity tip + 1 case study per month