Blog

Slippage floors come from the quoter, not the chart

Deriving a minimum output from the displayed price reverts every large trade on a steep curve. Why the swap panel simulates Quoter V2 for every quote, and how sells unwrap to ETH with the floor on the unwrap.

August 30, 2026 6 min readtradingengineering

The obvious way to compute a slippage floor is: take the price on screen, multiply by the input amount, knock off the user’s tolerance, and pass that as amountOutMinimum. It is wrong, and on a launchpad it is wrong in a way that bites hardest exactly when a user is most excited.

Spot price ignores your own trade

The price on screen is slot0 — the pool’s marginal price for an infinitesimal trade. A real trade moves along the curve as it fills, so its average price is worse than spot. On a fresh launch, where fully diluted value is around 1.36 ETH, the effect is enormous: a 0.1 ETH buy moves the price by roughly 15%. A floor at “spot minus 1%” is above what the trade can possibly fill. It reverts. The user raises tolerance to 5%. It reverts again. They conclude the site is broken.

Ask the pool what it would actually give you

Uniswap ships Quoter V2 for this. quoteExactInputSingle runs the swap against current pool state and returns the output, the price after, the ticks crossed and a gas estimate — then reverts, by design, so it cannot be called as a view. We simulate it with eth_call instead, which is what it is for.

lib/use-pons-swap.ts
function applySlippage(amount: bigint, slippagePercent: number): bigint {
  const bps = BigInt(Math.round(Math.max(0, Math.min(50, slippagePercent)) * 100));
  return (amount * (10_000n - bps)) / 10_000n;
}
// amountOutMinimum = applySlippage(quoted.amountOut, tolerance)

The floor is now “what the pool said a moment ago, less tolerance”. The only thing tolerance has to cover is other people’s trades landing between the quote and inclusion, which is what slippage tolerance was always supposed to mean.

Buys: native ETH in

A buy is one exactInputSingle on SwapRouter02 with the ETH as msg.value. The router wraps it to WETH itself. No approval needed.

Sells: the floor lives on the unwrap

A sell should deliver ETH, not WETH. SwapRouter02 supports this with a multicall: first exactInputSingle with recipient = address(2) — a sentinel meaning “keep the output in the router” — then unwrapWETH9(amountMinimum, seller). We put the slippage floor on the unwrap. If the swap produced less WETH than the floor, the unwrap reverts and the whole multicall with it, so the seller either gets ETH above the floor or keeps their tokens.

Sell multicall
router.multicall([
  exactInputSingle({ tokenIn: token, tokenOut: WETH, fee: 10000, recipient: 0x…0002, amountIn, amountOutMinimum: 0 }),
  unwrapWETH9(applySlippage(quote, tolerance), seller),
])

What the client never does

It never writes a trade to the database. The pool’s Swap event is the record; the indexer reads it back. A client-side write would double-count, and would also be a place where a number that never happened on chain could enter the system.