Uniswap v3 turns out to be very different from Uniswap v2. Although Uniswap v3 pools also have two tokens and use a constant product formula, Uniswap v3 introduces a new concept that is called concentrated liquidity. The main idea of this feature is that liquidity providers can provide liquidity in a chosen price range and implies that the reserves of each position are just enough to support trading within its range. When the price goes out of that range, the position of the liquidity provider is swapped entirely into one of the two tokens, depending on the price going above or below the range.
A consequence of concentrated liquidity is that the positions are highly personalized, since liquidity providers can choose not only the amount to deposit but also the range in which they want to provide liquidity. This implies that the positions in a Uniswap v3 pool are naturally nonfungible. Therefore, liquidity providers must be given nonfungible tokens in exchange for their deposit, and these nonfungible LP tokens will have to keep a record of the details of their particular position. In addition, due to the customizable liquidity provision feature, fees must now be collected and stored separately as individual tokens rather than being automatically reinvested as liquidity into the pool.
In this chapter, we will present a thorough exposition of the Uniswap v3 AMM, together with a comprehensive analysis of liquidity provisioning in this protocol. We will also explain in detail how this AMM is actually implemented.
5.1 Ticks
As we mentioned before, in Uniswap v3, liquidity providers provide liquidity in a chosen bounded price range. However, the lower and upper limits of this range cannot be defined arbitrarily but rather can be chosen from a finite (but very big!) subset of the set of real positive numbers. The elements of this subset are called ticks and are indexed by integer numbers in the following way: i ∈ ℤ represents the tick (and hence the price) p(i) = 1.0001i. We will also say that the tick index for the tick p(i) is i.
which cover almost all possible prices in the asset space.
where for any real number x, ⌊x⌋ denotes the floor of x, that is, the greatest integer that is less than or equal to x.
Tokens | DAI | ETH |
Balances | 40,000 | 10 |
Tick index | 82,164 | 82,165 | 83,667 | 83,668 |
Tick (approx.) | 3,699.634 | 3,700.004 | 4,299.619 | 4,300.049 |
Hence, the liquidity provider can choose to provide liquidity in the price interval [3,700.004, 4,300.049].
5.1.1 Initialized Ticks
If a tick is used in a new position and that tick has not been initialized yet, then the tick is initialized. If a tick is a boundary of only one position, when that position is removed, the tick is uninitialized. In order to keep track of initialized and uninitialized ticks, Uniswap v3 introduces a tick bitmap, where each bitmap position corresponds to a tick index. If the tick is initialized, the value of the bitmap position that corresponds to that tick is 1, and if the tick is uninitialized, the value of the bitmap position that corresponds to that tick is 0.
5.1.2 Tick Spacing
In Example 5.1, we did not impose any restrictions on the tick indexes, and hence, any 24-bit signed integer could be a tick index. In general, Uniswap v3 does not allow an arbitrary choice of the tick indexes but rather introduces the concept of tick spacing, which, in informal terms, is a measure of the separation between the allowed tick indexes. Concretely, only tick indexes that are multiples of the tick spacing are permitted. For example, if the tick spacing is 5, the only tick indexes that can be used are the multiples of 5: …, –10, –5, 0, 5, 10,….
Tick index | 82,160 | 82,165 | 83,665 | 83,670 |
Tick (approx.) | 3,698.155 | 3,700.004 | 4,298.759 | 4,300.909 |
The tick spacing parameter is defined and fixed when the pool is created. Clearly, if the tick spacing is small, then liquidity providers can choose more precise ranges, but on the other hand, a small tick spacing may cause trading to be more expensive in terms of gas fees, since every time the price crosses an initialized tick, new values for certain variables need to be set, which imposes a gas cost on the trader.
5.2 Liquidity Providers’ Position
Consider a Uniswap v3 liquidity pool with tokens X and Y. From now on, when we speak about the price, we will be referring to the price of token X in terms of token Y. Suppose that a liquidity provider provides liquidity in a price range [pa, pb]. The main idea of Uniswap v3 is that the liquidity provider’s position will use its assets to allow trading between the prices pa and pb. When the price goes out of this range, the position’s assets are not used for trading, and hence, the position’s balances remain unmodified until the price enters the interval [pa, pb] again. In addition, if the price falls below pa, then the liquidity provider’s position will be fully converted to token X, and if the price goes above pb, then the liquidity provider’s position will be fully converted to token Y. For example, in this last case, we can think that the whole amount of token X of the position has been sold as the price of token X increased, and thus, the position only has token Y left. Note that in both cases, the liquidity provider is left with the asset that is less valuable.
In order to find the parameters of the translation that we need to apply, we will compute the balances of the tokens at the pool states a and b. Let xa and ya be the balances at state a of tokens X and Y, respectively. Clearly, xaya = L2, and since the spot price at state a is pa, we obtain that Hence, and Thus, . In a similar way, we obtain that .
which defines the curve of real balances given in Figure 5-2.
Virtual reserves. We will now introduce the concept of virtual reserves, which will be needed later. With the previous notations, the virtual reserves (or virtual balances) of tokens X and Y are the positive real numbers xv and yv that satisfy the constant product formula xvyv = L2 and the spot price formula where p is the spot price of the pool at a certain moment. This means that the virtual reserves define a virtual pool state (xv, yv) that is located on the curve of virtual balances of Figure 5-2. We can think that whenever the price belongs to the range [pa, pb], the trades in the pool are carried out following the constant product formula xvyv = L2, although the real reserves of the pool—the actual balances of the tokens—do not coincide with the virtual reserves.
are equivalent.
If p = pa, the balance y of token Y is equal to zero, and hence, isolating x from the previous equation, we obtain that the balance of token X is . Thus, the state of the liquidity provider’s position when p = pa is , which are the coordinates of the point c of thecurve of real balances of Figure 5-2 that lies on the horizontal axis. These coordinates could also have been obtained by translating the point a by the vector (−xb, −ya), which was the vector used for translating the curve of virtual balances of Figure 5-2 to the curve of real balances.
In a similar way, if p = pb, the balance x of token X is equal to zero, and isolating y from the previous equation, we obtain that the balance of token Y is . In addition, using Figure 5-2, a geometrical interpretation (similar to the previous one) can be made.
Note that in the particular cases p = pa and p = pb, we obtain the results of the previous paragraphs again.
Formulae for the real balances of a Uniswap v3 position
Price range | Real balance of token X | Real balance of token Y |
---|---|---|
p ≤ pa | 0 | |
pa ≤ p ≤ pb | ||
p ≥ pb | 0 |
Opening a position. Suppose that a liquidity provider wants to provide liquidity in the price range [pa, pb]. Using the notations of this section, if the parameter L is chosen, then the curve of virtual balances of Figure 5-2 is fixed, and thus, the curve of real balances is uniquely determined. Hence, by the previous results, given a price p, the real balances of the liquidity provider’s position are determined. It is important to observe that since the trading fees of Uniswap v3 are stored separately, the parameter L will remain constant, the curves of Figure 5-2 will not change, and the state of the position will always be a point of the curve of real balances.
In consequence, if the liquidity provider wants to start a position with those characteristics−pa, pb and L chosen—given the price p, they need to deposit the amounts of tokens X and Y given by Table 5-1 so that the state of their position belongs to the curve of real balances. Concretely, we need to consider the following three possible situations:
(A) If the current price p is below the price range [pa, pb] — that is, if p < pa — the liquidity provider will have to deposit only token X, and the amount of token X they need to deposit is .
(B) If the current price p is within the price range [pa, pb]−that is, if pa ≤ p ≤ pb−the liquidity provider will need to deposit certain amounts of both tokens. Specifically, they will need to deposit an amount of token X and an amount of token Y.
(C) If the current price p is above the price range [pa, pb]—that is, if p > pb—the liquidity provider will have to deposit only token Y, and the amount of token Y they need to deposit is
Of course, in practice, a liquidity provider will not want to choose the parameter L, but instead, they will choose a price range [pa, pb] and a certain amount of either token X or token Y to deposit. In that case, we can simply use that amount and the formulae of Table 5-1 to find out the value of the parameter L, and then use this value to compute the amount of the other token that the liquidity provider needs to deposit. We show how this works in the following example.
Example 5.2. Suppose that a liquidity provider wants to provide liquidity in a Uniswap v3 pool with tokens ETH and USDC. Suppose, in addition, that the price of ETH in terms of USDC is 4,000 and that the liquidity provider wants to deposit liquidity in the price range (3,700.004, 4,300.049) as in Example 5.1, and that the liquidity provider wants to deposit 2 ETH.
Therefore, the liquidity provider will have to make a deposit of 2 ETH and 8,610.458 USDC to set up their position.
If p ≤ pa, then
If pa ≤ p ≤ pb, then
If p ≥ pb, then
which is greater than V (4,200). Thus, the liquidity provider is facing an impermanent loss. We will analyze impermanent losses in Uniswap v3 in the following section.
5.3 Impermanent Loss
In the previous section, we studied and computed the value of a liquidity provider’s position in terms of the price p. We will now study the possible impermanent losses that may occur, generalizing what was shown in Example 5.3.
As in the previous section, consider a Uniswap v3 liquidity pool with tokens X and Y and a liquidity provider that deposits liquidity into the pool in a range [pa, pb]. Let p0 be the entry price (i.e., the price at which the liquidity deposit is made); let x0 and y0 be the amounts of tokens X and Y, respectively, that the liquidity provider deposits; and let L be the corresponding liquidity parameter. Let p be a positive real number, and let xp and yp be the real reserves of tokens X and Y, respectively, that correspond to the liquidity provider’s position when the price of token X in terms of token Y is p. Let V (p) be the value of the liquidity provider’s position (in terms of token Y) and let W (p) be the current joint value of the amounts x0 and y0 of tokens X and Y, respectively (in terms of token Y), that is the value of the liquidity provider’s position if they had not deposited it into the pool. Note that V(p) = xp ⋅ p + yp and W(p) = x0 ⋅ p + y0.
We will divide our analysis into three cases with three subcases each.
Case 1: p0 ∈ [pa, pb]
If p ≤ pa, yp = 0, and from Table 5-1, we have that
If p ≥ pb, xp = 0, and from Table 5-1, we know that
Case 2: p0 ≤ pa.
If p ∈ [pa, pb], from Table 5-1, we have that
If p ≥ pb, xp = 0, and from Table 5-1, we know that
Case 3: p0 ≥ pb.
If p ∈ [pa, pb], from Table 5-1, we have that
If p ≥ pb, xp = 0, and from Table 5-1, we know that
Again, note that there is no impermanent loss in this case since the deposited position coincides with the real reserves.
We need to mention that in this case, the comparison between Uniswap v3 and v2 is tricky when token Y is a stablecoin, and thus, we will give a more detailed analysis. Consider liquidity pools with ETH and USDC and suppose that the entry price is above the chosen interval. In a Uniswap v3 pool, the liquidity provider would have to deposit only USDC, but in a Uniswap v2 pool, they would have to swap half of that USDC into ETH before making the deposit. Hence, if the price of ETH in terms of USDC goes up, the position of the Uniswap v3 pool will not change—it will have the same amount of USDC and 0 ETH—while the position of the Uniswap v2 pool will have less ETH than at the beginning and more USDC, which amounts to more than half of USDC and less than half of ETH (with respect to the initial value). Since the price of ETH increased, this gives an impermanent loss with respect to the position that has half of USDC and half of ETH, but nevertheless, this position would have more value than the Uniswap v3 position that consisted of simply holding USDC.
5.4 Multiple Positions
In this section, we will explain how multiple different positions are combined in Uniswap v3. Concretely, we will show that the liquidity parameter at a price p that is not an initialized tick is the sum of the liquidity parameters of all the positions whose price range contains p. Observe that the liquidity parameter at an initialized tick cannot be defined in a reasonable way since the liquidity parameters of a Uniswap v3 pool at the right and at the left of an initialized tick might be different.
Then the liquidity parameter of the Uniswap v3 pool at the price p0 is .
Proof. Observe that if A = ∅, then p0 does not belong to any of the price ranges of the positions of the pool, and hence, the liquidity parameter of the pool at a price p0 is 0, which coincides with , since the last one is a sum without summands.
Therefore, we may assume that A ≠ ∅. Note that if j ∉ A, then position j does not provide liquidity at the price p0, and thus, we do not need to consider position j.
for all j ∈ A and for all p ∈ I.
Therefore, when the price p belongs to the interval I, trading in the Uniswap v3 pool that we are considering is equivalent to trading in a constant product AMM with liquidity parameter . The result follows since the liquidity parameter is valid within the interval I, hence, in particular, at price p0 as p0 ∈ I.□
5.5 Protocol Implementation
In Uniswap v2, a trading fee is charged on the incoming token for each trade that is made. The collected fees stay in the pool, causing the liquidity parameter L to increase. And liquidity providers collect their share of fees when they remove liquidity.
In Uniswap v3, things are completely different. Fees are still charged on the incoming token, but they are stored separately and kept outside the pool as the individual tokens in which they are paid. This means that fees are not reinvested as in Uniswap v2. In addition, liquidity providers earn a portion of the trading fees only when the price moves within the range in which they provide liquidity. In order to allow this, the smart contract of Uniswap v3 needs to keep track of many variables.
In this section, we will explain how the Uniswap v3 protocol is actually implemented. To this end, we will analyze several concepts since the implementation of the Uniswap v3 protocol is much more complex than those of the AMMs that we have studied in the previous chapters.
5.5.1 Variables
Variables used in the smart contract of Uniswap v3
Variable | Notation | Type |
---|---|---|
Current tick index | ic | |
Square root of the current price | ||
Total liquidity | Ltot | |
Net liquidity | ΔL(i) | (T) |
Gross liquidity | Lg(i) | (T) |
Total of collected fees | ||
Fees collected from "outside" | (T) |
Square root of the current price and total liquidity. Instead of tracking the pool balances as in Uniswap v2, the smart contract of Uniswap v3 tracks the square root of the price and the total liquidity Ltot at that specific price. As we have seen in Section 5.2, the values of the virtual reserves xv and yv can be obtained from and Ltot, and hence, the required amounts of each token for any given trade can be computed from those values. Therefore, knowing the values of and Ltot is enough to compute the parameters of a trade. Moreover, tracking the values of and Ltot is equivalent to tracking the virtual reserves xv and yv.
Net liquidity. For any initialized tick index i, the net liquidity variable ΔL(i) measures the change of the liquidity parameter Ltot when crossing the initialized tick whose index is i. Hence, when the price crosses the tick index i, the amount ΔL(i) is added to or subtracted from Ltot in order to obtain the liquidity parameter of the new price interval.
In this way, the smart contract keeps track of the total liquidity Ltot, and when the price crosses an initialized tick t whose index is i, the total liquidity is updated as Ltot + ΔL(i) if the tick t is crossed from left to right, or as Ltot − ΔL(i) if the tick t is crossed from the right to left.
Gross liquidity. For any initialized tick index i, the gross liquidity at tick i is the sum of the liquidity parameters of all the positions referencing the tick indexed by i. This variable is used to track which ticks are really needed. If, after a position is removed, the gross liquidity of an initialized tick t becomes 0, this means that tick t is no longer referenced by any position, and thus, it can be uninitialized.
Observe that this cannot be done simply by observing the net liquidity of a tick, since the net liquidity of a tick t might be zero, but there might still be positions referencing that tick. For example, if there are exactly two positions referencing tick t given by price intervals [p1, t] and [t, p2], respectively, and both positions have the same liquidity parameter, then, if i is the tick index of t, we obtain that the net liquidity ΔL(i) is 0, but clearly, we still need to keep track of tick t because there are two positions that reference it and, for example, we need to record separately the fees earned by both positions.
Before going on with the description of the next variables, we will give an example to illustrate how the net liquidity and gross liquidity variables are defined.
First, we will analyze the net liquidity variable. Moving from lower prices to higher prices, when we cross tick t1, an amount 2 of liquidity is added (the liquidity of position A). Thus, the value of the net liquidity variable at tick t1 is 2. Then, when we cross tick t2, an amount 3 of liquidity is added (the liquidity of position B), and an amount 2 of liquidity is removed (the liquidity of position A). Hence, the value of the net liquidity variable at tick t2 is 1. Note that the total liquidity within the interval (t2, t3) is 3, which is 1 unit higher than the total liquidity that corresponds to the interval (t1, t2).
Afterward, when we cross tick t3, an amount 1 of liquidity is added (the liquidity of position C), and hence, the value of the net liquidity variable at tick t3 is 1. Then, when we cross tick t4, an amount 3 of liquidity is removed (the liquidity of position B), and hence, the value of the net liquidity variable at tick t4 is –3. Observe that the total liquidity within the interval (t4, t5) is 1, which is 3 units lower than the total liquidity that corresponds to the interval (t3, t4) (which is equal to 4 since it is the sum of the liquidity parameters of positions B and C).
Then, when we cross tick t5, an amount 1 of liquidity is removed (the liquidity of position C), and an amount 1 of liquidity is added (the liquidity of position D). Thus, the value of the net liquidity variable at tick t5 is 0. Finally, when we cross tick t6, an amount 1 of liquidity is removed, and hence, the value of the net liquidity variable at tick t6 is −1.
Now, we will analyze the gross liquidity variable. Clearly, the value of the gross liquidity variable at tick t1 is 2 since the only position that references tick t1 is position A, and the liquidity parameter of position A is 2. Next, the value of the gross liquidity variable at tick t2 is 5 since the positions that reference tick t2 are A and B, and their liquidity parameters are 2 and 3, respectively. Then, observe that the only position that references tick t3 is position C, and hence, the value of the gross liquidity variable at tick t3 is 1, which is the value of the liquidity parameter of position C. Note that position B does not reference tick t3 even though tick t3 is within its range. In a similar way, the only position that references tick t4 is position B, and hence, the value of the gross liquidity variable at tick t4 is 3 since this is the value of the liquidity parameter of position B.
Next, there exist two positions that reference tick t5, which are positions C and D. Since the liquidity parameters of both of them are 1, we obtain that the value of the gross liquidity variable at tick t5 is 2. Observe that the value of the net liquidity variable at tick t5 is 0, but nonetheless, there exist two positions that reference that tick. This illustrates why the gross liquidity variable is needed in the smart contract of Uniswap v3. Finally, the value of the gross liquidity variable at tick t6 is 1 since the only position that references tick t6 is position D, which has liquidity parameter equal to 1.
Total of collected fees. The protocol keeps track of all the fees that were collected. These fees are not deposited into the pool but rather kept separately in the same token that they were charged. Hence, two variables are needed to do that, which are denoted by and . For simplicity, the variables and record the total amount of fees collected per unit of liquidity. Otherwise, it would be harder to track the amount of fees that a particular liquidity provider owns if the amount of total liquidity changes, and in this case, it would be needed to record the amount of fees owned by each and every position.
Total of fees collected from “outside.” Any tick t divides the set of positive real numbers into two intervals: [0, t) and [t, +∞). Given any tick t and the current price p, we define the outer interval that corresponds to tick t with respect to the price p as the only of the two intervals, [0, t) and [t, +∞), that does not contain p. Clearly, which of the previous intervals is the outer one may change if the price changes.
For example, consider tick t = 3,699.634 with tick index 82,164 from Example 5.1. The tick t divides the set of positive real numbers into the two intervals [0, 3,699.634) and [3,699.634, +∞). If the current price p is 3,750, then the outer interval that corresponds to tick t with respect to p is [0,3,699.634), since that interval does not contain p. On the other hand, if the current price p is 3,345, then the outer interval that corresponds to tick t with respect to p is [3,699.634, +∞), since that interval does not contain p.
For any initialized tick index i, the smart contract of Uniswap v3 tracks two variables, denoted by and , which record the total amount of fees per unit of liquidity that were collected in each of the two tokens in the outer interval corresponding to the tick with index i, that is, in the interval [0, t) if ic ≥ i, or in the interval [t, +∞) if ic < i.
Given an initialized tick t with index i, if the price crosses t, then the outer interval changes, and hence, the variables and need to be updated consequently. This is done by setting, for each j ∈ {0, 1}, the variable as , since the sum of the amount of fees collected in the intervals [0, t) and [t, +∞) is equal to the total collected fees.
where ic is the current tick index. This amounts to saying that all the fees were collected below the tick whose index is i, which clearly might not be true. However, since these values are only used for relative computations (as we shall see), this inaccuracy does not modify the results that we are interested in. Hence, the variables and do not have to be deemed to be real amounts of collected fees but rather auxiliary values that are needed for the computation of the fees that each position has earned. Observe also that an arbitrary definition as the previous one for the initial values of and is needed, since when the new tick index i is initialized, there is no way to determine the amount of fees that correspond to each of the intervals [0, t) and [t, +∞), where t is the tick whose index is i.
5.5.2 Fees
As we mentioned at the beginning of this section, Uniswap v3 charges a trading fee on the incoming token for each trade that is made. The smart contract of Uniswap v3 also allows setting a protocol fee, which is a fraction of each trading fee that is kept aside for the protocol and hence is not given to the liquidity providers. Both the percentages of the trading fee and the protocol fee are set and fixed when a new liquidity pool is created. The percentage of the protocol fee is zero by default but can be defined to be any number among a permitted set of values. For simplicity, from now on, we will assume that the protocol fee is zero. This simplification will not affect the understanding of how Uniswap v3 works since the only consequence when the protocol fee is not zero is that the amount of trading fees changes, but the way they are collected and tracked in the different variables and paid to liquidity providers remains unmodified.
In order to compute the amount of fees that a certain position collects, we need to introduce the concepts of fees from above, fees from below, and fees within a range.
Fees from above and below. Given a tick t with index i, and given j ∈ {0, 1}, we want to compute the fees in token j that were collected (per unit of liquidity) from above and from below tick t, that is, in the intervals [t, +∞) and [0, t), respectively. The fees from above the tick whose index is i will be denoted by , and the fees from below the tick whose index is i will be denoted by .
Example on how the variables , and (for j ∈ {0, 1}) are updated
Input data | Global fee | Fee variables at tick t2 | |||||||
Price movement | Collected fee | ||||||||
Initial price: t1 | − | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
t1 → t2 | 10 Y | 0 | 10 | 0 | 0 | 0 | 0 | 0 | 10 |
t2 tick cross | − | 0 | 10 | 0 | 0 | 0 | 10 | 0 | 10 |
t2 → t3 | 15 Y | 0 | 25 | 0 | 0 | 0 | 10 | 15 | 10 |
t2 ← t3 | 4 X | 4 | 25 | 0 | 4 | 0 | 10 | 15 | 10 |
t2 tick cross | − | 4 | 25 | 4 | 4 | 0 | 15 | 15 | 10 |
t1 ← t2 | 3 X | 7 | 25 | 4 | 4 | 3 | 15 | 15 | 10 |
It is important to observe that when a trade is performed and a fee is collected, the global fee variables and need to be updated—actually, just only one of them is updated because the fee is collected in only one of the two pool tokens. In consequence, the values of the variables and , might change as well. In addition, the values of the variables and remain unmodified when a trade is performed (if no initialized ticks are crossed) since these variables represent the fees collected outside the price interval we are currently in, and clearly, the amounts of fees collected outside the current interval do not change when a trade is performed inside this interval.
On the other hand, when a tick is crossed, the variables and need to be updated, as we explained in the previous subsection. Observe that the values of the variables and , remain the same when a tick is crossed since the change in the values of the variables and is compensated by the modification in the formulae that define and .
In Table 5-3, we show how all this occurs. We have to mention, though, that actually, the variables and , are neither tracked nor updated in the Uniswap v3 smart contract since there is no need to do that. They are just auxiliary variables that are used to make easier the computation of the fees earned by a position. Nevertheless, we think that it is worth including in this example how these variables change in order to better understand how the Uniswap v3 smart contract works.
Note that represents the fees in token j that were collected (per unit of liquidity) within the price range [l, u). It is important to observe that since is defined using and , which are in turn defined in terms of , as it occurred with , these variables cannot be used to obtain the real values of what they represent. However, they are useful to compute the amount of fees that a position has collected, as we will see next.
First part of the Position class of the smart contract of Uniswap v3
For simplicity, we will denote the initial values of and at the moment the deposit is made by F0 and F1. When the liquidity provider wants to redeem their fees, the smart contract takes the updated amounts and and gives to the liquidity provider an amount of token 0 and an amount of token 1. Then, the protocol updates the variables feeGrowthInside0LastX128 and feeGrowthInside1LastX128 as and , respectively, so as to be able to compute the amounts of fees that the position collects from this time on.
We previously mentioned that the variables , and cannot be used to determine the exact amount of collected fees in the intervals they track, since when a new tick index i is initialized, the variable is arbitrarily defined in a certain way that does not measure the amount of fees that were collected before. However, after that initial state, and since the variable is continuously incremented with the new fees that are charged, the variables , and are also updated with those increments on fees (when the fees are collected in the corresponding intervals, of course). Therefore, the differences and do indeed track the increments in the amounts of fees (per unit of liquidity) since the last time the liquidity provider redeemed their share of fees.
5.5.3 Trades
In Uniswap v3, trades are performed in a similar way as in Uniswap v2, following a constant product formula. However, in order to perform trades in Uniswap v3, we need to take into account the list of initialized ticks, since the liquidity parameter might change when the price crosses an initialized tick.
Informally, iu and il are the initialized tick indexes that are the nearest to ic that are located to the right and to the left of ic, respectively. Observe that ic ∈ [il, iu) and that the interval (il, iu) does not contain any initialized tick indexes. Thus, the liquidity parameter does not change within the interval [il, iu].
Let tl and tu be the ticks whose indexes are il and iu, respectively. Note that p ∈ [tl, tu) since ic ∈ [il, iu). When a trade is going to be made, the protocol checks if the trade can be made within the current tick interval [tl, tu]; that is, the protocol checks if the liquidity of the interval [tl, tu] is enough to perform the trade. If this is the case, the trade is executed, and the corresponding fees are collected, as we explained in previous sections. On the other hand, if the liquidity on that interval is not enough to perform the whole trade, then a portion of the trade is performed until the price reaches the boundary of the interval [tl, tu], and then the trade continues on the next interval (to the right or the left depending on which boundary was reached) with the remaining part of the trade. We mention that the intervals considered are formed by consecutive initialized ticks, since ticks that are not initialized might be ignored.
We will analyze four cases.
Case 1: The trader wants to obtain an amount a of token X.
and thus, the updated price would be equal to tu, as expected. Now, since a > xr, there is a remaining amount of token X to be included in the trade (the remaining amount is a − xr). Hence, tick tu needs to be crossed, and the trade continues on the next interval, which is [tu, t'], where t' is the smallest initialized tick that is greater than tu. When tick tu is crossed, the variables , and are updated. After that, the remaining amount of token X is traded considering the interval [tu, t'].
Clearly, the same considerations explained previously apply to the remaining part of the trade. If the real balance of token X within the interval [tu, t'] is enough to perform the rest of the trade, then the trade is completed, the necessary (input) amount of token Y is computed and deposited, and the corresponding fee on token Y is collected. On the other hand, if the trade cannot be completed within the interval [tu, t'], another fraction of the trade is computed, and the trade continues on the next interval.
Case 2: The trader wants to deposit an amount a of token X in order to obtain an amount of token Y.
Note also that the trading fee that was charged in the first step is .
After the first step of the trade is completed, tick tl is crossed, and the trade continues in the interval [tʹ, tl], where tʹ is the greatest initialized tick that is smaller than tl. As in Case 1, when tick tl is crossed, the variables , and are updated.
It is important to mention that the rest of the trade is executed taking into account the previous discussion regarding this case and considering the interval [tʹ, tl]. Specifically, if the remaining part of the trade can be completed within the interval [tʹ, tl], then this remaining part is performed, and the whole trade is completed. On the other hand, if the remaining part of the trade cannot be completed within the interval [tʹ, tl], a suitable portion of the trade is executed in that interval, and the trade continues in the next one (to the left).
Case 3: The trader wants to obtain an amount b of token Y.
and thus, the price is tl, as expected. As there is a remaining amount of token Y to be included in the trade (since b > yr), tick tl is crossed, and the trade continues in the interval [tʹ, tl], where tʹ is the greatest initialized tick that is smaller than tl. Note that the remaining amount of token Y to be traded is b − yr. As in Case 2, the variables , and are updated when tick tl is crossed.
Again, the same considerations discussed previously apply for the remaining part of the trade with respect to the interval [tʹ, tl], in a similar way as in the previous cases.
Case 4: The trader wants to deposit an amount b of token Y in order to obtain an amount of token X.
Note also that the trading fee that was charged in the first step is .
After the first step of the trade is completed, tick tu is crossed, and the trade continues in the interval [tu, tʹ], where tʹ is the smallest initialized tick that is greater than tu. Observe that, as in the previous cases, when tick tu is crossed, the variables , and are updated.
It is worth mentioning that the same considerations discussed previously apply to the remaining part of the trade with respect to the interval [tu, tʹ]. Explicitly, if the remaining part of the trade can be completed within the interval [tu, tʹ], then this remaining part is executed, and the whole trade is completed. On the other hand, if the remaining part of the trade cannot be completed within the interval [tu, tʹ], a suitable portion of the trade is performed in that interval, and the trade continues in the next one.
We will make here an additional remark. In all four cases, when the trade cannot be completed within the current interval, we have implicitly assumed that a next (or previous) initialized tick tʹ exists, which defines the next interval [tu, tʹ] or the previous interval [tʹ, tl] according to the situation. Although this is usually the case, it might happen that such tick tʹ does not exist. This implies that there is no liquidity above tu (or below tl), and hence, the trade cannot continue on the next (or previous) interval. Thus, the algorithm will stop, and the trade will have been only partially fulfilled.
5.5.4 The swap Function
Function computeSwapStep of the smart contract of Uniswap v3
SwapState and StepComputations structs of the smart contract of Uniswap v3
swap function of the smart contract of Uniswap v3
while loop of the swap function of the smart contract of Uniswap v3
Usage of the computeSwapStep function in the smart contract of Uniswap v3
The computed fee is added to the global fee variable
The liquidity parameter is updated when an initialized tick is crossed
The global fee variable is updated
The token tranfers are performed
5.5.5 Example
We will now give a detailed example to illustrate how the Uniswap v3 protocol works.
Consider a Uniswap v3 liquidity pool with tokens ETH and USDC, a fee of 0.3%, and a tick spacing equal to 1. Let ϕ= 0.003. Suppose that the current price of ETH in terms of USDC is 3,800 and that exactly two liquidity providers, A and B, provided liquidity to the pool. Liquidity provider A chose to provide liquidity in the interval [3,600.005212, 3,999.742678] depositing 3 ETH and a suitable amount of USDC. Liquidity provider B decided to provide liquidity in the interval [3,899.823492, 4,099.761469] depositing 10 ETH. Observe that liquidity provider B only needs to deposit ETH since the current price of ETH is below the interval they chose.
Thus, liquidity provider A deposited liquidity in the range [t1, t3], and liquidity provider B deposited liquidity in the interval [t2, t4]. Let LA and LB be the liquidity parameters of the positions of liquidity providers A and B, respectively.
and hence, LA ≈ 7,312.6996.
and thus, LB ≈ 25,294.2194.
Since the trader wants to buy 1 ETH and xr > 1, it follows that the trade can be completed within the interval [t1, t2].
Note that 3,864.8853 ∈ [t1, t2].
Clearly, and also for all j ∈ {0, 1} and for all k ∈ {1,2,3,4}.
Trade 2. Suppose now that another trader wants to deposit 5,000 USDC into the pool in order to buy ETH.
Summary of price movements and update of fee variables in trades 1 and 2
Trade 1 | Trade 2 | |||
Step 1 | Tick cross | Step 2 | ||
Price | p0 → p1 | p1 → t2 | t2 | t2 → p3 |
Fee | 11.53151 | 6.1692 | 0 | 8.8308 |
0.0015769 | 0.0024205 | 0.0024205 | 0.0026913 | |
0 | 0 | 0.0024205 | 0.0024205 | |
0 | 0 | 0 | 0.0002708 | |
0.0015769 | 0.0024205 | 0.0024205 | 0.0024205 |
(see Table 5-4).
Since is clearly smaller than , the second step of the trade can be completed within the interval [t2, t3]. Note that is the amount to be actually deposited in the pool and traded for ETH.
In Table 5-4, we summarize the price movements, the amounts of fees charged, and how the variables , and are updated in the trades that were analyzed before. The values that are updated in each step are highlighted in bold type.
Collected fees and impermanent loss. We will now compute the amount of fees that each liquidity provider has obtained and find out whether they are facing an impermanent loss or not.
where the second equality follows from the fact that t1 < p3 < t3.
where the second equality follows from the fact that t2 < p3 < t4.
Therefore, liquidity provider A is facing an impermanent loss of approximately 23,756 − 23,661.1 = 94.9 USDC, which is not compensated by the fees of 19.68 USDC they have earned.
Therefore, liquidity provider B is facing an impermanent loss of approximately 39,110.73 − 39,107.45 = 3.28 USDC, which, in this case, is compensated by the fees of 6.85 USDC they have earned. Observe that the impermanent loss for liquidity provider B is very small because the amounts of ETH and USDC that their position currently has are very similar to those of their original deposit. This is so because although they entered the position at a price of 3,800, they provided liquidity in the interval [t2, t4] (which is approximately [3,900, 4,100]), and hence, their position did not change when the price moved from 3,800 to 3,900 (neither did they earn any trading fees). Note that the current price is p3 ≈ 3,911.0729, which is very near t2 ≈ 3,900.
5.6 LP Tokens
Unlike Uniswap v2, LP tokens in Uniswap v3 are nonfungible tokens (NFTs) since positions are highly customizable and will be, in general, very different for different liquidity providers. When a liquidity provider adds liquidity to a specific price range, a unique NFT defining the position is minted. Recall that in Uniswap v3, a liquidity provider’s position consists of the address of the owner, the lower tick index, and the upper tick index. These last two define the boundaries of the interval in which the liquidity provider deposited liquidity. The owner of the NFT that represents a certain position can modify that position or redeem the corresponding tokens.
Position library of the smart contract of Uniswap v3
We also point out that the Position class has two methods: get and update, as we could see in the previous code. The get method returns the Info struct of a position, while the update method credits the accumulated fees to a position and, if necessary, updates the liquidity parameter of that position.
5.6.1 Minting LP Tokens
mint function of the smart contract of Uniswap v3
5.6.2 Modifying the Position: Part I
Parameters needed to modify a position
First part of the _modifyPosition function of the smart contract of Uniswap v3
5.6.3 Update the Position
_updatePosition function of the smart contract of Uniswap v3
5.6.4 Tick Class
liquidityGross
liquidityNet
feeGrowth0utside0X128
feeGrowth0utside1X128
feeGrowthBelow0X128
feeGrowthBelow 1X128
feegrowthAbove0X128
feegrowthAbove 1X128
feeGrowthInside0X128
feeGrowthInside1X128
Tick class of the smart contract of Uniswap v3
Modifying the Position: Part II
_modifyPosition function of the smart contract of Uniswap v3
Functions getAmount0Delta and getAmount1Delta of the smart contract of Uniswap v3
correspond to the variables and , respectively, that are used in Table 5-1.
5.6.5 Burning LP Tokens
The owner of LP tokens of a Uniswap v3 pool can remove liquidity. The liquidity removal process will burn the LP tokens, remove the position, and give back to the liquidity provider a certain amount of assets, according to the formulae of Table 5-1. In addition, the fees that accumulated since the last time they were collected are given to the liquidity provider.
burn function of the smart contract of Uniswap v3
5.7 Analysis of Liquidity Provisioning
Compared to Uniswap v2, in Uniswap v3, liquidity is provided with higher capital efficiency, meaning that with the same amount of deposited tokens, one can obtain a higher liquidity parameter in the chosen interval. This is beneficial for traders, since the higher the liquidity parameter is, the lower the price impact will be. However, for liquidity providers, the situation is very different. In order to analyze this, we will consider two situations with two different assumptions.
First, suppose that the exact same trades are executed in a Uniswap v2 pool and in a Uniswap v3 pool. In this case, the amount of trading fees collected by both pools will be the same—for example, 0.3% of the incoming amount of each trade—and the total collected fees will be shared between all the liquidity providers. In the Uniswap v2 pool, the fees will be shared in a way that is proportional to the deposits the liquidity providers made, while in the Uniswap v3 pool, this proportion will also depend on the interval that each liquidity provider chose. More explicitly, in the case of the Uniswap v3 pool, as we have seen, the liquidity providers earn fees only for the trades (or portion of trades) that are performed within the interval they chose, and in this case, their share of fees is equal to the liquidity parameter they provided divided by the total liquidity—which will also be higher than in a Uniswap v2 pool. To sum up, comparing a Uniswap v2 pool with a Uniswap v3 pool, if the same trades are performed, the total collected fees will be the same, and thus, liquidity providers will not be able to obtain more fees in general with the Uniswap v3 pool, although it may happen that some of them earn more fees with respect to a Uniswap v2 pool and some of them earn fewer fees.
For the second situation, we will consider a completely different assumption. One can argue that a considerable part of the trades that are executed in a Uniswap pool (either v2 or v3) is done by arbitrageurs and that arbitrageurs have enough money to perform any required trade. Thus, every time the spot price of the pool deviates from the market price, arbitrageurs will step in and make a trade. Due to the concentrated liquidity of Uniswap v3, in the price range that is near the market price, we usually have far more liquidity than in a Uniswap v2 pool. Hence, the arbitrageurs of Uniswap v3 will need to perform a bigger trade to make the spot price equal to the market price, or in other words, they will have the opportunity to make more money with the arbitrage. Since a bigger trade is performed, a larger amount of fees is collected, which may, at first sight, seem beneficial to liquidity providers. However, as we have seen in Section 5.3, concentrated liquidity also implies a higher impermanent loss for liquidity providers in many different possible scenarios. In addition, several analyses show that the possible greater amount of fees that liquidity providers collect (under the assumptions of this paragraph) does not compensate for the higher impermanent losses [25, 33].
5.7.1 Capital Efficiency
In order to compute the amount of liquidity that a liquidity provider puts into a Uniswap v3 pool, we will assume that the entry price is the geometric mean of the boundaries of the interval in which they provide liquidity. Hence, if the current price is p, we will assume that the liquidity provider chooses a price interval [pa, pb] such that . With this assumption, the amounts of each token that are needed to provide liquidity in a Uniswap v3 pool will satisfy the deposit requirements of a Uniswap v2 pool, as we shall see in the following text.
This means that if, for example, p is 20% higher than pa, then pb is 20% higher than p.
Values of the ratios between the liquidity parameters of a Uniswap v3 and a Uniswap v2 position for different values of the parameter r that measures the length of the price interval of Uniswap v3
r | ||
1.005 | 1.010025 | 401.5 |
1.01 | 1.0201 | 201.5 |
1.05 | 1.1025 | 41.5 |
1.1 | 1.21 | 21.5 |
1.2 | 1.44 | 11.5 |
2 | 4 | 3.41 |
10 | 100 | 1.46 |
100 | 10,000 | 1.11 |
If r = 1.05, then pb = 1.1025pa, and hence, pb is approximately 10% higher than pa. In this case, the liquidity parameter of a Uniswap v3 position is approximately 41.5 times higher than that of the same deposit into a Uniswap v2 pool. As expected, if the interval in which liquidity is deposited is smaller, then the liquidity parameter will be bigger since if the same amount of tokens is used for trading in a smaller interval, then we can provide more liquidity into that interval.
Of course, we have to remember that if the interval in which the liquidity provider provided liquidity is smaller, then the likelihood of the price falling out of that interval will be greater, and so will be the impermanent loss (see Figure 5-5). In addition, when the price goes out of that interval, the liquidity provider will stop earning trading fees.
5.7.2 Independence with Respect to Other Liquidity Providers
We will now prove that the amount of fees that a liquidity provider earns in a Uniswap v3 pool depends only on the price movement and the parameters that define their position and not on the positions of the other liquidity providers that have deposited liquidity on the same pool. We formalize this statement in the following proposition.
Proposition 5.2. Consider a Uniswap v3 liquidity pool with tokens X and Y and fee ϕ. Suppose that a liquidity provider owns a position in the pool. For each positive real number p, let xr(p) and yr(p) be the amount of real reserves at price p of tokens X and Y, respectively, of the liquidity provider’s position.
If p1 < p2, then the liquidity provider earns an amount
If p1 > p2, then the liquidity provider earns an amount
of token X in fees.
Let L be the liquidity parameter of the liquidity provider’s position, and let [pa, pb] be the price interval of the position. For all positive real numbers p, q, let F(p, q) be the amount of trading fees that the liquidity provider’s position earns when the price moves from p to q in a single trade. We will first prove the following assertion, which will be called assertion (A).
(A) For all positive real numbers p, q, if pa ≤ p < q ≤ pb, then .
where the last equality follows from the formulae of Table 5-1. Hence, we have proved assertion (A).
Now we will prove the following assertion, which will be called assertion (B).
(B) For all positive real numbers p, q, if pa ≤ q < p ≤ pb, then .
where the last equality follows from the formulae of Table 5-1. Hence, we have proved assertion (B).
We will now prove the first statement of the proposition. Suppose that p1 < p2. Since the price increases, an amount of token Y is deposited, and thus, the trading fee is charged on token Y. We will divide our analysis into several cases and subcases.
Case 1: p1 ≥ pb.
that is, the first formula of the statement of the proposition holds.
Case 2: p2 ≤ pa.
and hence, the first formula of the statement of the proposition holds.
Case 3: p1 < pb and p2 > pa.
We divide the analysis of this case into four subcases.
Case 3.1: p1 < pa and pa < p2 ≤ pb.
where the second equality holds by assertion (A).
Case 3.2: p1 < pa and p2 > pb.
where the second equality holds by assertion (A).
Case 3.3: pa ≤ p1 < pb and pa < p2 ≤ pb.
Case 3.4: pa ≤ p1 < pb and p2 > pb.
where the second equality holds by assertion (A).
Therefore, we have proved the first statement of the proposition. Clearly, the second statement of the proposition can be proved in a similar way using assertion (B) instead of assertion (A).□
5.8 Summary
In this chapter, we gave a comprehensive description of the Uniswap v3 AMM, and we thoroughly explained how it is implemented. In addition, we delved deeply into its smart contract and described the variables and methods that are used in the actual code. We also analyzed the impermanent losses of Uniswap v3 positions and compared them with those of similar Uniswap v2 positions.