How to calculate Uniswap v3 pool's Total Value Locked (TVL) on chain?

7.9k views Asked by At

I want to calculate the total value locked in a particular pool in Uniswap v3. I can't use the subgraph API for this.

enter image description here

I can get current liquidity / in range liquidity using uniswapV3pool contract function:

in_range_liquidity = uniswapV3pool_contract.functions.liquidity().call()

I get the result 10608850786221311055 for liquidity. Do I need to process it to get the USD value or something else?

Finally this is just current liquidity, I need total locked value, which includes both active and inactive liquidity in the pool.

1

There are 1 answers

9
kfx On

The total value locked in Uniswap's v3 pool is not always straightforward to get. The liquidity itself not a good measure of the real token amounts in the pool. Uniswap v3 liquidity describes the concentrated liquidity value of the virtual token amounts, not the real amounts.

As the simplest option, you can get the on-chain amounts by calling the balanceOf function on the pool's contract:

balanceToken0 = poolContract.functions.balanceOf(token0Address).call()

This value is going to also include unclaimed fees. In Uniswap v3, these fees are not part of the liquidity. If you want to get the token amounts that contribute to liquidity, then a balanceOf call is not sufficient. It leaves you with two different options for on-chain calculations:

a) Iterate over all tick ranges with non-zero liquidity.

b) Iterate over all open positions.

What follows is some quick and unoptimized Python code that implements the approach (a). It needs MIN_TICK, MAX_TICK, TICK_SPACING, as well as URL, POOL_ADDRESS and V3_ABI to be defined.

from collections import namedtuple
from web3 import Web3

web3 = Web3(Web3.HTTPProvider(URL))
pool = Web3.toChecksumAddress(POOL_ADDRESS)
contract = web3.eth.contract(address=POOL_ADDRESS, abi=V3_ABI)

Tick = namedtuple("Tick", "liquidityGross liquidityNet feeGrowthOutside0X128 feeGrowthOutside1X128 tickCumulativeOutside secondsPerLiquidityOutsideX128 secondsOutside initialized")

amounts0 = 0
amounts1 = 0
liquidity = 0
slot0 = contract.functions.slot0().call()
sqrtPriceCurrent = slot0[0] / (1 << 96)

def calculate_token0_amount(liquidity, sp, sa, sb):
    sp = max(min(sp, sb), sa)
    return liquidity * (sb - sp) / (sp * sb)

def calculate_token1_amount(liquidity, sp, sa, sb):
    sp = max(min(sp, sb), sa)
    return liquidity * (sp - sa)

for tick in range(MIN_TICK, MAX_TICK, TICK_SPACING):
  tickRange = Tick(*contract.functions.ticks(tick).call())
  liquidity += tickRange.liquidityNet
  sqrtPriceLow = 1.0001 ** (tick // 2)
  sqrtPriceHigh = 1.0001 ** ((tick + TICK_SPACING) // 2)
  amounts0 += calculate_token0_amount(liquidity, sqrtPriceCurrent, sqrtPriceLow, sqrtPriceHigh)
  amounts1 += calculate_token1_amount(liquidity, sqrtPriceCurrent, sqrtPriceLow, sqrtPriceHigh)

print(amounts0, amounts1) # for better output, should correct for the amount of decimals before printing

The value of TICK_SPACING can be read from the tickSpacing() function of the pool's contract. Alteratively, if you know swap fee levels of the pool, you can use a constant: 1% pools have always have 200 as the tick spacing, etc.

The values of MIN_TICK and MAX_TICK can be obtained from tickBitmap() calls and looking at the lowest and the highest initialized tick respectively. It's quite complex and a better fit for a separate question. At the worst case if you may need to cover the whole tick range, which spans between -887272 and +887272. So for a start you can use these values rounded down / up to the tick spacing value.

Edit: the square root of 1.0001 ^ tick is equal to 1.0001 ^ (tick / 2), a fact that use in these lines to make the computation simples:

sqrtPriceLow = 1.0001 ** (tick // 2)
sqrtPriceHigh = 1.0001 ** ((tick + TICK_SPACING) // 2)