Quietbroom Blog

Understanding Uniswap V3 - Part I

This series will be an attempt to provide the perfect mix between understanding the math behind V3 and also not getting lost in the details so you can still get a high-level grasp of how the protocol works. I am by no means an expert on Uniswap V3, but hopefully by the end of it you’ll know a little bit more about the juicy bits that make Uniswap V3 so different from V2.

Motivation

After the release of Uniswap V2, researchers realized the lack of capital efficiency in some of the liquidity pools, especially in pools that didn’t fluctuate much in price, such as a DAI/USDC pair.

To illustrate the capital inefficiency, here is an example of a V2 pool between two stablecoins where the price consistently fluctuates between 0.990.99 and 1.011.01.

Using the standard xy=kx \cdot y = k curve from V2, we can say:

x=1,000,000x = 1,000,000

y=1,000,000y = 1,000,000

k=1,000,0001,000,000=1012k = 1,000,000 \cdot 1,000,000 = 10^{12}

In this example, the current price of the pool is yx=1,000,0001,000,000=1\displaystyle\frac{y}{x} = \displaystyle\frac{1,000,000}{1,000,000} = 1. This is the price of one xx token denominated in yy tokens.

To see the Δx\Delta x (or, change in xx) that is needed to be added to the pool to push the price down to 0.990.99, we start by assuming the price is already at xy=0.99\displaystyle\frac{x}{y} = 0.99 and that k=1012k = 10^{12}. Then, new xx and yy values can be derived from here.


Isolate yx\displaystyle\frac{y}{x} and kk to one side:

xy=ky=kxyy=kyxx \cdot y = k \longrightarrow y = \frac{k}{x} \longrightarrow y \cdot y = k \cdot \frac{y}{x}

Plug in values to solve for yy:

y2=0.991012y=0.991012y=994,987.44y^{2} = 0.99 \cdot 10^{12} \longrightarrow y = \sqrt{0.99 \cdot 10^{12}} \longrightarrow y = 994,987.44

Solve for xx:

x994,987.44=1012x=1,005,037.81x \cdot 994,987.44 = 10^{12} \longrightarrow x = 1,005,037.81

The result is that an incoming Δx\Delta{x} of 5,037.815,037.81 tokens to the pool and an outgoing Δy\Delta{y} of 5,012.56-5,012.56 tokens from the pool are needed to drop the pool’s price from 11 to 0.990.99. Meaning, only 5012.565012.56 of the pool’s 1,000,0001,000,000 yy tokens were needed by the pool to cover a price drop to 0.990.99, which is only ~0.5%0.5\% (5,012.561,000,000)(\frac{5,012.56}{1,000,000}) of the yy tokens.

99.5%99.5\% of the tokens were unnecessary…

Fixing capital inefficiency

If 1,000,0001,000,000 yy tokens are in the pool, it would be better that all 1,000,0001,000,000 yy tokens be swapped out before the price drops to 0.990.99. Similarly, it would be better that all 1,000,0001,000,000 xx tokens be swapped out before the price rises to 1.011.01.

To enable this behavior, two new features are needed: the ability for LPs (liquidity providers) to deposit liquidity between any two prices they see fit, and that the AMM (automated market maker) offers a xy=kx \cdot y = k pricing curve between any two prices where an LP has deposited their liquidity.

With the addition of these features, a central limit order book is introduced into the AMM. Rather than having traditional market makers create single-price limit orders, LPs will create ranged-price limit orders. And rather than the price skipping up and down due to bid/ask spreads, prices instead smooth out using a xy=kx \cdot y = k curve between the lower and upper price of a ranged-price limit order. It does, however, require active management of LP positions, which is something that was not necessary in uniswap V2.

So, how does V2’s xy=kx \cdot y = k curve evolve to support the addition of these new features in V3?

A new kind of curve

Specifically, the goal for V3 is to have a xy=kx \cdot y = k curve where two prices, PaP_{a} and PbP_{b}, are specified as lower and upper bounds for the curve.

With this new curve, when yy tokens are completely exhausted from the pool, the price will be PaP_{a}. When xx tokens are completely exhausted from the pool, the price will be PbP_{b}.


In a V2 stablecoin pair, prices tend to hover between 0.990.99 and 1.011.01, but it allowed for the possibility of any price between 00 and \infty to be quoted because the curve is unbounded.

image info


In V3, the curve forces trades to take place between prices PaP_{a} and PbP_{b}.

image info

V3 uses the original xy=kx \cdot y = k function as a base to create a new bounded-curve function:

(x+LPb)(y+LPa)=L2(x + \frac{L}{\sqrt{P_{b}}})(y + L \cdot \sqrt{P_{a}}) = L^{2}

The following sections will describe how this new function is derived, which is introduced in the whitepaper.

Breaking down the x ⋅ y = k curve from V2

image info

In V2, all xx reserves would have to be depleted from the pool to reach the highest price on the curve, which would approach infinity. But in V3, we say the upper price bound is just PbP_{b}.

So if PP is the current price, then the pool only has to reduce by xrealx_{real} amount of xx tokens to reach the price PbP_{b}. And at that price PbP_{b}, xx' is the amount of xx tokens remaining in the pool.

