Missing Slippage & Deadline Parameters: Why Naive Swaps Get Sandwiched

Every swap that hardcodes `amountOutMin = 0` or `deadline = type(uint256).max` is a free lunch for MEV bots. Searchers extract hundreds of millions per year from contracts that call Uniswap without thinking. The fix is two parameters and five lines of math — but most devs still get it wrong…

Missing slippage and deadline parameters in AMM swaps allow MEV bots to sandwich the transaction: front-running with a buy to inflate price, letting the victim swap at the worst rate, then dumping. Solidity contracts that pass `amountOutMin = 0` or `deadline = type(uint256).max` to Uniswap-style routers expose users to near-total value extraction. The fix is computing a minimum output off-chain using an oracle or recent price, then passing realistic bounds.
··7 min read

The Bug in One Line

```solidity
router.swapExactTokensForTokens(amountIn, 0, path, address(this), type(uint256).max);
```

If you've ever written that line — or reviewed a contract that did — congratulations, you've just signed a blank check to every MEV searcher on mainnet. `amountOutMin = 0` means "I'll accept literally any amount of output, including dust." `deadline = type(uint256).max` means "feel free to hold this transaction in the mempool for the next 10,000 years and execute it whenever it's most profitable for someone else."

This isn't a theoretical issue. It's the single most common bug I find when reviewing DeFi integrations.

How the Sandwich Works

AMMs like Uniswap V2/V3 use constant-product pricing: `x * y = k`. The price of a swap depends entirely on the pool's current reserves. A searcher watching the mempool sees your pending swap and:

1. **Front-runs** with a buy of the same token, pushing the price up.
2. Your swap executes at the inflated price, getting fewer tokens than expected.
3. **Back-runs** by selling the tokens they just bought, pocketing the spread.

The larger your swap relative to pool depth, the more the bot extracts. Without slippage protection, even a $10k swap in a thin pool can lose 30-50% to a sandwich. With private mempools and MEV-Boost, this pattern is industrialized — Flashbots and similar relays process billions in sandwich volume annually, and the cumulative extraction from naive contracts is in the hundreds of millions of dollars.

The deadline parameter is the second half of the trap. Even if you set a reasonable `amountOutMin`, a `deadline = max` lets a validator or relay hold your transaction until the market moves in a direction that *just barely* satisfies your minimum — extracting whatever buffer you left.

The Vulnerable Pattern

Here's a zap contract I audited last year (anonymized). Looks innocent:

```solidity
contract YieldZap {
IUniswapV2Router02 public router;
IERC20 public usdc;
IERC20 public targetToken;

function zapIn(uint256 usdcAmount) external {
usdc.transferFrom(msg.sender, address(this), usdcAmount);
usdc.approve(address(router), usdcAmount);

address[] memory path = new address[](2);
path[0] = address(usdc);
path[1] = address(targetToken);

router.swapExactTokensForTokens(
usdcAmount,
0, // <-- accept any output
path,
address(this),
block.timestamp + 3600 // <-- 1 hour, plenty of MEV window
);

// ... deposit targetToken into vault
}
}
```

Three problems:

1. `amountOutMin = 0` — total sandwich exposure.
2. `block.timestamp + 3600` — a validator can defer inclusion for an hour, plenty of time to wait for adverse price movement.
3. No way for the *user* to specify their own tolerance — the contract decides for them, and it decides badly.

The Fix

Let the caller pass slippage and deadline parameters. Compute `amountOutMin` off-chain from a quoter or oracle, apply a tolerance (typically 0.5%-1% for liquid pairs, more for exotic ones), and pass a *tight* deadline (5 minutes is generous):

```solidity
function zapIn(
uint256 usdcAmount,
uint256 amountOutMin,
uint256 deadline
) external {
require(deadline >= block.timestamp, "expired");
require(deadline <= block.timestamp + 1800, "deadline too far");

usdc.transferFrom(msg.sender, address(this), usdcAmount);
usdc.approve(address(router), usdcAmount);

address[] memory path = new address[](2);
path[0] = address(usdc);
path[1] = address(targetToken);

uint256[] memory amounts = router.swapExactTokensForTokens(
usdcAmount,
amountOutMin,
path,
address(this),
deadline
);

require(amounts[amounts.length - 1] >= amountOutMin, "slippage");
// ... deposit
}
```

The explicit `require` after the swap is redundant with the router's check, but I keep it because routers can be swapped out and defense-in-depth costs nothing.

On-Chain Slippage Calculation: Don't

A pattern I see in junior code: computing `amountOutMin` on-chain using `getAmountsOut`.

```solidity
uint256[] memory expected = router.getAmountsOut(amountIn, path);
uint256 minOut = expected[expected.length - 1] * 99 / 100; // 1% slippage
router.swapExactTokensForTokens(amountIn, minOut, path, ...);
```

**This does nothing.** `getAmountsOut` reads the *same reserves* the swap will use. If a sandwich bot front-runs, both calls see the manipulated reserves, and `minOut` becomes the manipulated price minus 1%. You've just authorized the sandwich.

