Calculating liquidity
Trading is not possible without liquidity, and to make our first swap we need to put some liquidity into the pool contract. Here’s what we need to know to add liquidity to the pool contract:
- A price range. As a liquidity provider, we want to provide liquidity at a specific price range, and it’ll only be used in this range.
- Amount of liquidity, which is the amounts of two tokens. We’ll need to transfer these amounts to the pool contract.
Here, we’re going to calculate these manually, but, in a later chapter, a contract will do this for us. Let’s begin with a price range.
Price Range Calculation
Recall that, in Uniswap V3, the entire price range is demarcated into ticks: each tick corresponds to a price and has an index. In our first pool implementation, we’re going to buy ETH for USDC at the price of $5000 per 1 ETH. Buying ETH will remove some amount of it from the pool and will push the price slightly above $5000. We want to provide liquidity at a range that includes this price. And we want to be sure that the final price will stay within this range (we’ll do multi-range swaps in a later milestone).
We’ll need to find three ticks:
- The current tick will correspond to the current price (5000 USDC for 1 ETH).
- The lower and upper bounds of the price range we’re providing liquidity into. Let the lower price be $4545 and the upper price be $5500.
From the theoretical introduction, we know that:
Since we’ve agreed to use ETH as the reserve and USDC as the reserve, the prices at each of the ticks are:
Where is the current price, is the lower bound of the range, and is the upper bound of the range.
Now, we can find corresponding ticks. We know that prices and ticks are connected via this formula:
Thus, we can find tick via:
The square roots in this formula cancel out, but since we’re working with we need to preserve them.
Let’s find the ticks:
- Current tick:
- Lower tick:
- Upper tick:
To calculate these, I used Python:
import math def price_to_tick(p): return math.floor(math.log(p, 1.0001)) price_to_tick(5000) > 85176
That’s it for price range calculation!
Last thing to note here is that Uniswap uses Q64.96 number to store . This is a fixed-point number that has 64 bits for the integer part and 96 bits for the fractional part. In our above calculations, prices are floating point numbers: 70.71
, 67.42
, and 74.16
. We need to convert them to Q64.96. Luckily, this is simple: we need to multiply the numbers by (Q-number is a binary fixed-point number, so we need to multiply our decimals numbers by the base of Q64.96, which is ). We’ll get:
In Python:
q96 = 2**96 def price_to_sqrtp(p): return int(math.sqrt(p) * q96) price_to_sqrtp(5000) > 5602277097478614198912276234240
Notice that we’re multiplying before converting to an integer. Otherwise, we’ll lose precision.
Token Amounts Calculation
The next step is to decide how many tokens we want to deposit into the pool. The answer is as many as we want. The amounts are not strictly defined, we can deposit as much as it is enough to buy a small amount of ETH without making the current price leave the price range we put liquidity into. During development and testing we’ll be able to mint any amount of tokens, so getting the amounts we want is not a problem.
For our first swap, let’s deposit 1 ETH and 5000 USDC.
Recall that the proportion of current pool reserves tells the current spot price. So if we want to put more tokens into the pool and keep the same price, the amounts must be proportional, e.g.: 2 ETH and 10,000 USDC; 10 ETH and 50,000 USDC, etc.
Liquidity Amount Calculation
Next, we need to calculate based on the amounts we’ll deposit. This is a tricky part, so hold tight!
From the theoretical introduction, you remember that:
However, this formula is for the infinite curve 🙂 But we want to put liquidity into a limited price range, which is just a segment of that infinite curve. We need to calculate specifically for the price range we’re going to deposit liquidity into. We need some more advanced calculations.
To calculate for a price range, let’s look at one interesting fact we have discussed earlier: price ranges can be depleted. It’s possible to buy the entire amount of one token from a price range and leave the pool with only the other token.
At the points and , there’s only one token in the range: ETH at the point and USDC at the point .
That being said, we want to find an that will allow the price to move to either of the points. We want enough liquidity for the price to reach either of the boundaries of a price range. Thus, we want to be calculated based on the maximum amounts of and .
Now, let’s see what the prices are at the edges. When ETH is bought from a pool, the price is growing; when USDC is bought, the price is falling. Recall that the price is . So, at point , the price is the lowest of the range; at point , the price is the highest.
In fact, prices are not defined at these points because there’s only one reserve in the pool, but what we need to understand here is that the price around the point is higher than the start price, and the price at the point is lower than the start price.
Now, break the curve from the image above into two segments: one to the left of the start point and one to the right of the start point. We’re going to calculate two ’s, one for each of the segments. Why? Because each of the two tokens of a pool contributes to either of the segments: the left segment is made entirely of token , and the right segment is made entirely of token . This comes from the fact that, during swapping, the price moves in either direction: it’s either growing or falling. For the price to move, only either of the tokens is needed:
- when the price is growing, only token is needed for the swap (we’re buying token , so we want to take only token from the pool);
- when the price is falling, only token is needed for the swap.
Thus, the liquidity in the segment of the curve to the left of the current price consists only of token and is calculated only from the amount of token provided. Similarly, the liquidity in the segment of the curve to the right of the current price consists only of token and is calculated only from the amount of token provided.
This is why, when providing liquidity, we calculate two ’s and pick one of them. Which one? The smaller one. Why? Because the bigger one already includes the smaller one! We want the new liquidity to be distributed evenly along the curve, thus we want to add the same to the left and to the right of the current price. If we pick the bigger one, the user would need to provide more liquidity to compensate for the shortage in the smaller one. This is doable, of course, but this would make the smart contract more complex.
What happens with the remainder of the bigger ? Well, nothing. After picking the smaller we can simply convert it to a smaller amount of the token that resulted in the bigger –this will adjust it down. After that, we’ll have token amounts that will result in the same .
The final detail I need to focus your attention on here is: new liquidity must not change the current price. That is, it must be proportional to the current proportion of the reserves. And this is why the two ’s can be different–when the proportion is not preserved. And we pick the small to reestablish the proportion.
I hope this will make more sense after we implement this in code! Now, let’s look at the formulas.
Let’s recall how and are calculated:
We can expand these formulas by replacing the delta P’s with actual prices (we know them from the above):
is the price at the point , is the price at the point , and is the current price (see the above chart). Notice that, since the price is calculated as (i.e. it’s the price of in terms of ), the price at point is higher than the current price and the price at . The price at is the lowest of the three.
Let’s find the from the first formula:
And from the second formula:
So, these are our two ’s, one for each of the segments:
Now, let’s plug the prices we calculated earlier into them:
After converting to Q64.96, we get:
And for the other :
Of these two, we’ll pick the smaller one.
In Python:
sqrtp_low = price_to_sqrtp(4545) sqrtp_cur = price_to_sqrtp(5000) sqrtp_upp = price_to_sqrtp(5500) def liquidity0(amount, pa, pb): if pa > pb: pa, pb = pb, pa return (amount * (pa * pb) / q96) / (pb - pa) def liquidity1(amount, pa, pb): if pa > pb: pa, pb = pb, pa return amount * q96 / (pb - pa) eth = 10**18 amount_eth = 1 * eth amount_usdc = 5000 * eth liq0 = liquidity0(amount_eth, sqrtp_cur, sqrtp_upp) liq1 = liquidity1(amount_usdc, sqrtp_cur, sqrtp_low) liq = int(min(liq0, liq1)) > 1517882343751509868544
Token Amounts Calculation, Again
Since we choose the amounts we’re going to deposit, the amounts can be wrong. We cannot deposit any amounts at any price range; the liquidity amount needs to be distributed evenly along the curve of the price range we’re depositing into. Thus, even though users choose amounts, the contract needs to re-calculate them, and actual amounts will be slightly different (at least because of rounding).
Luckily, we already know the formulas:
In Python:
def calc_amount0(liq, pa, pb): if pa > pb: pa, pb = pb, pa return int(liq * q96 * (pb - pa) / pa / pb) def calc_amount1(liq, pa, pb): if pa > pb: pa, pb = pb, pa return int(liq * (pb - pa) / q96) amount0 = calc_amount0(liq, sqrtp_upp, sqrtp_cur) amount1 = calc_amount1(liq, sqrtp_low, sqrtp_cur) (amount0, amount1) > (998976618347425408, 5000000000000000000000)
As you can see, the numbers are close to the amounts we want to provide, but ETH is slightly smaller.
Hint: use
cast --from-wei AMOUNT
to convert from wei to ether, e.g.:
cast --from-wei 998976618347425280
will give you0.998976618347425280
.