Sign in

AMM

The problem: trading without an order book

Centralized exchanges work with order books. Buyers post bids, sellers post asks, the exchange matches them. On chain, this is hard. Every post and every cancel is a transaction that costs gas. A market maker who wants to update quotes every block goes bankrupt fast.

Uniswap V2 takes a different approach. Instead of matching orders, it holds a pool of both tokens and uses a formula to set the price. Anyone can swap one token for the other against the pool. Anyone can add tokens to the pool to make it deeper. The price comes out of one piece of math.

The pool and its rule

A V2 pool is a contract that holds two tokens. We'll call them token A and token B. The contract enforces one rule:

x * y = k

x is how much of token B the pool holds. y is how much of token A the pool holds. k is the product, and the pool keeps k constant across every swap.

Suppose the pool currently holds 100 of token A and 100 of token B. Then x = 100, y = 100, and k = 10,000. After any swap, the product of the two reserves must still equal 10,000. That's all the rule says.

Walking through one swap

Let's see what happens when someone trades against this pool. A trader wants to swap token B for token A. They put 10 of token B into the pool. What does the contract give them back?

Step one: after the trader's deposit, the pool's reserve of token B has grown by 10.

x_new = 100 + 10 = 110

Step two: to keep k = 10,000, the pool's reserve of token A must shrink to whatever value satisfies 110 * y_new = 10,000.

y_new = 10,000 / 110 = 90.91

Step three: the pool used to have 100 of token A. Now it must have 90.91. The difference goes to the trader.

trader receives = 100 - 90.91 = 9.09 of token A

That's it. The trader put in 10 of token B and got 9.09 of token A. There was no price quote, no order matching, no oracle. The output was simply "whatever amount keeps the product equal to 10,000."

Written as a formula in general terms, with Δx for the input and Δy for the output:

Δy = y - k / (x + Δx)

In plain words: the new reserve of token A must equal k divided by the new reserve of token B, and the trader gets whatever the old reserve was minus that. After simplification:

Δy = (y * Δx) / (x + Δx)

That's the swap formula. It's just the invariant x * y = k rearranged to solve for the output.

The pool's reserves always sit on the curve x * y = k reserve of token B (x) reserve of token A (y) x · y = k before (X, Y) X Y after (X + Δx, Y − Δy) X + Δx Y − Δy Δx in Δy out A swap pushes the pool from one point on the curve to another. The product x · y stays the same.

The curve x * y = k is a hyperbola. The pool's current state is always one specific point on this curve. A swap is a movement from one point on the curve to another point on the same curve. The trader's input pushes the pool to the right along the curve, and the contract gives back enough token A to make the pool's new position satisfy the invariant.

The price changes after every swap

Here's the part that makes the system work. The pool's reserves change after every swap, which means the next trader doing the same swap gets a different result.

Continuing the example. After the first trader's swap, the pool is at (110, 90.91). A second trader wants to do the same thing, putting in another 10 of token B.

x_new = 110 + 10 = 120
y_new = 10,000 / 120 = 83.33
second trader receives = 90.91 - 83.33 = 7.58 of token A

The first trader got 9.09 of token A for their 10. The second trader gets only 7.58 for theirs. Same input, smaller output.

A third trader comes along. Same 10 of token B.

x_new = 120 + 10 = 130
y_new = 10,000 / 130 = 76.92
third trader receives = 83.33 - 76.92 = 6.41 of token A

9.09, then 7.58, then 6.41. Each trader gets less than the one before, even though they all put in the same amount. The pool has been giving away token A and receiving token B, so token A is becoming scarcer in the pool, and the pool charges more for it.

This is the price discovery mechanism. The pool doesn't ask anyone what the price should be. It just defends the invariant, and the invariant produces prices that respond to supply and demand. If token A is in high demand (lots of traders putting B in to get A out), the reserve of A shrinks fast, and the price of A in B rises fast. If demand fades, no swaps happen, and the price stays put.

Three traders, same input. Each one gets less output than the last. reserve of token B (x) reserve of token A (y) x · y = k start 100 / 100 23.08 14.42 9.87 trade 1 trade 2 trade 3 Each trader puts in the same Δx = 30 of token B: trade 1: pool was 100 / 100 gets 23.08 of A trade 2: pool was 130 / 76.92 gets 14.42 of A trade 3: pool was 160 / 62.50 gets 9.87 of A

(The diagram uses larger trades, Δx = 30, so the differences in output stand out visually. The arithmetic works the same way as the Δx = 10 walkthrough above.)

The brackets on the right side show how much token A each trader received. The first trader's bracket is the tallest. Each subsequent trader gets a shorter bracket. Look at the curve itself: each trade segment slides further down and to the right, and the curve gets flatter as it goes. Flatter curve means less token A given out per token B taken in.

Why bigger trades face worse rates

What if a single trader had wanted to buy all that token A in one shot instead of three separate trades? They'd put Δx = 90 (the total of all three) into the same starting pool.