Slippage must be computed against a price source the attacker cannot manipulate in the same transaction — typically an off-chain quote from the user's wallet/frontend, or an on-chain TWAP that lags by several blocks.

Special Cases

**Vaults rebalancing on a schedule:** users don't sign each swap. Use Chainlink price feeds or a Uniswap V3 TWAP (30+ minute window) to compute `amountOutMin` on-chain. Sandwich bots can't move a TWAP cheaply.
- **Cross-chain bridges and intent systems:** the destination-chain swap often has no signer present. Solver/keeper architecture must enforce slippage off-chain against a verified quote.
- **Permit2-based routers:** check that the deadline you sign for Permit2 isn't reused as the swap deadline — they have different threat models.

If you're building any of these patterns and want a second pair of eyes, our [free AI audit](https://www.cryptohawking.com/audit) flags hardcoded `0` and `type(uint256).max` arguments in swap calls automatically — it's one of the rules I tuned myself.

Real-World Pattern, Real-World Losses

There's no single "$X million sandwich hack" headline for this class because the losses are distributed across every user of every naive integration, every block. Studies of Ethereum mempool data have consistently put MEV extraction in the multi-hundred-million-dollar range annually, with sandwich attacks the dominant category. Contracts that swap without slippage protection are the supply side of that market.

Notable incidents:

A well-known stablecoin protocol's reward-harvester swapped CRV → USDC with `amountOutMin = 0` and lost ~15% per harvest to sandwiches for months before the team noticed.
- Multiple "auto-compounder" vaults on BSC and Polygon have been drained by 30%+ on individual rebalances due to thin-pool sandwiches.
- Even Uniswap's *own* universal router has shipped with edge cases where deadline enforcement was looser than expected on certain paths.

Checklist Before Shipping

1. No literal `0` as `amountOutMin` anywhere except in tests.
2. No `type(uint256).max` or far-future timestamp as `deadline`.
3. Slippage computed off-chain (user-signed) or against a manipulation-resistant oracle/TWAP.
4. Deadlines capped at ~5-20 minutes from `block.timestamp`.
5. The user — not the contract — controls the tolerance whenever possible.

If your protocol routes any user funds through an AMM and you're not 100% sure each of these is covered, get a [manual audit](https://www.cryptohawking.com/audit/manual). I review every swap path by hand, including the off-chain components that feed the parameters. Slippage bugs hide in the seams between contract and frontend, which is exactly where automated tools miss them.

The two parameters exist for a reason. Use them.

FAQ

Why can't I just compute amountOutMin on-chain using getAmountsOut?

Because `getAmountsOut` reads the live pool reserves — the same reserves the swap itself will use. A sandwich attacker front-runs your transaction by manipulating those reserves, so both your quote and your swap see the manipulated state. Your computed `minOut` ends up being (manipulated price - 1%), which the attacker can satisfy while still extracting the bulk of the value. Slippage must come from a source the attacker cannot influence within the same transaction: an off-chain quote signed by the user, or an on-chain TWAP with a long enough window that moving it is uneconomical.

What's a reasonable deadline value?

For user-initiated swaps from a wallet, 5-20 minutes from `block.timestamp` is standard. The longer your deadline, the longer a validator or private relay can hold the transaction waiting for adverse price movement that still satisfies your `amountOutMin`. Never use `type(uint256).max` or `block.timestamp + 1 days`. For keeper-driven contracts, the deadline should be set by the keeper at submission time, not hardcoded. Permit2 deadlines and swap deadlines should be considered separately — they protect against different things.

Does using a private mempool like Flashbots Protect eliminate the need for slippage?

No. Private mempools reduce sandwich risk because searchers can't see your pending transaction, but they don't eliminate it. The transaction still executes on a public AMM with public reserves, so any other transaction landing in the same block can move the price. Builders can also reorder transactions within a block. Slippage protection is your contract-level invariant — it's the only thing that guarantees a minimum output regardless of mempool, builder behavior, or block-level MEV. Use private mempools as defense-in-depth, not as a substitute.

How do auto-compounding vaults handle slippage when there's no user to sign a quote?

The robust pattern is to use a manipulation-resistant price source for `amountOutMin`. The two main options: (1) Chainlink price feeds, where you compute the expected output from the oracle price and apply a tolerance; (2) Uniswap V3 TWAPs with a window of 30+ minutes, which require an attacker to sustain price manipulation across many blocks at significant capital cost. Avoid spot price reads. Additionally, batch or randomize compounding times so attackers can't precisely predict when large swaps will occur.

Are slippage bugs really a 'vulnerability' if no funds are stolen from the contract directly?

Yes, and treating them otherwise is how protocols lose users. The funds aren't stolen from the contract's balance sheet — they're stolen from users during the transit through your contract. From a user's perspective, depositing $10,000 and getting $7,000 of vault shares because your zap got sandwiched is indistinguishable from a $3,000 hack. Auditors and serious bug bounty programs (Immunefi etc.) classify these as medium or high severity depending on impact. If you're routing user value, you own the slippage problem.

One Solidity tip + 1 case study per month

Missing Slippage & Deadline Parameters: Why Naive Swaps Get Sandwiched | Crypto Hawking