Command Palette

Search for a command to run...

Price Impact — Missing Slippage Check

A single-sided zapper executes a Uniswap V3 swap with amountOutMinimum hardcoded to zero and deadline set to block.timestamp. Any swap is open to sandwich attacks and executes at any price no matter how bad the rate.

~7 min read

Available

Price impact is the change in exchange rate caused by your own trade. Slippage is the acceptable tolerance for that change. When amountOutMinimum = 0, the swap accepts any price — including a price that has been artificially worsened by a sandwich bot. This lab explores both organic price impact and MEV-extracted value.

1. Uniswap V3 and concentrated liquidity

Uniswap V3 concentrates liquidity in price ranges. A swap depletes liquidity from the current range and may cross into adjacent ranges — each with a different effective exchange rate. Large swaps relative to available liquidity have high price impact: the marginal price worsens with each unit of input token.

The exactInputSingle() function takes amountOutMinimum: the minimum acceptable output. If the actual output falls below this value, the transaction reverts. Setting it to 0 disables this protection entirely — the transaction will execute even if you receive 0 tokens of output.

The deadline parameter is a Unix timestamp after which the transaction must not execute. deadline = block.timestamp means the transaction executes in the current block regardless of how long it has been waiting in the mempool. A stale transaction from 30 minutes ago still executes at today's price — potentially after a large market move.

2. Two slippage failure modes

Organic slippage: your swap itself moves the price. For large trades relative to pool depth, the first tokens you get are at the pre-trade price, and later tokens get progressively worse rates as reserves shift. amountOutMinimum protects against organic slippage by reverting if the aggregate output is too low.

Adversarial slippage (sandwich): a bot pre-moves the price before your transaction arrives. Your transaction then executes at the adversarially worsened price. The bot post-corrects the price. With amountOutMinimum = 0, the bot can extract nearly the entire trade value without your transaction reverting.

Both failure modes are addressed by the same fix: a real amountOutMinimum. The difference is that adversarial slippage requires the bot to take active risk (the market could move against them between front-run and back-run). Organic slippage is purely structural — no attacker needed.

3. The attack — sandwich on zero-slippage swap

The VulnerableZapper calls exactInputSingle() with amountOutMinimum = 0. Sandwich attack pseudocode:

// Zapper call: zapIn(10 WETH, feeTier=3000)
// Bot sees pending tx in mempool
// Front-run: bot buys poolToken with WETH, shifts pool price up
// Victim's zapIn executes: gets far fewer poolToken (price is now worse)
// amountOutMinimum = 0 → does not revert
// Back-run: bot sells poolToken back at original price, pockets spread
// Victim deposited into vault with severely diluted position

Beyond the sandwich: because deadline = block.timestamp, the transaction can be held in the mempool and included in a future block after a large market downturn. The user submits a zap expecting 100 poolTokens; a block later the price has dropped 20%; they receive 80 tokens with no protection.

Attack timeline

  1. 1

    Victim submits zapIn

    User calls zapIn(10 WETH, 3000). Transaction enters the public mempool. Bot detects it and computes expected price impact.

  2. 2

    Bot front-runs

    Bot submits a buy of poolToken with a higher priority fee. This transaction arrives in the same block before zapIn. Pool price of poolToken rises.

  3. 3

    Victim executes at worse price

    zapIn executes at the now-higher poolToken price. User receives fewer poolToken per WETH. amountOutMinimum = 0 prevents no reverts. Vault receives diluted position.

  4. 4

    Bot back-runs

    Bot sells poolToken in the same block (after zapIn). Price returns to near-original. Bot pockets the spread: bought cheap (pre-front-run) and sold high (post-victim).

  5. 5

    User loss

    User's vault position is smaller than expected. Loss magnitude depends on pool depth and trade size. For shallow pools or large trades, losses can exceed 5-10%.

4. The fix — real amountOutMinimum and deadline

Pass amountOutMinimum computed from a current price quote or TWAP, and require the caller to provide a deadline in the future:

function zapIn(
    uint256 amountIn,
    uint24 feeTier,
    uint256 minAmountOut,  // caller must compute from current price
    uint256 deadline       // caller must provide future timestamp
) external {
    require(block.timestamp <= deadline, "expired");
    IERC20(weth).transferFrom(msg.sender, address(this), amountIn);
    IERC20(weth).approve(address(router), amountIn);

    IUniswapV3Router.ExactInputSingleParams memory params =
        IUniswapV3Router.ExactInputSingleParams({
            tokenIn: weth,
            tokenOut: poolToken,
            fee: feeTier,
            recipient: address(this),
            deadline: deadline,
            amountIn: amountIn,
            amountOutMinimum: minAmountOut,  // NOT 0
            sqrtPriceLimitX96: 0
        });

    router.exactInputSingle(params);
}

For protocol-owned zappers (e.g., keeper bots), compute minAmountOut server-side using a TWAP oracle: get the TWAP over 30 minutes and apply 0.5-1% slippage tolerance. Never use the current spot price as the reference for minAmountOut — that defeats the purpose since spot can be sandwiched.

For user-facing interfaces, the frontend computes minAmountOut from the current quote and a user-selected slippage tolerance (default 0.5%). The user should see a slippage warning for trades larger than 1% of pool depth.

5. What to look for as an auditor

  • Any call to exactInputSingle, swapExactTokensForTokens, or similar with amountOutMin/amountOutMinimum = 0. Immediate finding.
  • deadline = block.timestamp or deadline = 0. The deadline must be a future timestamp supplied by the caller (e.g., block.timestamp + 600 for 10 minutes). Block.timestamp as deadline is no protection.
  • Contracts that auto-execute swaps without user input: keeper bots, auto-compounders, harvest calls. These are the highest-value sandwich targets because the attacker knows the exact time of execution.
  • sqrtPriceLimitX96 = 0 in Uniswap V3 means no price limit on the swap path. This alone is not a bug but combined with amountOutMinimum = 0 it allows full pool traversal at any price.
  • On-chain price computation for minAmountOut: if the contract computes minAmountOut from a current spot price on the same chain, the computation is manipulable. Use a TWAP or Chainlink feed.
  • Multi-step zaps: if the function swaps twice (WETH → tokenA → tokenB), check slippage at each step. The second swap is also sandwichable if its amountOutMin is derived from the first swap's output.

With the two slippage failure modes in mind, open the Hunt tab. Find the exact parameter values that make the zapper exploitable, then describe the sandwich sequence.

← Back to skill tree