I reread Uniswap V3 and realized the hardest part is not the math, but the mental model
--
If you start from Uniswap V2, you usually begin with a very familiar model:
x * y = k
Two assets. One pool. One curve. Price changes simply mean the state point moves along that curve.
That model is elegant, and it works well for V2.
But if you try to use the same mental model to understand Uniswap V3, you will quickly get confused. V3 is not just “V2 with better formulas.” What it really does is break one global AMM curve into many locally active price ranges.
That is the key idea behind concentrated liquidity.
This is also why I think the hardest part of Uniswap V3 is not the formula itself, but the mental model you use at the beginning.
I put together a full set of architecture notes here:
https://uniswap-v3-architecture-notes.vercel.app/
1. The problem with V2: the money is in the pool, but not all of it is working
In Uniswap V2, liquidity is spread evenly across the entire price range (0, ∞).
That sounds simple, but it creates a capital efficiency problem.
A swap usually moves price only a small distance along the curve. In practice, only the liquidity near the current price is actually used. The rest just sits there, technically available, but not participating in the trade.
So the real problem is:
Most capital is idle most of the time.
That leads to a natural question:
If swaps only happen around the current price, why should LPs provide liquidity everywhere?
That is exactly the idea Uniswap V3 introduces: concentrated liquidity.
2. The essence of V3: not “market making across the whole curve,” but “market making inside a range”
In V3, an LP does not spread liquidity across the entire price space.
Instead, the LP chooses a limited range:
[p_lower, p_upper]
So a position is no longer just “I deposit money into the pool.”
It becomes:
“I provide liquidity only inside this price range.”
That single change makes the whole protocol much more powerful, but also much harder to reason about.
Because now you have to answer questions like:
- Where should I place my range?
- How wide should the range be?
- What happens when price leaves my range?
- Why do I sometimes end up holding only one token?
- Why do I sometimes stop earning fees?
All of those questions come from the same root cause:
Your liquidity is only active inside the range.
3. A position is not always working
This is the most important mental model shift in V3.
Whether a position participates in a swap depends only on whether the current price is inside its range:
tickLower <= currentTick < tickUpper
When this condition is true:
- the position provides liquidity
- it participates in swaps
- it earns fees
When price leaves the range, the position becomes inactive.
There are three cases:
Case 1: price is below the range
currentTick < tickLower
In this case, the position does not participate in swaps.
The asset is effectively all token0.
You can think of it as: price has not yet entered your market-making range.
Case 2: price is inside the range
tickLower <= currentTick < tickUpper
In this case, the position is active.
It holds both token0 and token1, and the proportions change as price moves.
This is the part that many people misunderstand at first. V3 positions are not static. They continuously transform from token0 into token1, or from token1 into token0, as price moves through the range.
Case 3: price is above the range
currentTick >= tickUpper
In this case, the position is also inactive.
The asset is effectively all token1.
So the position is not “always on.” It is only active while price stays inside the chosen interval.
4. Why virtual liquidity matters
If V3 only allowed range-based liquidity, it would still have a problem:
How do you keep a continuous AMM price curve if liquidity only exists locally?
The answer is virtual liquidity.
The idea is simple:
The protocol does not treat the local curve as isolated. Instead, it embeds the real reserves into a larger constant-product system by introducing virtual reserves.
That gives us a complete curve again:
(x_R + x_V)(y_R + y_V) = L²
Where:
- x_R, y_R are real reserves
- x_V, y_V are virtual reserves
- L is liquidity
This is a very elegant design.
It allows LPs to provide local liquidity, while the protocol still behaves as if it is operating on a full AMM curve.
So V3 does not throw away x * y = k.
It localizes it.
5. Why V3 uses ticks instead of continuous prices
Once you understand range-based liquidity, the next question is: why does V3 use ticks?
The answer is efficiency.
In V2, price can be thought of as a simple reserve ratio:
P = y / x
But in V3, price must be discretized, because the protocol needs to answer questions like:
- Which range is the current price in?
- Where is the next active boundary?
- Which ticks are initialized?
- When should a swap cross into the next range?
That is why V3 uses:
P = 1.0001^tick
So tick is basically the integer index of price in logarithmic space.
This gives V3 several advantages:
- price can be indexed discretely
- liquidity can be stored by range
- initialized ticks can be found efficiently with a bitmap
- swaps can move step by step along the tick axis
That is also why V3 has both tick and sqrtPriceX96.
- tick is for positioning
- sqrtPriceX96 is for precise calculation
6. Swap is not one formula. It is a state machine.
A lot of people think a swap is just:
- input one token
- apply a formula
- output the other token
That is not how V3 works.
In V3, a swap is a continuous price movement under current liquidity, and whenever price reaches an initialized tick, liquidity changes discretely.
That means one swap is broken into many local steps.
Each step asks one question:
Given the current price, current liquidity, target price, and remaining input/output, how far can price move in this step?
That is what computeSwapStep does.
If the step does not reach a tick boundary, price stops inside the current range.
If the step reaches a boundary, the protocol crosses the tick, reads liquidityNet, updates the current liquidity, and continues.
So the entire swap engine looks like this:
- find the next initialized tick
- determine the target price for this step
- run computeSwapStep
- settle amountIn, amountOut, and fee
- if necessary, cross the tick and update liquidity
- repeat
That is why I say V3 swap is not just a calculation.
It is a state machine.
7. Fees are not paid out immediately
V2 fee distribution is simple because all liquidity is global.
Fees go into the pool, and LPs receive them through their share of the pool.
V3 is different.
Because liquidity is spread across ranges, only active liquidity participates in a swap. That means different LPs participate at different times and in different ranges.
So V3 cannot just split fees globally.
If it tried to do that by scanning every position on every swap, gas costs would explode.
So V3 uses a different idea:
- accumulate fees
- record snapshots
- settle later
The core concept is feeGrowth.
You can think of feeGrowth as:
“how much fee has been earned per unit of liquidity so far.”
When a position is settled, the protocol looks at:
feeGrowthInsideNow — feeGrowthInsideLast
Then multiplies by the position’s liquidity.
That is how the protocol computes the amount owed:
tokensOwed += liquidity * (feeGrowthInsideNow — feeGrowthInsideLast)
This is one of the most elegant parts of the V3 design.
It avoids per-swap distribution and avoids iterating over all LPs.
8. Where V3 becomes hard
If you only look at the concept, V3 can be summarized in one sentence:
Concentrated liquidity.
But once you read the code, you realize the hard part is how all the pieces fit together:
- how liquidity is derived from token0 and token1
- how tick maps to price
- why sqrtPriceX96 uses Q96 fixed-point math
- how tickBitmap finds the next initialized tick
- how swaps are split into multiple steps
- how liquidityNet updates on crossing
- how feeGrowthInside is calculated
- how positions are settled lazily
- how TWAP is built from tickCumulative
Each piece is understandable on its own.
What makes V3 hard is that all of them work together around the same core problem:
How does price move through range-based liquidity?
That is the real architecture question.