The empty stadium: why v2 capital sits idle
Picture a Uniswap v2 ETH/USDC pool the way you learned it in the last two guides: an automated market maker holding a liquidity pool whose reserves obey the constant-product formula x·y = k. That single curve quotes a price at every point from ETH = \0.0001 all the way to ETH = \1,000,000. Your deposit is smeared evenly along the whole thing. But trades only ever cluster near today's price. So if ETH trades around \2,000, the liquidity you parked at \5 or \$200,000 is like buying every seat in a stadium when the crowd only ever fills the front three rows — paid for, and empty.
Uniswap v3 (May 2021) asked the obvious question: what if a liquidity provider could place capital only in a band around the current price — say ETH between \1,500 and \2,500 — and leave the dead zones empty? That is concentrated liquidity. The same dollar now provides far deeper liquidity and earns far more in trading fees per dollar — but only while the price stays inside your band, and at the cost of work you didn't have to do before.
Virtual reserves: a v2 curve, shifted to touch the axes
Here is the elegant trick. A v3 position in the price range [P_a, P_b] behaves exactly like a tiny v2 pool — same hyperbola — except the curve is shifted so that it hits the x-axis at P_b and the y-axis at P_a. Inside the range it satisfies a constant-product invariant on virtual reserves:
(x + L/sqrt(Pb)) * (y + L*sqrt(Pa)) = L^2 x, y = your REAL tokens in the position (token0, token1) L/sqrt(Pb) = virtual token0 added by the shift L*sqrt(Pa) = virtual token1 added by the shift L = the position's "liquidity" (its depth) # As long as the current price P is inside [Pa, Pb], the real amounts are: # x = L * (1/sqrt(P) - 1/sqrt(Pb)) # token0, e.g. ETH # y = L * (sqrt(P) - sqrt(Pa)) # token1, e.g. USDC
L is the liquidity — think of it as how deep the order book is, measured as the change in token1 reserves per unit change in √P. The contract never stores √price as a plain float; it keeps `sqrtPriceX96`, the square root of the price multiplied by 2⁹⁶, so all the depth math is exact integer arithmetic. Two corollaries fall straight out of the formulas above, and they matter enormously in practice:
# Real amounts a position of liquidity L holds when P is in range:
amount0 = L * (sqrtPb - sqrtP) / (sqrtP * sqrtPb) # token0
amount1 = L * (sqrtP - sqrtPa) # token1
# If price falls TO/THROUGH Pa -> position becomes 100% token0:
# amount0 = L * (sqrtPb - sqrtPa) / (sqrtPa * sqrtPb) amount1 = 0
# If price rises TO/THROUGH Pb -> position becomes 100% token1:
# amount0 = 0 amount1 = L * (sqrtPb - sqrtPa)
# Inverse: the liquidity a given deposit buys (take the binding side):
L = min( amount0 * (sqrtP * sqrtPb) / (sqrtPb - sqrtP),
amount1 / (sqrtP - sqrtPa) )Eight times the depth for the same money
Numbers make this vivid. Take ETH/USDC at P = 2,000 USDC per ETH. Two LPs each supply the same liquidity depth L = 100: one over the full v2 range [0, ∞], the other concentrated in [1,500, 2,500]. How much capital does each have to lock up?
import math
def amounts(L, P, Pa, Pb):
sP, sA, sB = math.sqrt(P), math.sqrt(Pa), math.sqrt(Pb)
sP = min(max(sP, sA), sB) # clamp price into the range
x = L * (sB - sP) / (sP * sB) # token0 (ETH)
y = L * (sP - sA) # token1 (USDC)
return x, y
L = 100
x2, y2 = amounts(L, 2000, 1e-12, 1e30) # v2: effectively [0, inf]
x3, y3 = amounts(L, 2000, 1500, 2500) # v3: concentrated band
val = lambda x, y, P: y + x * P # value in USDC
print(round(val(x2, y2, 2000), 1)) # 8944.3 USDC of capital
print(round(val(x3, y3, 2000), 1)) # 1071.3 USDC of capital
print(round(val(x2, y2, 2000) /
val(x3, y3, 2000), 2)) # 8.35x capital efficiency- The v2 LP must fund 2.236 ETH + 4,472 USDC — worth \$8,944 at \2,000/ETH — to achieve depth L = 100 everywhere, almost all of it idle far from \2,000.
- The v3 LP achieves the same L = 100 *within [1,500, 2,500]* with just 0.236 ETH + 599 USDC — worth \$1,071.
- Same depth quoted to traders, same fee earned per swap, but ~8.35× less capital at risk — that is the capital-efficiency gain. Freed capital can be deployed elsewhere, or the LP can just risk less.
Ticks: chopping the price line into 0.01% steps
An LP can't choose a continuous range — that would be impossible to track on-chain. Instead v3 discretizes the price axis into ticks. Tick i corresponds to the price 1.0001^i, so each tick is one basis point (0.01%) wide. Your range's bounds must snap to initialized ticks, and which ticks are usable depends on the pool's fee tier:
price(i) = 1.0001 ** i # price of token1 in units of token0 tick(p) = floor( ln(p) / ln(1.0001) ) tick(2000) -> 76012 # 1.0001^76012 ~= 2000 tick(1500) -> 73135 tick(2500) -> 78244 # Fee tier -> tick spacing (bounds must be multiples of it) # 0.01% -> 1 stable pairs (USDC/USDT) # 0.05% -> 10 correlated / stable-ish # 0.30% -> 60 most volatile pairs (ETH/USDC) # 1.00% -> 200 exotic / illiquid
The pool only ever cares about the active liquidity L — the sum of all positions whose range straddles the current tick. As a swap pushes the price and it crosses a tick boundary, the engine adds or removes that boundary's positions from L (its stored `liquidityNet`). Fees are tracked as a global `feeGrowthGlobal` per unit of liquidity, plus a `feeGrowthOutside` snapshot at each tick, so any position can later compute exactly the fees earned while it was in range. A simplified swap loop:
# Exact-input swap across concentrated liquidity (one direction)
sqrtP = current_sqrt_price
L = active_liquidity_at_current_tick
while amount_in_remaining > 0:
tick_next = next_initialized_tick(direction)
sqrtP_limit = sqrt_price_at_tick(tick_next)
# constant-product step on the VIRTUAL reserves of the active range:
in_used, out_got, sqrtP_new = compute_swap_step(
sqrtP, sqrtP_limit, L, amount_in_remaining, fee)
amount_in_remaining -= in_used
if sqrtP_new == sqrtP_limit: # price reached the tick edge
L += liquidity_net[tick_next] # switch boundary positions on/off
sqrtP = sqrtP_limit
else: # order filled inside this tick
sqrtP = sqrtP_new
breakWhen the price walks out of your range
Concentration is leverage, and leverage cuts both ways. Watch what the formulas do at the edges of our [1,500, 2,500] example. As ETH rises toward 2,500, the position is steadily selling ETH for USDC; the instant price reaches 2,500 it holds 0 ETH and 1,127 USDC — entirely the asset that just got more expensive to not hold. Symmetrically, if ETH falls to 1,500 the position is 0.582 ETH and 0 USDC — you bought ETH all the way down and are now 100% in the loser. Either way, once price is outside the band you earn no fees at all — you're a spectator until price returns.
This is the heart of the trade-off: concentration multiplies your fee income and your impermanent loss by roughly the same capital-efficiency factor. The position is mathematically identical to a v2 LP, but operating on a small slice of capital, so per-dollar everything is amplified ~8× in our example. There is no free lunch hiding in v3 — only a sharper bet. A tight range is a high-conviction wager that price will stay put and keep trading, and it pays beautifully if you're right and bleeds if you're wrong.
Liquidity provision becomes an active strategy
Because a position only earns inside its band and must be re-minted to move, v3 turns the once-passive act of LPing into portfolio management. The central choice is range width, and it's a genuine trade-off:
- Tight range → maximum capital efficiency and fee income, but the price escapes quickly, you stop earning, and you face the most rebalancing (each re-mint costs gas and realizes IL). It only wins on stable or range-bound pairs.
- Wide range → fewer fees per dollar but you stay in range through bigger moves and rebalance rarely; in the limit [0, ∞] it is plain v2. Lower maintenance, lower reward.
- Fee-tier choice matters too: stable pairs run the 0.01–0.05% tiers with ultra-tight ticks (the stableswap intuition, now achievable with a narrow v3 band), while volatile pairs use 0.30% to be paid more for carrying more IL.
An ecosystem grew up to automate this. Active liquidity managers (Gamma, Arrakis, and the like) wrap a v3 position in a vault that rebalances and compounds for you. At the predatory end sits just-in-time (JIT) liquidity: a searcher spots a large pending swap in the mempool, mints an ultra-tight position one block early to soak up nearly all of that swap's fee, then burns it the same block — a form of MEV that the next guide explores. And Uniswap v4 generalizes all of this with hooks, letting developers run custom logic at swap and position events. The takeaway: v3 didn't just make AMMs more efficient — it made LPing a skill.
The trade-off in one line
v3 lets you decide where on the price curve your money lives. Concentrate it and the same capital quotes far more depth and harvests far more fees — but only inside the band, and with impermanent loss amplified by the very same factor. Capital efficiency and risk are two readings of one dial. You turn it; the AMM does exactly what the math says.