x_new = 100 + 90 = 190
y_new = 10,000 / 190 = 52.63
trader receives = 100 - 52.63 = 47.37 of token A

47.37 is exactly the sum of 23.08 + 14.42 + 9.87. No coincidence. The pool ends at the same point whether you arrive in one big swap or three small ones, so the total output is identical.

But here's the catch: the single 90-token trade averaged a rate of 47.37 / 90 = 0.53 token A per token B. The first 30-token trade by itself got 23.08 / 30 = 0.77 token A per token B. The bigger your trade relative to the pool, the worse your average rate, because you push deeper into the unfavorable part of the curve in one move.

This effect has a name. It's called price impact. A trader doing a small swap against a deep pool barely moves the price. A trader doing a swap that's a sizable fraction of the pool gets a noticeably worse rate. There's nothing the contract can do about this: the rate is just what the invariant produces.

Where the reserves come from: liquidity providers

The pool needs reserves before anyone can swap against it. Liquidity providers (LPs) supply them.

An LP deposits both tokens. They must deposit them in the same ratio the pool currently holds. If the pool is at 100 of token A and 200 of token B, an LP depositing 1 of token A must also deposit 2 of token B. Depositing out of ratio would change the pool's spot price away from the market price, which would invite arbitrage and effectively let the LP donate value to whoever shows up next.

In exchange for the deposit, the LP receives LP tokens. LP tokens are themselves an ERC-20 issued by the pool contract. They represent a share of the pool. If an LP owns 10% of the LP token supply, they own a claim on 10% of the pool's reserves. To take their money out, they burn their LP tokens and the contract sends them their share.

The first deposit into a fresh pool is special, because there's no existing ratio. The first LP sets it. The LP tokens minted to them are computed as sqrt(x_deposit * y_deposit). The square root keeps the supply scale-invariant — depositing more value gives proportionally more LP tokens, regardless of which token units you use.

For every later deposit, LP tokens are minted in proportion. If an LP adds 5% to the pool's reserves (in the current ratio), the contract mints 5% of the current LP supply to them.

How LPs get paid: the fee

LPs take real risk by providing liquidity. The composition of the pool changes as traders swap, which means an LP can end up holding more of whichever token has fallen in value and less of whichever has risen. To compensate for that risk, the pool charges traders a fee on every swap.

The fee is 0.3% of the input amount, and the mechanism for collecting it is elegant. The fee is not sent anywhere. It is just left in the pool. The trader puts in Δx, the contract treats only 99.7% of Δx as effective input when computing the output, and the remaining 0.3% stays in the reserves. The pool's reserves grow by the full Δx, but the trader only gets paid as if 99.7% had been added.

The swap formula with the fee becomes:

Δy = (y * Δx * 997) / (x * 1000 + Δx * 997)

That's the same formula as before, but with Δx replaced by Δx * 997 / 1000 (the 99.7% that "counts"). The * 1000 factor in the denominator is just the standard way to handle the 0.3% in integer arithmetic without using floating point.

Now here's the part that matters. The fee adds to the reserves but no new LP tokens are minted to cover it. So the total LP supply stays the same while the reserves grow. That means each LP token now claims a slightly bigger slice of the pool. LPs don't have to claim anything, don't have to call any function — their LP tokens just become more valuable as fees accrue.

Fees accrue to LPs: pool grows, LP supply doesn't Stage 1: Alice provides initial liquidity Pool reserves: 100 USDC, 0.1 ETH LP token total supply: 3.16 (= sqrt(100 * 0.1)) Alice owns 100% of LP tokens many swaps over time Stage 2: Time passes, swaps generate fees Pool reserves grew: 108 USDC, 0.108 ETH LP token total supply unchanged: 3.16 Each LP token is worth more now Alice burns LP tokens Stage 3: Alice withdraws Alice receives 108 USDC, 0.108 ETH Why LP tokens become more valuable Every swap collects a 0.3% fee. The fee stays in the pool. No LP tokens are minted for fees. Reserves grow, supply stays flat. Each LP token's share of the pool stays the same percentage, but that percentage of a bigger pool means more underlying tokens. value per LP token = reserves / total LP supply

There's no separate fee accounting in the contract. No per-LP fee tracker. No claimFees function. The growing-reserves, flat-supply structure does all the work implicitly. This is one of the cleanest pieces of V2's design.

Impermanent loss: the cost LPs actually pay

Earlier the lecture mentioned impermanent loss as the LP's risk and skipped the details. Time to fill them in. Fees are the reward side, impermanent loss is the cost side, and the LP picture only makes sense with both on the table.

The mechanism is simple. The constant-product invariant rebalances the pool every time price changes. If ETH appreciates, swappers buy ETH from the pool, so the pool loses ETH and gains USDC. By the time price has doubled, the pool holds less ETH than at entry. An LP who owns a fixed share of the pool holds proportionally less ETH too. They missed part of the ETH appreciation.

