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.
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.
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.