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 compute and rent. A market maker who wants to update quotes every slot goes bankrupt fast.
Uniswap V2 took a different approach, and Solana AMMs inherited it. Instead of matching orders, the AMM 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 pool on Solana is not a contract that holds tokens, the way Uniswap V2's pool is a smart contract on Ethereum. Solana programs are stateless code. A pool is a set of accounts that one AMM program operates on. We will look at the account layout in a moment. For now, treat the pool as a logical thing that holds two tokens, token A and token B, and enforces one rule:
undefinedx * y = kx 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 is all the rule says.
Walking through one swap
Let us 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 program give them back?
Step one. After the trader's deposit, the pool's reserve of token B has grown by 10.
undefinedx_new = 100 + 10 = 110Step two. To keep k = 10,000, the pool's reserve of token A must shrink to whatever value satisfies 110 * y_new = 10,000.
undefinedy_new = 10,000 / 110 = 90.91Step three. The pool used to have 100 of token A. Now it must have 90.91. The difference goes to the trader.
undefinedtrader receives = 100 - 90.91 = 9.09 of token AThat is 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:
undefinedΔ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:
undefinedΔy = (y * Δx) / (x + Δx)That is the swap formula. It is the invariant x * y = k rearranged to solve for the output.
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 program gives back enough token A to make the pool's new position satisfy the invariant.
The price changes after every swap
Here is 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 AThe 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 A9.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 does not 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, with 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.
The diagram uses larger trades of Δ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 later 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 would 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 A47.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 is 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 is called price impact. A trader doing a small swap against a deep pool barely moves the price. A trader doing a swap that is a sizable fraction of the pool gets a noticeably worse rate. There is nothing the program 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, or 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. On Solana, LP tokens are a separate SPL mint that belongs to the pool. The mint authority on that mint is a PDA of the AMM program, so only the AMM program can create new LP tokens. The LP tokens themselves are normal SPL tokens, transferable between accounts like any other token, and they represent a share of the pool. If an LP owns 10 percent of the LP token supply, they own a claim on 10 percent of the pool's reserves. To take their money out, they hand back their LP tokens to be burned, and the program sends them their share of the underlying.
The first deposit into a fresh pool is special, because there is 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 percent to the pool's reserves in the current ratio, the program mints 5 percent 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 typically 0.3 percent of the input amount on a standard constant-product pool, 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 program treats only 99.7 percent of Δx as effective input when computing the output, and the remaining 0.3 percent stays in the reserves. The pool's reserves grow by the full Δx, but the trader only gets paid as if 99.7 percent had been added.
The swap formula with the fee becomes:
undefinedΔy = (y * Δx * 997) / (x * 1000 + Δx * 997)That is the same formula as before, with Δx replaced by Δx * 997 / 1000, the 99.7 percent that counts. The * 1000 factor in the denominator is the standard way to handle the 0.3 percent in integer arithmetic without using floating point.
Now here is 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 do not have to claim anything, do not have to call any instruction. Their LP tokens just become more valuable as fees accrue.
There is no separate fee accounting in the program. No per-LP fee tracker. No claim-fees instruction. The growing-reserves, flat-supply structure does all the work implicitly. This is one of the cleanest pieces of the V2 design, and it ports to Solana unchanged.
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 SOL appreciates, swappers buy SOL from the pool, so the pool loses SOL and gains USDC. By the time price has doubled, the pool holds less SOL than at entry. An LP who owns a fixed share of the pool holds proportionally less SOL too. They missed part of the SOL appreciation.
The same happens in reverse. If SOL falls, the pool buys SOL 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 selling winners and buying losers. From a passive investor's standpoint that is 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:
undefinedIL = 2 * sqrt(r) / (1 + r) - 1A few values worth memorizing:
- 1.25x price move gives 0.6 percent loss
- 1.5x gives 2.0 percent
- 2x gives 5.7 percent
- 3x gives 13.4 percent
- 4x gives 20 percent
The loss is symmetric. A 2x move and a 0.5x move both produce 5.7 percent 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. SOL/USDC on a 0.3 percent 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 do not.
The cleanest mental model. LP'ing in a constant-product AMM is selling volatility insurance. You collect premiums in fees and pay claims in IL when the underlying moves. Some books are profitable, some are not.
Two structural choices worth knowing
The first one is the biggest architectural difference between Uniswap V2 on Ethereum and any constant-product AMM on Solana, and it is worth slowing down for.
One program, many pools. On Ethereum, every new market is a freshly deployed contract. The USDC/WETH pair is a different contract from the DAI/USDC pair, and a factory contract deploys them deterministically as people want new markets. On Solana, no new program gets deployed. The same AMM program serves every market on the network. USDC/SOL, USDC/JUP, BONK/SOL, and every other pair run on the same deployed code.
What makes each pool distinct is a set of accounts. A pool consists of four accounts, all of them PDAs derived from the two underlying token mints.
- A pool config account that holds the pool's metadata. The two token mints, the current reserves, the fee rate, the LP mint address, the bumps. This is where the program reads
xandyfrom. - A vault for token A, an SPL token account custodying the actual token A. Its authority is a PDA of the AMM program, so only the AMM program can move tokens out of it.
- A vault for token B, the same shape, custodying token B.
- An LP mint, the SPL mint for this pool's LP tokens. Its mint authority is a PDA of the AMM program.
All four accounts are derived from the same seeds, typically the two token mints in canonical order. Given the two mints, anyone can find every account that belongs to that pool, without needing a registry. The risk isolation between markets that Ethereum gets from separate contracts, Solana gets from separate account sets. The code path is shared, but a bug in pool USDC/SOL cannot directly drain the vaults of pool BONK/USDC, because each pool's vaults have their own PDA authority and the program checks them.
Pools hold SPL tokens only. A pool's vault accounts are SPL token accounts, and they can only hold SPL tokens. To trade native SOL against another token, the user wraps SOL into wrapped SOL, an SPL token at the well-known mint So11111111111111111111111111111111111111112, before depositing. Solana's tooling makes the wrap and unwrap nearly invisible. Wrapped SOL accounts can be created and closed within the same transaction, so the user sees a normal "swap SOL for token" experience even though the program only ever handles SPL transfers. The reasoning is the same as on Ethereum. By accepting only SPL tokens, the program has one code path for token movement. Handling both native SOL and SPL tokens would double the surface area of every instruction 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. A correct implementation mints them slightly less than that, and locks a small amount, typically 1000 base units of the LP mint, by sending it to a non-spendable PDA or burning it outright. 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 and mints a tiny number of LP tokens, say 1 base unit. They then transfer a large donation of tokens directly into the pool's vaults, using a plain SPL transfer to the vault account address, bypassing the add-liquidity instruction entirely. The pool's reserves balloon, but the LP 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.
Locking 1000 base units of LP at the first deposit means there is 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 is a small detail. The naive implementation that just mints sqrt(x * y) to the depositor has this bug. Locking at the first deposit makes the bug go away.
There is one Solana wrinkle worth knowing. On Ethereum the lock is done by sending LP tokens to address(0), the zero address, which is unspendable. On Solana there is no equivalent universal sink. Two patterns work. You can burn the LP tokens by calling the SPL Token burn instruction, which permanently removes them from the supply. Or you can mint them to a PDA that has no instruction able to spend them, which keeps the supply intact while making the tokens unreachable. Either works for the security property. Burning is cleaner because it does not require maintaining a sink account, and most production AMMs on Solana do that.
You will need to think about this when you build your own version.