The same happens in reverse. If ETH falls, the pool buys ETH from sellers and ends up with more of it. The LP now holds more of the depreciated asset.

Either direction, the pool's rebalancing rule amounts to "sell winners, buy losers." From a passive investor's standpoint that's the wrong direction. Holding the original tokens would have done better.

The size of the loss is a clean function of the price ratio r between exit and entry:

IL = 2·√r / (1 + r) − 1

Impermanent loss: any price move makes the LP worse off than HODLing LP value vs HODL value, as a function of price change 0% −5% −10% −15% −20% LP loss vs HODL 0.25x 0.5x 1x (entry) 2x 4x price ratio (current price / entry price) no loss 2x → −5.7% 0.5x → −5.7% 3x → −13.4% 4x → −20% 0.25x → −20% Worked example: ETH doubles from $4,000 to $8,000 At entry (ETH = $4,000): Alice has 1 ETH + 4,000 USDC, total value = $8,000. She splits her capital evenly. Path A — HODL She keeps her tokens in her wallet. When ETH hits $8,000: 1 ETH × $8,000 = $8,000 + 4,000 USDC = $4,000 total = $12,000 Path B — LP in V2 pool Pool rebalances as price rises. When ETH hits $8,000, she now holds: 0.707 ETH × $8,000 = $5,657 + 5,657 USDC = $5,657 total = $11,313 Difference: $687 lost to rebalancing (the −5.7% in the chart above)

A few values worth memorizing:

  • 1.25x price move → 0.6% loss
  • 1.5x → 2.0%
  • 2x → 5.7%
  • 3x → 13.4%
  • 4x → 20%

The loss is symmetric. A 2x move and a 0.5x move both produce 5.7% loss. Magnitude matters, direction does not.

Why "impermanent"

The name is mostly historical. The loss is only impermanent in one specific sense: if price returns to entry before you withdraw, the loss disappears. The pool has rebalanced back to its original composition and you get out what you put in.

If you withdraw at any other price, the loss is realized and permanent. Most LP exits happen at prices different from entry, so the "impermanent" framing usually amounts to a hope that price returns. The loss is best understood as the cost of providing liquidity, paid in full when you exit.

Fees vs IL

The central LP question: do earned fees exceed IL?

For stablecoin pairs the price ratio barely moves, so IL stays negligible and small fee income wins. For volatile pairs the math is closer. ETH/USDC on a 0.3% pool needs enough trading volume to pay back the IL accumulated from price movement. Pools with high volume-to-TVL ratios earn back IL and then some. Quiet pools don't.

The cleanest mental model: LP'ing in a constant-product AMM is selling volatility insurance. You collect premiums (fees) and pay claims (IL) when the underlying moves. Some books are profitable, some aren't.

Two structural choices worth knowing

One pair per contract. A V2 pair is a self-contained contract holding exactly two tokens. The pool for USDC/WETH is a different deployed contract from the pool for DAI/USDC. This isolates risk between markets, simplifies the math (each pool tracks two reserves and one LP supply, nothing else), and lets a separate Factory contract deploy new pairs deterministically as people want new markets.

Pairs hold ERC-20s only. A pair contract cannot hold native ETH. Anyone wanting to trade ETH against an ERC-20 must first wrap it into WETH (the ERC-20 wrapper for ETH) and trade the WETH/token pair. This isn't a usability complaint, it's deliberate. By accepting only ERC-20s, the pool has one code path for token movement. Handling both native ETH and ERC-20s would double the surface area of every function and create edge cases.

A subtlety: the minimum liquidity lock

The first LP into a fresh pool gets sqrt(x_deposit * y_deposit) LP tokens. V2 actually mints them slightly less than that, and burns a small amount (1000 wei worth) of LP tokens by sending them to address(0), where nobody can ever spend them. This is the minimum liquidity lock.

Why is this needed? Without it, an attacker can manipulate the share price for the next depositor. Roughly: an attacker deposits a tiny amount as the first LP, mints a tiny number of LP tokens (say, 1 wei worth of LP), then transfers a large donation of tokens directly into the pool contract (bypassing the add-liquidity function entirely, just calling transfer on the underlying token). The pool's reserves balloon, but the LP token supply stays at the tiny number from the first deposit.

Now the next user who tries to deposit normally finds that their proportional share rounds down to zero LP tokens, because each LP token now represents a hugely inflated amount of underlying. They lose their deposit to the attacker, who can then withdraw and walk away with the donation plus the victim's funds.

Burning 1000 wei of LP tokens at the first deposit means there's always a baseline supply that nobody owns and nobody can manipulate around. This caps how much an attacker can inflate the per-share value. It's a small detail, and the naive implementation (just mint sqrt(x * y) to the depositor) has this bug. Locking 1000 wei at construction makes the bug go away.

You'll need to think about this when you build your own version.