wp_evm_v3_provider/
populate_ticks.rs1use alloy_primitives::{aliases::I24, Address, U256};
2use alloy_provider::{network::Ethereum, Provider};
3use alloy_sol_types::sol;
4use anyhow::{Context, Result};
5use wp_evm_v3_core::data::{PoolState, TickInfo};
6
7const MIN_TICK: i32 = -887272;
8const MAX_TICK: i32 = 887272;
9const MULTICALL_CHUNK_SIZE: usize = 500;
10
11sol! {
12 #[sol(rpc)]
13 interface IV3PoolBitmap {
14 function tickBitmap(int16 wordPos) external view returns (uint256);
15 function ticks(int24 tick) external view returns (
16 uint128 liquidityGross,
17 int128 liquidityNet,
18 uint256 feeGrowthOutside0X128,
19 uint256 feeGrowthOutside1X128,
20 int56 tickCumulativeOutside,
21 uint160 secondsPerLiquidityOutsideX128,
22 uint32 secondsOutside,
23 bool initialized
24 );
25 }
26}
27
28#[allow(async_fn_in_trait)]
30pub trait TickBitmapSource {
31 async fn fetch_bitmap_words<P: Provider<Ethereum>>(
33 &self,
34 provider: &P,
35 pool: Address,
36 word_positions: &[i16],
37 ) -> Result<Vec<U256>>;
38
39 async fn fetch_tick_rows<P: Provider<Ethereum>>(
41 &self,
42 provider: &P,
43 pool: Address,
44 ticks: &[i32],
45 ) -> Result<Vec<(u128, i128)>>;
46}
47
48pub struct NormalizedTickInfo {
50 pub tick: i32,
51 pub liquidity_gross: u128,
52 pub liquidity_net: i128,
53}
54
55pub trait TickStateSink {
57 fn tick_spacing(&self) -> i32;
58 fn set_ticks(&mut self, ticks: Vec<NormalizedTickInfo>);
59}
60
61impl TickStateSink for PoolState {
62 fn tick_spacing(&self) -> i32 {
63 self.tick_spacing
64 }
65
66 fn set_ticks(&mut self, ticks: Vec<NormalizedTickInfo>) {
67 self.ticks = ticks
68 .into_iter()
69 .map(|tick| TickInfo {
70 tick: tick.tick,
71 liquidity_gross: tick.liquidity_gross,
72 liquidity_net: tick.liquidity_net,
73 })
74 .collect();
75 }
76}
77
78struct V3TickSource;
79
80impl TickBitmapSource for V3TickSource {
81 async fn fetch_bitmap_words<P: Provider<Ethereum>>(
82 &self,
83 provider: &P,
84 pool: Address,
85 word_positions: &[i16],
86 ) -> Result<Vec<U256>> {
87 let contract = IV3PoolBitmap::new(pool, provider);
88 let mut mc = provider.multicall().dynamic::<IV3PoolBitmap::tickBitmapCall>();
89 for &word_pos in word_positions {
90 mc = mc.add_dynamic(contract.tickBitmap(word_pos));
91 }
92 mc.aggregate().await.context("populate_ticks phase1 multicall(tickBitmap)")
93 }
94
95 async fn fetch_tick_rows<P: Provider<Ethereum>>(
96 &self,
97 provider: &P,
98 pool: Address,
99 ticks: &[i32],
100 ) -> Result<Vec<(u128, i128)>> {
101 let contract = IV3PoolBitmap::new(pool, provider);
102 let mut mc = provider.multicall().dynamic::<IV3PoolBitmap::ticksCall>();
103 for &tick in ticks {
104 let tick_i24 = I24::try_from(tick).context("tick index out of i24 range")?;
105 mc = mc.add_dynamic(contract.ticks(tick_i24));
106 }
107
108 let rows = mc.aggregate().await.context("populate_ticks phase2 multicall(ticks)")?;
109 Ok(rows.into_iter().map(|row| (row.liquidityGross, row.liquidityNet)).collect())
110 }
111}
112
113fn word_range(tick_spacing: i32) -> std::ops::RangeInclusive<i16> {
114 let compressed_min = MIN_TICK.div_euclid(tick_spacing);
115 let compressed_max = MAX_TICK.div_euclid(tick_spacing);
116 let word_min = compressed_min.div_euclid(256) as i16;
117 let word_max = compressed_max.div_euclid(256) as i16;
118 word_min..=word_max
119}
120
121fn decompose_bitmap_word(word_pos: i16, word: U256, tick_spacing: i32) -> Vec<i32> {
122 let mut out = Vec::new();
123 for bit_pos in 0u32..256 {
124 if word.bit(bit_pos as usize) {
125 let compressed = (word_pos as i32) * 256 + (bit_pos as i32);
126 out.push(compressed * tick_spacing);
127 }
128 }
129 out
130}
131
132pub async fn populate_ticks_with<P, S, T>(
142 provider: &P,
143 pool: Address,
144 source: &S,
145 state: &mut T,
146) -> Result<()>
147where
148 P: Provider<Ethereum>,
149 S: TickBitmapSource,
150 T: TickStateSink,
151{
152 let tick_spacing = state.tick_spacing();
153 if tick_spacing <= 0 {
154 anyhow::bail!("invalid tick_spacing {}; expected > 0", tick_spacing);
155 }
156
157 let word_positions: Vec<i16> = word_range(tick_spacing).collect();
159 let mut initialized_ticks = Vec::<i32>::new();
160
161 for chunk in word_positions.chunks(MULTICALL_CHUNK_SIZE) {
162 let words = source.fetch_bitmap_words(provider, pool, chunk).await?;
163
164 for (&word_pos, word) in chunk.iter().zip(words.into_iter()) {
165 if !word.is_zero() {
166 initialized_ticks.extend(decompose_bitmap_word(word_pos, word, tick_spacing));
167 }
168 }
169 }
170
171 initialized_ticks.sort_unstable();
172
173 let mut tick_infos = Vec::<NormalizedTickInfo>::with_capacity(initialized_ticks.len());
175 for chunk in initialized_ticks.chunks(MULTICALL_CHUNK_SIZE) {
176 let rows = source.fetch_tick_rows(provider, pool, chunk).await?;
177
178 for (&tick, (liquidity_gross, liquidity_net)) in chunk.iter().zip(rows.into_iter()) {
179 if liquidity_gross > 0 && liquidity_net != 0 {
180 tick_infos.push(NormalizedTickInfo { tick, liquidity_gross, liquidity_net });
181 }
182 }
183 }
184
185 let sum_net: i128 = tick_infos.iter().map(|t| t.liquidity_net).sum();
186 if sum_net != 0 {
187 anyhow::bail!("populate_ticks invariant failed: sum(liquidity_net)={sum_net}, expected 0");
188 }
189
190 state.set_ticks(tick_infos);
191 Ok(())
192}
193
194#[tracing::instrument(skip_all, fields(pool = %pool, tick_spacing = state.tick_spacing), err)]
196pub async fn populate_ticks<P: Provider<Ethereum>>(
197 provider: &P,
198 pool: Address,
199 state: &mut PoolState,
200) -> Result<()> {
201 populate_ticks_with(provider, pool, &V3TickSource, state).await
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn bitmap_word_decomposition_recovers_ticks() {
210 let mut word = U256::ZERO;
211 word |= U256::from(1u8) << 0;
212 word |= U256::from(1u8) << 3;
213 let ticks = decompose_bitmap_word(2, word, 60);
214 assert_eq!(ticks, vec![2 * 256 * 60, (2 * 256 + 3) * 60]);
215 }
216}