diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index f969375..01a7a84 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -377,6 +377,32 @@ def test_get_tvl_in_pool_on_chain(self, client: Uniswap, tokens, token0, token1) assert tvl_0 > 0 assert tvl_1 > 0 + @pytest.mark.parametrize("token0, token1", [("DAI", "USDC")]) + def test_asset_locked_per_tick_sums_to_tvl(self, client: Uniswap, tokens, token0, token1): + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + + pool = client.get_pool_instance(tokens[token0], tokens[token1]) + asset_locked_per_tick_dict = client.get_asset_locked_per_tick_in_pool(pool) + + # check TVL adds up correctly + token0_total = 0 + token1_total = 0 + ticks = asset_locked_per_tick_dict['ticks'] + token0_arr = asset_locked_per_tick_dict['token0'] + token1_arr = asset_locked_per_tick_dict['token1'] + + for i, tick in enumerate(ticks): + token0_total += token0_arr[i] + token1_total += token1_arr[i] + + tvl_0, tvl_1 = client.get_tvl_in_pool(pool) + + # assert on values rounded to nearest million for now TODO: fix + assert round(tvl_0 / 1e6) == round(token0_total / 1e6) + assert round(tvl_1 / 1e6) == round(token1_total / 1e6) + assert round((tvl_0 + tvl_1) / 1e6) == round((token0_total + token1_total) / 1e6) + @pytest.mark.skip @pytest.mark.parametrize( "token, max_eth", diff --git a/uniswap/constants.py b/uniswap/constants.py index e6d3f89..3eac4d2 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -64,7 +64,7 @@ MAX_TICK = -MIN_TICK # Source: https://github.com/Uniswap/v3-core/blob/v1.0.0/contracts/UniswapV3Factory.sol#L26-L31 -_tick_spacing = {100:1, 500: 10, 3_000: 60, 10_000: 200} +_tick_spacing = {100: 1, 500: 10, 3_000: 60, 10_000: 200} # Derived from (MIN_TICK//tick_spacing) >> 8 and (MAX_TICK//tick_spacing) >> 8 -_tick_bitmap_range = {100:(-3466, 3465), 500: (-347, 346), 3_000: (-58, 57), 10_000: (-18, 17)} +_tick_bitmap_range = {100: (-3466, 3465), 500: (-347, 346), 3_000: (-58, 57), 10_000: (-18, 17)} diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index ae5ad16..deda008 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1385,6 +1385,101 @@ def get_tvl_in_pool(self, pool: Contract) -> Tuple[float, float]: token1_liquidity = token1_liquidity // (10**token1_decimals) return (token0_liquidity, token1_liquidity) + def get_asset_locked_per_tick_in_pool(self, pool: Contract) -> Dict: + """ + Iterate through each tick in a pool and calculate the amount of asset + locked on-chain per tick + + Note: the output of this function may differ from what is returned by the + UniswapV3 subgraph api (https://github.com/Uniswap/v3-subgraph/issues/74) + + Params + ------ + pool: Contract + pool contract instance to find TVL + """ + pool_tick_output_types = ( + "uint128", + "int128", + "uint256", + "uint256", + "int56", + "uint160", + "uint32", + "bool", + ) + + pool_immutables = self.get_pool_immutables(pool) + pool_state = self.get_pool_state(pool) + fee = pool_immutables["fee"] + sqrtPrice = pool_state["sqrtPriceX96"] / (1 << 96) + + TICK_SPACING = _tick_spacing[fee] + BITMAP_SPACING = _tick_bitmap_range[fee] + + _max_tick = self.find_tick_from_bitmap(BITMAP_SPACING, pool, TICK_SPACING, fee, True) + _min_tick = self.find_tick_from_bitmap(BITMAP_SPACING, pool, TICK_SPACING, fee, False) + + assert _max_tick != False, "Error finding max tick" + assert _min_tick != False, "Error finding min tick" + + # # Correcting for each token's respective decimals + token0_decimals = ( + _load_contract_erc20(self.w3, pool_immutables["token0"]) + .functions.decimals() + .call() + ) + token1_decimals = ( + _load_contract_erc20(self.w3, pool_immutables["token1"]) + .functions.decimals() + .call() + ) + Batch = namedtuple("Batch", "ticks batchResults") + ticks = [] + # Batching pool.functions.tick() calls as these are the major bottleneck to performance + for batch in list(chunks(range(_min_tick, _max_tick, TICK_SPACING), 1000)): + _batch = [] + _ticks = [] + for tick in batch: + _batch.append( + ( + pool.address, + HexBytes(pool.functions.ticks(tick)._encode_transaction_data()), + ) + ) + _ticks.append(tick) + ticks.append(Batch(_ticks, self.multicall(_batch, pool_tick_output_types))) + + liquidity_total = 0 + liquidity_per_tick_dict: Dict = { + 'ticks': [], + 'token0': [], + 'token1': [] + } + for tickBatch in ticks: + tick_arr = tickBatch.ticks + for i in range(len(tick_arr)): + tick = tick_arr[i] + tickData = tickBatch.batchResults[i] + # source: https://stackoverflow.com/questions/71814845/how-to-calculate-uniswap-v3-pools-total-value-locked-tvl-on-chain + liquidityNet = tickData[1] + liquidity_total += liquidityNet + sqrtPriceLow = 1.0001 ** (tick // 2) + sqrtPriceHigh = 1.0001 ** ((tick + TICK_SPACING) // 2) + token0_liquidity = self.get_token0_in_pool( + liquidity_total, sqrtPrice, sqrtPriceLow, sqrtPriceHigh + ) + token1_liquidity = self.get_token1_in_pool( + liquidity_total, sqrtPrice, sqrtPriceLow, sqrtPriceHigh + ) + token0_liquidity = token0_liquidity // (10**token0_decimals) + token1_liquidity = token1_liquidity // (10**token1_decimals) + liquidity_per_tick_dict['ticks'].append(tick) + liquidity_per_tick_dict['token0'].append(token0_liquidity) + liquidity_per_tick_dict['token1'].append(token1_liquidity) + + return liquidity_per_tick_dict + # ------ Approval Utils ------------------------------------------------------------ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: """Give an exchange/router max approval of a token."""