Uniswap V3 Concentrated Liquidity Explained
Quick answer. Uniswap V3 concentrated liquidity lets LPs supply liquidity only between two chosen ticks, instead of across the whole x × y = k curve. The same capital earns more fees, and swappers get less price impact.
Uniswap V3 is one of the most notoriously difficult DeFi protocols to understand. Its V2 model, the constant product AMM, is easy to follow; range-bound liquidity and ticks are not.
This guide explains why Uniswap V3 introduced concentrated liquidity, how it is implemented, and how it works when you create a position and when you swap.
It assumes you already understand the constant product AMM and Uniswap V2. If you do not, start with our guide to the constant product AMM.
Why Range-Bound Liquidity Exists
Range-bound liquidity benefits traders, liquidity providers and Uniswap as a protocol.
1. LPs earn more fees on their capital
In Uniswap V2, liquidity providers had to provide liquidity across the entire x × y = k curve. They had to support every price on it.
In Uniswap V3, an LP picks a specific price range they expect the tokens to trade in, and provides liquidity only there.
2. The same fees need less capital
To capture the trading within that range, an LP only needs enough liquidity for that range. They no longer need that amount across the whole curve just to catch swaps in one part of it.
The net result: the same fees on a much smaller amount of capital.
3. The whole exchange becomes more efficient
Because LPs concentrate their capital on the range where price actually trades, more liquidity is available to swappers.
Swappers suffer less price impact and get a better price. That draws more volume to the exchange and lets it capture more value in fees.
Ticks: How It Is Implemented
To understand the implementation, you first need ticks.
Ticks are tick marks, for lack of a better term, that divide the price curve into discrete prices. You select where your position begins and ends from among them.
How the Ticks Are Defined
Every tick follows one formula. A tick is the power you raise 1.0001, one plus one basis point, to get a price.
price = 1.0001 ^ tick.
For any price there is a tick value that produces it. Neighboring ticks sit exactly one basis point apart: each tick's price is the previous tick's price multiplied by 1.0001.
Worked example: a tick to an ETH price
Take the Uniswap V3 USDC and ether pool on Etherscan. In this pool, token0 is USDC.
At the time of recording (April 2023), the pool's latest tick was 200,787. Here is how to turn that into an ether price.
- Compute 1.0001 raised to 200,787. This is the price of one USDC in terms of ether, in raw units.
- Divide by 1e18, since the result is an ether value.
- Invert it, to get the price of ether in terms of USDC.
- Divide by 1e6, because USDC has 6 decimals.
The result was an ether price of about $1,900, which matched CoinMarketCap at the time.
Getting the Price From sqrtPriceX96
The pool also stores sqrtPriceX96. This is the actual current price of the pool, which usually sits somewhere between two ticks rather than exactly on one.
The name tells you how to decode it. Call the value s. It is the square root of the price, bit shifted left by 96 (multiplied by 296).
s = sqrt(price) * 2^96.
This gives the price of token0 in terms of token1, here USDC in ether. Decode it in three steps.
- Divide s by 296 to undo the bit shift.
- Square the result to undo the square root.
- Divide by 1e18, invert, then divide by 1e6, exactly as with the tick.
In the example, this gave an ether price of $1,906.9. It differs slightly from the tick price because it is an intermediate price, not the price of a discrete tick.
How Liquidity Works in Tick Ranges
Say you create a position with a lower tick and an upper tick. Your liquidity is split across every tick range in between, with the same liquidity amount in each tick bucket.
When swappers move the pool price into your range, your liquidity becomes active. For each tick the price moves inside your position, your liquidity gets used in that tick.
When the price crosses out of your range, your liquidity is deactivated and subtracted from the active liquidity.
As the price moves through your range, your position changes composition. Put token0 on the x-axis and token1 on the y-axis.
- At the start of your range, the position is entirely token0.
- As swappers use up your token0, you trade into token1.
- At the end of your range, the position is entirely token1.
The Real Reserves
You can visualize this by shifting the curve to the left, so it shows only the liquidity for your position's range.
On that shifted curve, one end has only x (y is zero) and the other end has only y (x is zero). This shifted curve is known as the real reserves.
Its formula is reminiscent of the classic x × y = k, with two modifications.
(x + L / √pb) × (y + L × √pa) = L².
- L is the square root of k. It measures the liquidity provided on the curve.
- pa is the price where the position begins, and pb is where it ends.
Stretch pa down to zero and pb up to infinity. L times zero is zero, L divided by infinity is zero, and the formula collapses to x × y = k.
So the formula takes x × y = k and makes it specific to the price range you provide. That is how the liquidity of a position over a discrete range, from pa to pb, is calculated.
Makeshift Limit Orders
One more detail: you can create something like a limit order on Uniswap V3.
Create a position that spans a single tick range. As the pool trades through that range, your entire position swaps from token0 into token1.
If the price keeps going, your makeshift limit order is filled.
But if the pool trades back the other way, your liquidity can be reactivated and trade back. That reverses your limit order, which is not ideal for a limit order.
At the time of recording, protocols such as Poolshark were tackling this by offering onchain limit orders that only trade one way.
How Swaps Use the Liquidity
Creating your position initializes two ticks, call them tick A and tick B. Your liquidity covers the range between them.
If no other ticks are initialized in that range, and no other positions overlap yours, the pool can treat the range as a normal constant product AMM. No liquidity needs to be kicked in or out halfway through.
Now say tick C, the end tick of somebody else's position, sits partway along the route. A swap from tick A to a price X beyond tick C happens in pieces.
- Do a classic swap from tick A to tick C, and calculate the amounts in and out for that piece.
- Cross tick C, and kick out that other position's liquidity.
- Compute the rest of the swap, from tick C to the resulting price X.
That is how the V3 codebase implements it. A while loop looks for the next initialized tick and processes the amounts in and out for the range up to it.
If the next range has enough liquidity to fill the rest of the swap, the swap simply ends at whatever the resulting pool price is.
For more DeFi internals, see how MakerDAO and DAI work and GMX V2 explained.
How Guardian Helps
Guardian has reviewed concentrated liquidity AMMs, including Poolshark, whose directional AMM enables onchain spot, limit and stop-loss orders.
A smart contract audit pairs invariant suites around accounting, rounding and math formulas with senior researchers who attack protocol logic and integrations.
Frequently Asked Questions
What is a tick in Uniswap V3?
A tick is one of the discrete prices that divide the price curve. Its price is 1.0001 raised to the tick, so neighbors sit one basis point apart. LPs pick a lower and an upper tick.
How do you get a price from sqrtPriceX96?
Divide sqrtPriceX96 by 2 to the 96th, then square it. That gives the price of token0 in terms of token1, in raw units. Adjust for each token's decimals, and invert it if you want the other direction.
Can you place a limit order on Uniswap V3?
Roughly. A position spanning a single tick range swaps entirely from one token into the other as price crosses it. If price comes back, though, the liquidity can reactivate and trade back, which reverses the order.
What happens when a swap crosses an initialized tick?
The swap is computed piece by piece. It swaps up to the next initialized tick, crosses it and adds or removes that position's liquidity, then continues. A while loop repeats this until the swap is filled.
Sources
Get your AMM math reviewed by Guardian
Guardian builds invariant suites around your pool's accounting and rounding, then attacks what they leave standing.