But because we decided PbP_{b} would be the absolute max price for this pool, those remaining xx' tokens are not needed. Those xx' tokens would only be used to trade between prices PbP_{b} and \infty, so we have no use for them in this new pool design.

In V3, those xx' reserves are considered “virtual” and do not need to be provided by LPs. The xx' number only exists in the math so that the xy=kx \cdot y = k curve can hold, where x=xreal+xx = x_{real} + x'.

The same can be said in the opposite direction for PaP_{a}, yrealy_{real}, and yy'.


To recap:

xx': number of xx tokens in the pool that would make the price PbP_{b}

yy': number of yy tokens in the pool that would make the price PaP_{a}

xrealx_{real}: amount of xx tokens in the pool that, when removed, would move the price from PP to PbP_{b}

yrealy_{real}: amount of yy tokens in the pool that, when removed, would move the price from PP to PaP_{a}

Looking at the chart, we can see that the xx' and xrealx_{real} line segments add up to xx. This means that LPs only contribute xrealx_{real}, and when combined with the virtual xx' number, it adds up to the full xx value that is used in xy=kx \cdot y = k.

As another abstraction, V3 swaps out the kk value for L2L^{2} (this will be explained later). Instead of the function xy=kx \cdot y = k, we insert our modifications and get:

(xreal+x)(yreal+y)=L2(x_{real} + x')(y_{real} + y') = L^{2}

Calculating x’ and y’

To complete the V3 curve derivation, the only remaining task is to calculate xx' and yy'. To do that, we must look at our new curve as xy=L2x \cdot y = L^{2} where we already know x=xreal+xx = x_{real} + x' and y=yreal+yy = y_{real} + y'.

V3 does not store global xx and yy values like it did in V2. Instead, V3 elects to store a global LL and P\sqrt{P} value for each pool. The reasoning that Uniswap offers for this is that at any one time, only one of the values of LL or P\sqrt{P} can change, which helps prevent rounding errors. Price (P\sqrt{P}) only changes with a swap between ticks, and LL changes when crossing ticks.

Using xy=L2x \cdot y = L^{2} and P=yxP = \displaystyle\frac{y}{x}, values for xx and yy can be calculated purely in terms of LL and P\sqrt{P}.


Solving for xx:

xy=L2x=L2yx \cdot y = L^{2} \longrightarrow x = \frac{L^{2}}{y}

we know that y=Pxy = P \cdot x, so substitute it in:

x=L2Pxx = \frac{L^{2}}{P \cdot x}

Put xx on one side:

x2=L2Px^{2} = \frac{L^{2}}{P}

Take square root:

x=LPx = \frac{L}{\sqrt{P}}

Solving for yy:

xy=L2y=L2xx \cdot y = L^{2} \longrightarrow y = \frac{L^{2}}{x}

we know that x=yPx = \displaystyle\frac{y}{P}, so substitute it in:

y=L2yP=L2Py=L2Pyy = \frac{L^{2}}{\frac{y}{P}} = L^{2} \cdot \frac{P}{y} = \frac{L^{2} \cdot P}{y}

Put yy on one side:

y2=L2Py^{2} = L^{2} \cdot P

Take square root:

y=LPy = L \cdot \sqrt{P}

By storing LL and P\sqrt{P} globally, it makes it trivially easy to calculate xx and yy. If L2L^{2} and PP were stored instead, each calculation for xx and yy would have a square root calculation added to it, which hampers efficiency with a smart contract implementation.


We now know how to calculate xx and yy but we still don’t know the exact ratio of xrealx_{real} to xx' or yrealy_{real} to yy' yet.

Looking at the price curve diagram, we can see that at price PbP_{b}, there are zero xrealx_{real} reserves and only the virtual xx' reserves remaining. Similarly, at price PaP_{a}, there are zero yrealy_{real} reserves and only the virtual yy' reserves remaining. Using this insight, we can finally calculate a value for xx' and yy'.

We know three things about xx and yy:

1. x=LPx = \displaystyle\frac{L}{\sqrt{P}} and y=LPy = L \cdot \sqrt{P}

2. x=xreal+xx = x_{real} + x' for some price PP, y=yreal+yy = y_{real} + y' for some price PP

3a. At price PbP_{b}, xrealx_{real} reserves are zero, so x=xreal+x=0+x=xx = x_{real} + x' = 0 + x' = x'

3b. At price PaP_{a}, yrealy_{real} reserves are zero, so y=yreal+y=0+y=yy = y_{real} + y' = 0 + y' = y'

Putting this together, we can see that for the two specific prices, PbP_{b} and PaP_{a}:

x=LPbx' = \frac{L}{\sqrt{P_{b}}}
y=LPay' = L \cdot \sqrt{P_{a}}

Conclusion

Knowing how xx' and yy' are calculated, we plug them into our function:

(xreal+LPb)(yreal+LPa)=L2(x_{real} + \frac{L}{\sqrt{P_{b}}})(y_{real} + L \cdot \sqrt{P_{a}}) = L^{2}

And now we finally reach equation 2.2 from the Uniswap V3 whitepaper.


Alec DiFederico

Hey there, I'm Alec.

This is my portfolio/blog where you can fing things I find interesting or some projects I've built.

Enjoy your stay, and feel free to check out my github while you're here.