Want to explore these formulas interactively? → UniswapV2 Interactive Simulator lets you configure reserves, execute trades, and watch contract internals update live in your browser.
UniswapV2 launched in May 2020 and changed DeFi forever. At its core is a formula so elegant it fits on a business card: x · y = k. Yet this three-character equation encodes price discovery, liquidity provision, fee distribution, and arbitrage mechanics all at once.
This post derives the math from scratch, explains every edge case, and builds intuition for what happens at the protocol level every time you swap tokens.
Table of contents
Open Table of contents
The Constant Product Formula
A UniswapV2 pool holds two tokens — call them token X and token Y. At any point in time:
x · y = kWhere:
x= reserve of token X in the pooly= reserve of token Y in the poolk= a constant (the “invariant”)
When you trade, you add tokens of one type and receive tokens of the other type. The key constraint is that k must remain constant (modulo fees — more on that shortly).
A Concrete Example
Imagine a USDC/ETH pool with:
x = 1,000,000 USDCy = 500 ETHk = 1,000,000 × 500 = 500,000,000
The implicit price of ETH is x/y = 2,000 USDC/ETH. But this is not a fixed price — it moves as trades happen.
Price Impact and Slippage
Suppose you want to buy Δy ETH from the pool by sending Δx USDC. The invariant must hold:
(x + Δx) · (y - Δy) = kSolving for Δy:
y - Δy = k / (x + Δx)Δy = y - k / (x + Δx)Δy = y - (x·y) / (x + Δx)Δy = y · Δx / (x + Δx)This is the exact output formula. Let’s plug in numbers: buying ETH by sending 10,000 USDC:
Δy = 500 × 10,000 / (1,000,000 + 10,000)Δy = 5,000,000 / 1,010,000Δy ≈ 4.95 ETHThe “naive” price would give you 10,000 / 2,000 = 5 ETH. You actually get 4.95 ETH — the missing 0.05 ETH is price impact, caused by your trade moving the market.
The Price Impact Formula
The price impact percentage for buying Δx worth of X tokens:
Price Impact = Δx / (x + Δx) × 100%For our example: 10,000 / 1,010,000 = 0.99%
This scales non-linearly. A trade of 100,000 USDC (10% of liquidity) would have ~9% price impact. This is why large trades are fragmented across multiple routes in aggregators like 1inch.
The 0.3% Fee
UniswapV2 charges a 0.3% fee on every swap, paid in the input token. In practice, the protocol applies the fee by only counting 99.7% of the input:
Δx_effective = Δx × 0.997The actual formula in the contract uses integer arithmetic to avoid floating point:
uint amountInWithFee = amountIn.mul(997);uint numerator = amountInWithFee.mul(reserveOut);uint denominator = reserveIn.mul(1000).add(amountInWithFee);amountOut = numerator / denominator;This is equivalent to:
Δy = (y · Δx · 997) / (x · 1000 + Δx · 997)Applying this to our example:
Δy = (500 × 10,000 × 997) / (1,000,000 × 1000 + 10,000 × 997)Δy = 4,985,000,000 / 1,009,970,000Δy ≈ 4.936 ETHSo of the “price impact loss” of 0.05 ETH, about 0.015 ETH goes to the fee (0.3% of input ≈ 30 USDC ÷ 2000 ≈ 0.015 ETH). The rest is genuine slippage.
Liquidity Provider Shares
When you add liquidity, you receive LP tokens representing your proportional share of the pool. The pool mints LP tokens at a rate that preserves proportionality:
LP_minted = total_LP_supply × min(Δx/x, Δy/y)The min() ensures you can’t game the ratio. If the pool has 1M USDC and 500 ETH (ratio 2000:1) and you try to add 10k USDC and 10 ETH (ratio 1000:1), you’d only get credit for the lesser proportion.
Impermanent Loss
Here’s the painful part of LP provision. Suppose ETH doubles from $2,000 to $4,000. Arbitrageurs will buy ETH from the pool until its price matches the market, shifting reserves to a new equilibrium:
x_new · y_new = kx_new / y_new = 4,000 (new market price)
→ x_new = √(k × 4000) = √(500,000,000 × 4000) ≈ 1,414,213 USDC→ y_new = √(k / 4000) ≈ 353.55 ETHYour pool holdings are now worth:
353.55 ETH × $4,000 = $1,414,213+ 1,414,213 USDC = $1,414,213- Total: $2,828,427
If you had just held: 500 ETH × $4,000 + 1,000,000 = $3,000,000
Impermanent loss = ($3,000,000 - $2,828,427) / $3,000,000 ≈ 5.72%
The general formula for IL given a price ratio change r = P_new/P_old:
IL = 2√r/(1+r) - 1 (always negative — LP value < hold value)| Price change | IL (loss vs holding) |
|---|---|
| 1.25x (25% up) | -0.6% |
| 1.5x (50% up) | -2.0% |
| 2x (doubled) | -5.7% |
| 3x | -13.4% |
| 5x | -25.5% |
| 10x | -42.5% |
This is why “impermanent loss” is a misnomer — at extreme price moves, it’s very permanent.
The TWAP Oracle
UniswapV2 introduced an on-chain price oracle using a Time-Weighted Average Price (TWAP). Every block, the contract accumulates:
price0CumulativeLast += reserve1/reserve0 × timeElapsedprice1CumulativeLast += reserve0/reserve1 × timeElapsedUsing fixed-point arithmetic (UQ112×112 format — 112 bits integer, 112 bits fractional). To get the TWAP over a period:
TWAP = (price0Cumulative_t2 - price0Cumulative_t1) / (t2 - t1)This is resistant to single-block manipulation because an attacker would need to maintain a false price for many blocks (very expensive). Most DeFi protocols that need a price feed use a 30-minute TWAP at minimum.
The Mint and Burn Functions
When you add the very first liquidity (pool creation), there’s a bootstrapping problem — there’s no existing LP supply to calculate proportions from. UniswapV2 solves this with:
initial_LP = √(Δx × Δy) - MINIMUM_LIQUIDITYWhere MINIMUM_LIQUIDITY = 1000 (permanently locked to prevent division-by-zero). The geometric mean ensures the initial LP token value doesn’t depend on the ratio you seed with.
The sync() and skim() Functions
Two safety functions handle cases where the actual token balances drift from recorded reserves (e.g., someone sends tokens directly without using the swap function):
sync(): Updates reserves to match actual balances. Used after airdrops or accidental transfers.skim(to): Sends excess tokens (balance - reserve) to an address. Lets anyone extract tokens that were sent to the pool by mistake.
Comparing AMMs: Why x·y=k Works
The constant product formula is just one point in a spectrum of bonding curves:
| Formula | Characteristic |
|---|---|
x·y=k (UniswapV2) | Infinite price range, always liquid |
(x+y)=k (linear) | Fixed price, capital efficient but runs out |
| Stableswap (Curve) | Between linear and xy=k, good for pegged pairs |
x·y=k with ticks (V3) | Concentrated liquidity in ranges |
The constant product works precisely because it never reaches zero — you can always trade, but large trades cost exponentially more. This creates natural arbitrage incentives.
What This Means for Developers
If you’re building on top of AMMs — whether for arbitrage, MEV, integrations, or your own DeFi protocol — understanding this math unlocks everything:
- Routing algorithms (1inch, Paraswap): Split trades across pools to minimize total price impact
- Flash swap arbitrage: Borrow X, buy cheap on DEX A, sell expensive on DEX B, repay — all in one transaction
- Liquidation bots: Use flash swaps to liquidate underwater positions without upfront capital
- Price oracles: Use TWAP to get manipulation-resistant on-chain prices
The code is simple. The math is beautiful. The emergent behavior — a decentralized, self-regulating market — is genuinely remarkable.
Further Reading
- UniswapV2 Whitepaper — the original 8-page paper
- UniswapV2Core contracts — the full implementation in ~400 lines of Solidity
- Next post in this series: UniswapV3 — Concentrated Liquidity and Ticks (Jun 2024)