Slippage is the difference between the price you expect when you submit a trade and the price you actually receive when the trade executes. If you see a quoted price of $2,000 per ETH and submit a buy order, but by the time the trade processes you end up paying $2,012, that $12 difference is slippage. It is an unavoidable friction in every market, but especially pronounced in on-chain trading where block latency and public mempools create unique challenges.
Slippage has three main causes.
In an order book, slippage happens when your trade is too large to fill at the best price level. A market buy order for 100 ETH might fill the first 50 ETH at $2,001 (the best ask), but the remaining 50 ETH must "walk the book" and fill at $2,002, $2,003, and so on. The average execution price ends up higher than the quoted best ask. This is sometimes called market depth slippage.
In an AMM, the same concept applies but the mechanism is continuous. Instead of stepping through discrete price levels, your trade moves along a smooth curve. A large trade pushes the price further along the \(x \cdot y = k\) curve, and the average execution price is worse than the spot price at the start.
Price impact is the component of slippage caused by your own trade moving the price. For a constant product AMM with reserves \(x\) (ETH) and \(y\) (USDC), buying \(\Delta x\) ETH costs you
\[\text{cost} = y - \frac{x \cdot y}{x + \Delta x}\]The average execution price is \(\text{cost} / \Delta x\), and the price impact is the percentage difference between this average price and the spot price before the trade.
// Calculate price impact for a given trade size
func calculatePriceImpact(reserveETH, reserveUSDC, ethToBuy float64) float64 {
k := reserveETH * reserveUSDC
spotPrice := reserveUSDC / reserveETH
// After buying ethToBuy ETH from the pool
newReserveETH := reserveETH - ethToBuy
newReserveUSDC := k / newReserveETH
usdcCost := newReserveUSDC - reserveUSDC
avgExecutionPrice := usdcCost / ethToBuy
priceImpactPct := ((avgExecutionPrice - spotPrice) / spotPrice) * 100
fmt.Printf("Spot price: $%.2f\n", spotPrice)
fmt.Printf("Avg exec price: $%.2f\n", avgExecutionPrice)
fmt.Printf("Price impact: %.2f%%\n", priceImpactPct)
fmt.Printf("New spot price: $%.2f\n", newReserveUSDC/newReserveETH)
return priceImpactPct
}
// Pool with 1,000 ETH and 2,000,000 USDC
calculatePriceImpact(1000, 2000000, 1) // ~0.1% impact
calculatePriceImpact(1000, 2000000, 10) // ~1.0% impact
calculatePriceImpact(1000, 2000000, 100) // ~11.1% impact
The relationship is nonlinear. Doubling the trade size more than doubles the price impact. A trade that is 1% of the pool reserves has roughly 1% impact, but a trade that is 10% of the reserves has roughly 11% impact. This is why pool depth matters so much.
Because slippage is unpredictable (other trades can land before yours), DeFi protocols provide parameters to protect you. The most important one is amountOutMin, the minimum amount of output tokens you will accept. If the actual output would be less than this threshold, the entire transaction reverts and you only lose gas fees.
// Uniswap V2 Router swap with slippage tolerance
func swapWithSlippageProtection(
client *ethclient.Client,
routerAddr common.Address,
tokenOut common.Address,
amountIn *big.Int,
auth *bind.TransactOpts,
) (*types.Transaction, error) {
parsed, _ := abi.JSON(strings.NewReader(routerABI))
router := bind.NewBoundContract(routerAddr, parsed, client, client, client)
weth := common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
path := []common.Address{weth, tokenOut}
// Get expected output
var result []interface{}
router.Call(&bind.CallOpts{}, &result, "getAmountsOut", amountIn, path)
amounts := result[0].([]*big.Int)
expectedOut := amounts[1]
// Set 0.5% slippage tolerance (50 bps)
bps := big.NewInt(10000)
slippage := big.NewInt(50)
amountOutMin := new(big.Int).Sub(bps, slippage)
amountOutMin.Mul(expectedOut, amountOutMin)
amountOutMin.Div(amountOutMin, bps)
// Deadline 5 minutes from now
deadline := big.NewInt(time.Now().Unix() + 300)
// Send ETH with the transaction
auth.Value = amountIn
tx, err := router.Transact(auth, "swapExactETHForTokens",
amountOutMin, // reverts if output < this
path,
auth.From,
deadline, // reverts if block.timestamp > this
)
return tx, err
}
The deadline parameter adds a second layer of protection. If your transaction sits in the mempool too long and the market has moved significantly, the deadline ensures it expires rather than executing at a stale price.
Because Ethereum transactions are visible in the public mempool before they are included in a block, bots can see your pending swap and exploit your slippage tolerance. The most common exploit is the sandwich attack.
A sandwich bot sees your pending swap for 10 ETH with 1% slippage tolerance. It submits a transaction before yours (frontrun) that buys ETH, pushing the price up. Your swap then executes at the inflated price, but still within your slippage tolerance. The bot then submits a transaction after yours (backrun) that sells the ETH it bought, pocketing the difference. You got a worse price, and the bot extracted value from you.
This is a form of MEV (Maximal Extractable Value). Validators and searcher bots can reorder transactions within a block to extract value from other users. Sandwich attacks are the most visible form, but MEV also includes arbitrage and liquidations.
There are several practical strategies to reduce the slippage you experience.
For the broader framework of trading mechanics that produce slippage, see Market Microstructure. For the related concept of the baseline trading cost, see The Bid-Ask Spread. For how transactions travel through the mempool before execution, see Sending Transactions on Ethereum. For production patterns on managing transaction ordering, see Nonce Management Patterns.