From 3c690b4cff69584a91656bc53e2bca16a1f51a8e Mon Sep 17 00:00:00 2001 From: ethzoomer Date: Mon, 12 Feb 2024 07:18:04 -0600 Subject: [PATCH 1/4] feat: add tick data to Lp output --- contracts/LpSugar.vy | 47 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/contracts/LpSugar.vy b/contracts/LpSugar.vy index 1a01b49..23a993f 100644 --- a/contracts/LpSugar.vy +++ b/contracts/LpSugar.vy @@ -14,9 +14,10 @@ MAX_LPS: constant(uint256) = 500 MAX_EPOCHS: constant(uint256) = 200 MAX_REWARDS: constant(uint256) = 16 MAX_POSITIONS: constant(uint256) = 10 +MAX_TICKS: constant(uint256) = 20 WEEK: constant(uint256) = 7 * 24 * 60 * 60 -# Slot0 from V3Pool.sol +# Slot0 from CLPool.sol struct Slot: sqrt_price: uint160 tick: int24 @@ -25,7 +26,7 @@ struct Slot: cardinality_next: uint16 unlocked: bool -# GaugeFees from V3Pool.sol +# GaugeFees from CLPool.sol struct GaugeFees: token0: uint128 token1: uint128 @@ -43,6 +44,19 @@ struct PositionData: unstaked_earned0: uint128 unstaked_earned1: uint128 +# Tick.Info from CLPool.sol +struct TickInfo: + liquidity_gross: uint128 + liquidity_net: int128 + staked_liquidity_net: int128 + fee_growth_outside0: uint256 + fee_growth_outside1: uint256 + reward_growth_outside: uint256 + tick_cumulative_outside: int56 + seconds_per_liquidity_outside: uint160 + seconds_outside: uint32 + initialized: bool + struct Position: id: uint256 # NFT ID on v3, 0 on v2 manager: address # NFT Position Manager on v3, router on v2 @@ -55,6 +69,10 @@ struct Position: tick_upper: int24 # Position upper tick on v3, 0 on v2 alm: bool # True if Position is deposited into ALM on v3, False on v2 +struct Tick: + tick: int24 + liquidity_gross: uint128 + struct Token: token_address: address symbol: String[100] @@ -108,6 +126,7 @@ struct Lp: alm_reserve1: uint256 # ALM token1 reserves on v3, 0 on v2 positions: DynArray[Position, MAX_POSITIONS] + ticks: DynArray[Tick, MAX_TICKS] struct LpEpochReward: token: address @@ -172,6 +191,7 @@ interface IPool: def gaugeFees() -> GaugeFees: view # v3 gauge fees amounts def fee() -> uint24: view # v3 fee level def unstakedFee() -> uint24: view # v3 unstaked fee level + def ticks(_tick: int24) -> TickInfo: view # v3 tick data interface IVoter: def gauges(_pool_addr: address) -> address: view @@ -519,6 +539,8 @@ def _byData(_data: address[3], _token0: address, _token1: address, _account: add }) ) + ticks: DynArray[Tick, MAX_TICKS] = empty(DynArray[Tick, MAX_TICKS]) + return Lp({ lp: _data[1], symbol: pool.symbol(), @@ -556,7 +578,8 @@ def _byData(_data: address[3], _token0: address, _token1: address, _account: add alm_reserve0: 0, alm_reserve1: 0, - positions: positions + positions: positions, + ticks: ticks }) @internal @@ -579,6 +602,7 @@ def _byDataCL(_data: address[3], _token0: address, _token1: address, _account: a emissions_token: address = empty(address) token0: IERC20 = IERC20(_token0) token1: IERC20 = IERC20(_token1) + tick_spacing: int24 = pool.tickSpacing() fee_voting_reward = gauge.feesVotingReward() emissions_token = gauge.rewardToken() @@ -621,6 +645,18 @@ def _byDataCL(_data: address[3], _token0: address, _token1: address, _account: a }) ) + ticks: DynArray[Tick, MAX_TICKS] = empty(DynArray[Tick, MAX_TICKS]) + + # fetch liquidity from the ticks surrounding the current tick + for index in range((-1 * MAX_TICKS / 2), (MAX_TICKS / 2)): + tick: int24 = slot.tick + (index * tick_spacing) + tick_info: TickInfo = pool.ticks(tick) + + ticks.append(Tick({ + tick: tick, + liquidity_gross: tick_info.liquidity_gross + })) + return Lp({ lp: pool.address, symbol: "", @@ -628,7 +664,7 @@ def _byDataCL(_data: address[3], _token0: address, _token1: address, _account: a total_supply: 0, nft: nft.address, - type: pool.tickSpacing(), + type: tick_spacing, tick: slot.tick, price: price, @@ -659,7 +695,8 @@ def _byDataCL(_data: address[3], _token0: address, _token1: address, _account: a alm_reserve0: 0, alm_reserve1: 0, - positions: positions + positions: positions, + ticks: ticks }) @external From 6d4b872db5a5c3780e746f34684a57a0dad721da Mon Sep 17 00:00:00 2001 From: ethzoomer Date: Tue, 13 Feb 2024 19:17:13 -0600 Subject: [PATCH 2/4] feat: create separate tick prices functions --- contracts/LpSugar.vy | 90 +++++++++++++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/contracts/LpSugar.vy b/contracts/LpSugar.vy index 23a993f..36eb6c4 100644 --- a/contracts/LpSugar.vy +++ b/contracts/LpSugar.vy @@ -14,7 +14,7 @@ MAX_LPS: constant(uint256) = 500 MAX_EPOCHS: constant(uint256) = 200 MAX_REWARDS: constant(uint256) = 16 MAX_POSITIONS: constant(uint256) = 10 -MAX_TICKS: constant(uint256) = 20 +MAX_PRICES: constant(uint256) = 20 WEEK: constant(uint256) = 7 * 24 * 60 * 60 # Slot0 from CLPool.sol @@ -69,10 +69,14 @@ struct Position: tick_upper: int24 # Position upper tick on v3, 0 on v2 alm: bool # True if Position is deposited into ALM on v3, False on v2 -struct Tick: - tick: int24 +struct Price: + tick_price: int24 liquidity_gross: uint128 +struct LpPrice: + lp: address + prices: DynArray[Price, MAX_PRICES] + struct Token: token_address: address symbol: String[100] @@ -126,7 +130,6 @@ struct Lp: alm_reserve1: uint256 # ALM token1 reserves on v3, 0 on v2 positions: DynArray[Position, MAX_POSITIONS] - ticks: DynArray[Tick, MAX_TICKS] struct LpEpochReward: token: address @@ -539,8 +542,6 @@ def _byData(_data: address[3], _token0: address, _token1: address, _account: add }) ) - ticks: DynArray[Tick, MAX_TICKS] = empty(DynArray[Tick, MAX_TICKS]) - return Lp({ lp: _data[1], symbol: pool.symbol(), @@ -578,8 +579,7 @@ def _byData(_data: address[3], _token0: address, _token1: address, _account: add alm_reserve0: 0, alm_reserve1: 0, - positions: positions, - ticks: ticks + positions: positions }) @internal @@ -645,18 +645,6 @@ def _byDataCL(_data: address[3], _token0: address, _token1: address, _account: a }) ) - ticks: DynArray[Tick, MAX_TICKS] = empty(DynArray[Tick, MAX_TICKS]) - - # fetch liquidity from the ticks surrounding the current tick - for index in range((-1 * MAX_TICKS / 2), (MAX_TICKS / 2)): - tick: int24 = slot.tick + (index * tick_spacing) - tick_info: TickInfo = pool.ticks(tick) - - ticks.append(Tick({ - tick: tick, - liquidity_gross: tick_info.liquidity_gross - })) - return Lp({ lp: pool.address, symbol: "", @@ -695,8 +683,7 @@ def _byDataCL(_data: address[3], _token0: address, _token1: address, _account: a alm_reserve0: 0, alm_reserve1: 0, - positions: positions, - ticks: ticks + positions: positions }) @external @@ -1009,3 +996,62 @@ def _is_cl_factory(_factory: address) -> (bool): )[1] return len(response) > 0 + +@external +@view +def prices(_limit: uint256, _offset: uint256) -> DynArray[LpPrice, MAX_POOLS]: + """ + @notice Returns a collection of tick price data for pools + @param _limit The max amount of pools to return + @param _offset The amount of pools to skip + @return Array of LpPrice structs + """ + col: DynArray[LpPrice, MAX_POOLS] = empty(DynArray[LpPrice, MAX_POOLS]) + pools: DynArray[address[3], MAX_POOLS] = self._pools() + pools_count: uint256 = len(pools) + + for index in range(_offset, _offset + MAX_POOLS): + if len(col) == _limit or index >= pools_count: + break + + factory: IPoolFactory = IPoolFactory(pools[index][0]) + is_cl_factory: bool = self._is_cl_factory(pools[index][0]) + + if is_cl_factory: + col.append(LpPrice({ + lp: pools[index][1], + prices: self._price(pools[index][1]) + })) + else: + empty_prices: DynArray[Price, MAX_PRICES] = empty(DynArray[Price, MAX_PRICES]) + col.append(LpPrice({ + lp: pools[index][1], + prices: empty_prices + })) + + return col + +@internal +@view +def _price(_pool: address) -> DynArray[Price, MAX_PRICES]: + """ + @notice Returns price data at surrounding ticks for a v3 pool + @param _pool The pool to check price data of + @return Array of Price structs + """ + prices: DynArray[Price, MAX_PRICES] = empty(DynArray[Price, MAX_PRICES]) + pool: IPool = IPool(_pool) + tick_spacing: int24 = pool.tickSpacing() + slot: Slot = pool.slot0() + + # fetch liquidity from the ticks surrounding the current tick + for index in range((-1 * MAX_PRICES / 2), (MAX_PRICES / 2)): + tick: int24 = slot.tick + (index * tick_spacing) + tick_info: TickInfo = pool.ticks(tick) + + prices.append(Price({ + tick_price: tick, + liquidity_gross: tick_info.liquidity_gross + })) + + return prices From fd9e63fcf07d31e034df8418be7c8fef275f1efe Mon Sep 17 00:00:00 2001 From: ethzoomer Date: Wed, 14 Feb 2024 08:25:34 -0600 Subject: [PATCH 3/4] refactor: add individual tick price function --- contracts/LpSugar.vy | 43 +++++++++++-------------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/contracts/LpSugar.vy b/contracts/LpSugar.vy index 36eb6c4..94054e1 100644 --- a/contracts/LpSugar.vy +++ b/contracts/LpSugar.vy @@ -73,10 +73,6 @@ struct Price: tick_price: int24 liquidity_gross: uint128 -struct LpPrice: - lp: address - prices: DynArray[Price, MAX_PRICES] - struct Token: token_address: address symbol: String[100] @@ -999,37 +995,20 @@ def _is_cl_factory(_factory: address) -> (bool): @external @view -def prices(_limit: uint256, _offset: uint256) -> DynArray[LpPrice, MAX_POOLS]: +def price(_pool: address, _factory: address) -> DynArray[Price, MAX_PRICES]: """ - @notice Returns a collection of tick price data for pools - @param _limit The max amount of pools to return - @param _offset The amount of pools to skip - @return Array of LpPrice structs + @notice Returns price data at surrounding ticks for a pool + @param _pool The pool to check price data of + @param _factory The factory of the pool + @return Array of Price structs """ - col: DynArray[LpPrice, MAX_POOLS] = empty(DynArray[LpPrice, MAX_POOLS]) - pools: DynArray[address[3], MAX_POOLS] = self._pools() - pools_count: uint256 = len(pools) - - for index in range(_offset, _offset + MAX_POOLS): - if len(col) == _limit or index >= pools_count: - break - - factory: IPoolFactory = IPoolFactory(pools[index][0]) - is_cl_factory: bool = self._is_cl_factory(pools[index][0]) + is_cl_factory: bool = self._is_cl_factory(_factory) - if is_cl_factory: - col.append(LpPrice({ - lp: pools[index][1], - prices: self._price(pools[index][1]) - })) - else: - empty_prices: DynArray[Price, MAX_PRICES] = empty(DynArray[Price, MAX_PRICES]) - col.append(LpPrice({ - lp: pools[index][1], - prices: empty_prices - })) - - return col + if is_cl_factory: + return self._price(_pool) + else: + empty_prices: DynArray[Price, MAX_PRICES] = empty(DynArray[Price, MAX_PRICES]) + return empty_prices @internal @view From a791b61802572f20f7b3db1c0d1f6eb0c6aa7d96 Mon Sep 17 00:00:00 2001 From: ethzoomer Date: Thu, 15 Feb 2024 05:15:37 -0600 Subject: [PATCH 4/4] clean up prices name and return statement, deployment --- contracts/LpSugar.vy | 5 ++--- readme.md | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/LpSugar.vy b/contracts/LpSugar.vy index 94054e1..ecd3202 100644 --- a/contracts/LpSugar.vy +++ b/contracts/LpSugar.vy @@ -995,7 +995,7 @@ def _is_cl_factory(_factory: address) -> (bool): @external @view -def price(_pool: address, _factory: address) -> DynArray[Price, MAX_PRICES]: +def prices(_pool: address, _factory: address) -> DynArray[Price, MAX_PRICES]: """ @notice Returns price data at surrounding ticks for a pool @param _pool The pool to check price data of @@ -1007,8 +1007,7 @@ def price(_pool: address, _factory: address) -> DynArray[Price, MAX_PRICES]: if is_cl_factory: return self._price(_pool) else: - empty_prices: DynArray[Price, MAX_PRICES] = empty(DynArray[Price, MAX_PRICES]) - return empty_prices + return empty(DynArray[Price, MAX_PRICES]) @internal @view diff --git a/readme.md b/readme.md index 2bf98a2..a8eee2c 100644 --- a/readme.md +++ b/readme.md @@ -35,7 +35,7 @@ Below is the list of datasets we support. ### Liquidity Pools Data -`LpSugar.vy` is deployed at `0x90b18D906aa461a45229877469eD4c6A41c31Da6` +`LpSugar.vy` is deployed at `0x3c971c178Bb0697F3D59CC043855e96f389dF61c` It allows fetching on-chain pools data. The returned data/struct of type `Lp` values represent: