Understanding Uniswap V3 - Part III
Motivation
This section aims to explain what happens to liquidity after it is deposited by an LP, and how that liquidity gets activated and used in a swap.
Ticks
In traditional finance, there is an idea of a tick size, which is the smallest amount that a price of a stock can move in either direction. For most stocks, the granularity of a tick is \$0.01. In Uniswap V3, a similar approach is used. Instead of using \$0.01 as a tick, V3 uses a basis point (0.01%) as a tick.
Each tick is a int24 value order from $-2^{23}$ to $2^{23}-1$, where each tick moving from left to right represents a price that is 0.01% greater
than the price at the previous tick.
So, if you wanted to increase the number 5 by 0.01%, you would do:
$$ 5 \cdot 1.0001 = 5.0005 $$
To increase this value again by 0.01%, you do:
$$ 5 \cdot 1.0001 \cdot 1.0001 = 5.00100005 $$
Eventually, you can arrive at any price with a chain of 0.01% multiplications:
$$ \underbrace{1.0001 \cdot 1.0001 \cdot 1.0001 \cdot \ldots \cdot 1.0001 \cdot 1.0001 \cdot 1.0001}_{i \text{ times}} = 1.0001^i $$
The amount of times, $i$, that you multiply by $1.0001$ can be turned into an exponent, and this is the function that is used to calculate a price from a corresponding tick. The value $i$ is the index of the tick representing a price with an increase of 0.01%, $i$ times:
$$ P(i) = 1.0001^i $$
For example, the tick $100$ corresponds to the price $1.0001^{100}$ which equals $1.01004966$, and the tick $2^{18}$ corresponds to the price $1.0001^{2^{18}}$ which equals $242,214,459,604.341$.
It’s important to note, however, that not all pools use the full tick granularity. A pool with a 0.3% fee tier uses every 60th tick, while a pool with a 1% fee tier uses every 200th tick.
The next step is converting a known price of the pool into a tick. If a user wants to add liquidity to a range between \$500 and \$1000 on an ETH/DAI pool, Uniswap V3 needs to interpret \$500 and \$1000 as ticks, not as prices.
We can take the inverse of the exponential function $P(i) = 1.0001^i$ and convert it into its logarithmic form to solve for $i$ instead of $P(i)$:
$$ i = \log_{1.0001}(P) $$
And now we have equations to go from tick to price, and from price to tick.
There are a few caveats to this, though. $i = \log_{1.0001}(P)$ doesn’t always return integers, and a tick is always said to be an integer. To fix this, we take the floor value: $i = \lfloor\log_{1.0001}(P) \rfloor$
This means that when a given price is between two tick integers, the associated tick for the price is the one to the left of the price.
As we know, Uniswap V3 pools store $\sqrt{P}$ in the global state and not $P$ itself, so the implementation in the code looks a bit different, but it functions the same:
$$ \sqrt{P(i)} = \sqrt{1.0001^i} = 1.0001^{\frac{1}{2} \cdot i} = 1.0001^{\frac{i}{2}} $$
$$ i = \lfloor 2 \cdot \log_{1.0001}(\sqrt{P}) \rfloor $$
Tick State
We already know each pool stores $\sqrt{P}$ and $L$, and each pool also stores the current tick indirectly from $\sqrt{P}$. As a reminder, the tick of the pool remains the same until the price gains a sufficient amount to cross the next initialized tick to the right.
Each tick holds two values about the liquidity stored at that tick, liquidityNet and liquidityGross:
liquidityNet: The gain or loss of liquidity when the price crosses this tick from left to right. Can be positive or negative.
liquidityGross: The total liquidity pointing at a tick that is used as a lower or upper bound. This value is used to determine if a tick is initialized, since
liquidityNetcould be 0 but still have liquidity referencing it. Can only be positive.
Storing Liquidity in a Pool
When a user provides liquidity, this liquidity is considered to be the liquidityDelta. This value is added to the liquidityNet value of the
lower tick and subtracted from the liquidityNet value of the upper tick. Why do this? When moving left to right in price, by crossing the lower
tick, we’ve activated liquidity at the lower tick which sustains until the price crosses the upper tick where the liquidity is deactivated.
Each time a tick is crossed, the global available liquidity is updated by the crossed tick’s liquidityNet value, which can be positive or
negative. If the crossed tick has a liquidityGross of 0, it’s considered uninitialized and is skipped.
One caveat here is that if the liquidityDelta is added to a range that the price is currently in, then the liquidityDelta is also directly
added to the current global liquidity value $L$. This is because the price never had the chance to cross the lower tick where the liquidity became
active.
Tick Range Examples
In this scenario, we can say:
$A$ provides $\Delta L$ of 5 to ticks $(0, 2)$
$B$ provides $\Delta L$ of 10 to ticks $(1, 4)$
$C$ provides $\Delta L$ of 20 to ticks $(2, 3)$
$D$ provides $\Delta L$ of 15 to ticks $(3, 6)$

Lets assume the current price of the pool corresponds to tick 0 which has liquidity of 5 and can be considered the current global liquidity.
When the price crosses from tick 0 to tick 1, we check the liquidityNet value of tick 1 and see that it has a positive value of 10. We then
add 10 to the current global liquidity to get a global $L$ value of 15 at tick 1, which can be used in swaps between tick 1 and tick 2.
If the price were to cross tick 2, we can see that user $A$’s liquidity is no longer active, which results in subtracting 5 from tick 2’s
liquidityNet. But, user $C$ provides 20 units of liquidity at tick 2, so the total liquidityNet entering tick 2 is 15. This value is then
added to the current global liquidity to get a total of 30 units of liquidity for swaps anywhere between tick 2 and 3.
In summary, each tick uses the liquidityNet value to track whether crossing the tick results in a net gain or loss compared to the current
global liquidity. This net value is added to the global liquidity and becomes the new $L$ value in the swap function for all swaps between the
current tick and the next initialized tick to the right.
Performing a V3 Swap
With an intuition for how liquidity is used, understanding a swap becomes straightforward. A swap begins inside a while loop and only cuts out when the tokens remaining to be swapped equals 0 or when a price limit has been reached due to slippage.
The pool is aware of the current tick and current price, and then fetches the next tick and its associated price.
A swap is then performed using the concentrated liquidity function:
- Current tick price is $\sqrt{P_a}$, next tick price is $\sqrt{P_b}$
- $L$ is the current global liquidity
- $\Delta x$ are the amount of tokens to be swapped
If no tokens remain to be swapped, then the swap ends. If the current price hit the upper bound price $\sqrt{P_b}$, it means liquidity was exhausted and the next tick has to be crossed to gain more liquidity to swap the remaining tokens.
Once the tick is crossed, the liuquidityNet of the tick is added to the current global liquidity and we set the current tick to be the
tick that was just crossed into.
This process is repeated until all tokens are swapped or if the price gets too high due to slippage.