Skip to main content

evm_dex_pool/v3/
pool.rs

1use crate::contracts::{IAlgebraPoolSei, IPancakeV3Pool, IUniswapV3Pool};
2use crate::pool::base::{EventApplicable, PoolInterface, PoolType, PoolTypeTrait, TopicList};
3use alloy::primitives::FixedBytes;
4use alloy::primitives::{aliases::U24, Address, Signed, U160, U256};
5use alloy::rpc::types::Log;
6use alloy::sol_types::SolEvent;
7use anyhow::{anyhow, Result};
8use log::{debug, trace};
9use serde::{Deserialize, Serialize};
10use std::any::Any;
11use std::{collections::BTreeMap, fmt};
12
13use super::{v3_swap, Tick, TickMap};
14
15/// The Q64.96 precision used by Uniswap V3
16pub const Q96_U128: u128 = 1 << 96;
17pub const FEE_DENOMINATOR: u32 = 1000000;
18pub const RAMSES_FACTOR: u128 = 10000000000;
19
20/// Enum representing the type of V3 pool
21#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
22pub enum V3PoolType {
23    UniswapV3,
24    PancakeV3,
25    AlgebraV3,
26    RamsesV2,
27    AlgebraTwoSideFee,
28    AlgebraPoolFeeInState,
29}
30
31/// Struct containing V3 pool information including tick data
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct UniswapV3Pool {
34    /// Pool type
35    pub pool_type: V3PoolType,
36    /// Pool address
37    pub address: Address,
38    /// First token address in the pool
39    pub token0: Address,
40    /// Second token address in the pool
41    pub token1: Address,
42    /// Fee tier in the pool in basis points 1000000 = 100%
43    pub fee: U24,
44    /// Tick spacing for this pool
45    pub tick_spacing: i32,
46    /// Current sqrt price (sqrt(token1/token0)) * 2^96
47    pub sqrt_price_x96: U160,
48    /// Current tick
49    pub tick: i32,
50    /// Current liquidity
51    pub liquidity: u128,
52    /// Mapping of initialized ticks
53    pub ticks: TickMap,
54    /// Ratio conversion factor
55    pub ratio_conversion_factor: U256,
56    /// Factory address
57    pub factory: Address,
58    /// Last update timestamp
59    pub last_updated: u64,
60    /// Creation timestamp or block
61    pub created_at: u64,
62}
63
64impl UniswapV3Pool {
65    /// Create a new V3 pool
66    pub fn new(
67        address: Address,
68        token0: Address,
69        token1: Address,
70        fee: U24,
71        tick_spacing: i32,
72        sqrt_price_x96: U160,
73        tick: i32,
74        liquidity: u128,
75        factory: Address,
76        pool_type: V3PoolType,
77    ) -> Self {
78        let current_time = chrono::Utc::now().timestamp() as u64;
79        Self {
80            pool_type,
81            address,
82            token0,
83            token1,
84            fee,
85            tick_spacing,
86            sqrt_price_x96,
87            tick,
88            liquidity,
89            ticks: BTreeMap::new(),
90            last_updated: current_time,
91            created_at: current_time,
92            ratio_conversion_factor: U256::from(RAMSES_FACTOR),
93            factory,
94        }
95    }
96
97    pub fn update_ratio_conversion_factor(&mut self, factor: U256) {
98        self.ratio_conversion_factor = factor;
99    }
100
101    /// Update pool state based on swap event
102    pub fn update_state(&mut self, sqrt_price_x96: U160, tick: i32, liquidity: u128) -> Result<()> {
103        if sqrt_price_x96 == U160::ZERO {
104            return Err(anyhow!("Invalid sqrt_price_x96: zero"));
105        }
106        if tick < -887272 || tick > 887272 {
107            return Err(anyhow!("Invalid tick: {} out of bounds", tick));
108        }
109        self.sqrt_price_x96 = sqrt_price_x96;
110        self.tick = tick;
111        self.liquidity = liquidity;
112        self.last_updated = chrono::Utc::now().timestamp() as u64;
113        Ok(())
114    }
115
116    /// Add or update a tick
117    pub fn update_tick(
118        &mut self,
119        index: i32,
120        liquidity_net: i128,
121        liquidity_gross: u128,
122    ) -> Result<()> {
123        if liquidity_gross == 0 {
124            self.ticks.remove(&index);
125        } else {
126            let tick = Tick {
127                index,
128                liquidity_net,
129                liquidity_gross,
130            };
131            self.ticks.insert(index, tick);
132        }
133        Ok(())
134    }
135
136    /// Get the price of token1 in terms of token0 from sqrt_price_x96
137    pub fn get_price_from_sqrt_price(&self) -> Result<f64> {
138        let sqrt_price: f64 = self.sqrt_price_x96.to::<u128>() as f64 / Q96_U128 as f64;
139        Ok(sqrt_price * sqrt_price)
140    }
141
142    /// Calculate the amount of token1 for a given amount of token0
143    fn calculate_zero_for_one(&self, amount: U256, is_exact_input: bool) -> Result<U256> {
144        let amount_specified = if is_exact_input {
145            Signed::from_raw(amount)
146        } else {
147            Signed::from_raw(amount).saturating_neg()
148        };
149        let swap_state = v3_swap(
150            self.fee,
151            self.sqrt_price_x96,
152            self.tick,
153            self.liquidity,
154            &self.ticks,
155            true,
156            amount_specified,
157            None,
158        )?;
159        if !swap_state.amount_specified_remaining.is_zero() {
160            return Err(anyhow!(
161                "Amount specified remaining: {}",
162                swap_state.amount_specified_remaining
163            ));
164        }
165        Ok(swap_state.amount_calculated.abs().into_raw())
166    }
167
168    /// Calculate the amount of token0 for a given amount of token1 (exact input)
169    fn calculate_one_for_zero(&self, amount: U256, is_exact_input: bool) -> Result<U256> {
170        let amount_specified = if is_exact_input {
171            Signed::from_raw(amount)
172        } else {
173            Signed::from_raw(amount).saturating_neg()
174        };
175        let swap_state = v3_swap(
176            self.fee,
177            self.sqrt_price_x96,
178            self.tick,
179            self.liquidity,
180            &self.ticks,
181            false,
182            amount_specified,
183            None,
184        )?;
185        if !swap_state.amount_specified_remaining.is_zero() {
186            return Err(anyhow!(
187                "Amount specified remaining: {}",
188                swap_state.amount_specified_remaining
189            ));
190        }
191        Ok(swap_state.amount_calculated.abs().into_raw())
192    }
193
194    /// Get the adjacent initialized ticks for a given tick
195    pub fn get_adjacent_ticks(&self, tick: i32) -> (Option<&Tick>, Option<&Tick>) {
196        let below = self.ticks.range(..tick).next_back().map(|(_, tick)| tick);
197        let above = self.ticks.range(tick..).next().map(|(_, tick)| tick);
198        (below, above)
199    }
200
201    /// Check if the pool has sufficient liquidity
202    pub fn has_sufficient_liquidity(&self) -> bool {
203        self.liquidity != 0 && !self.ticks.is_empty()
204    }
205
206    /// Calculate the amount out for a swap with the exact formula
207    pub fn calculate_exact_input(&self, token_in: &Address, amount_in: U256) -> Result<U256> {
208        let result;
209        if token_in == &self.token0 {
210            result = self.calculate_zero_for_one(amount_in, true)?;
211        } else if token_in == &self.token1 {
212            result = self.calculate_one_for_zero(amount_in, true)?;
213        } else {
214            return Err(anyhow!("Token not in pool"));
215        }
216        if self.pool_type == V3PoolType::RamsesV2 {
217            Ok(result * self.ratio_conversion_factor / U256::from(RAMSES_FACTOR))
218        } else {
219            Ok(result)
220        }
221    }
222
223    /// Calculate the amount out for a swap with the exact formula
224    pub fn calculate_exact_output(&self, token_out: &Address, amount_in: U256) -> Result<U256> {
225        if token_out == &self.token0 {
226            self.calculate_one_for_zero(amount_in, false)
227        } else if token_out == &self.token1 {
228            self.calculate_zero_for_one(amount_in, false)
229        } else {
230            Err(anyhow!("Token not in pool"))
231        }
232    }
233
234    /// Apply a swap to the pool, updating the internal state
235    fn apply_swap_internal(
236        &mut self,
237        token_in: &Address,
238        _amount_in: U256,
239        _amount_out: U256,
240    ) -> Result<()> {
241        self.last_updated = chrono::Utc::now().timestamp() as u64;
242
243        if !self.contains_token(token_in) {
244            return Err(anyhow!("Token not in pool"));
245        }
246
247        Ok(())
248    }
249
250    /// Convert a tick to its corresponding word index in the tick bitmap
251    pub fn tick_to_word(&self, tick: i32) -> i32 {
252        let compressed = tick / self.tick_spacing;
253        let compressed = if tick < 0 && tick % self.tick_spacing != 0 {
254            compressed - 1
255        } else {
256            compressed
257        };
258        compressed >> 8
259    }
260
261    /// Helper for applying burn events (R6 refactoring: dedup V3 burn handling)
262    fn apply_burn_event(&mut self, tick_lower: i32, tick_upper: i32, amount: u128) -> Result<()> {
263        if tick_lower >= tick_upper {
264            return Err(anyhow!(
265                "Invalid tick range: tick_lower {} >= tick_upper {}",
266                tick_lower,
267                tick_upper
268            ));
269        }
270
271        // Update tick_lower
272        if let Some(tick) = self.ticks.get_mut(&tick_lower) {
273            let liquidity_net = tick.liquidity_net;
274            tick.liquidity_net = tick.liquidity_net.saturating_sub(amount as i128);
275            tick.liquidity_gross = tick.liquidity_gross.saturating_sub(amount);
276            if tick.liquidity_gross == 0 {
277                self.update_tick(tick_lower, liquidity_net, 0)?;
278            }
279        } else {
280            return Err(anyhow!(
281                "Burn attempted on uninitialized tick_lower: {}",
282                tick_lower
283            ));
284        }
285
286        // Update tick_upper
287        if let Some(tick) = self.ticks.get_mut(&tick_upper) {
288            let liquidity_net = tick.liquidity_net;
289            tick.liquidity_net = tick.liquidity_net.saturating_add(amount as i128);
290            tick.liquidity_gross = tick.liquidity_gross.saturating_sub(amount);
291            if tick.liquidity_gross == 0 {
292                self.update_tick(tick_upper, liquidity_net, 0)?;
293            }
294        } else {
295            return Err(anyhow!(
296                "Burn attempted on uninitialized tick_upper: {}",
297                tick_upper
298            ));
299        }
300
301        // Update pool liquidity if current tick is in range [tick_lower, tick_upper)
302        if self.tick >= tick_lower && self.tick < tick_upper {
303            self.liquidity = self.liquidity.saturating_sub(amount);
304        }
305
306        Ok(())
307    }
308}
309
310impl PoolInterface for UniswapV3Pool {
311    fn calculate_output(&self, token_in: &Address, amount_in: U256) -> Result<U256> {
312        self.calculate_exact_input(token_in, amount_in)
313    }
314
315    fn calculate_input(&self, token_out: &Address, amount_out: U256) -> Result<U256> {
316        self.calculate_exact_output(token_out, amount_out)
317    }
318
319    fn apply_swap(&mut self, token_in: &Address, amount_in: U256, amount_out: U256) -> Result<()> {
320        self.apply_swap_internal(token_in, amount_in, amount_out)
321    }
322
323    fn address(&self) -> Address {
324        self.address
325    }
326
327    fn tokens(&self) -> (Address, Address) {
328        (self.token0, self.token1)
329    }
330
331    fn fee(&self) -> f64 {
332        self.fee.to::<u128>() as f64 / FEE_DENOMINATOR as f64
333    }
334
335    fn fee_raw(&self) -> u64 {
336        self.fee.to::<u128>() as u64
337    }
338
339    fn id(&self) -> String {
340        format!(
341            "v3-{}-{}-{}-{}",
342            self.address,
343            self.token0,
344            self.token1,
345            self.fee.to::<u128>()
346        )
347    }
348
349    fn log_summary(&self) -> String {
350        format!(
351            "V3 Pool {} - {} <> {} (fee: {:.2}%, tick: {}, liquidity: {}, sqrt_price_x96: {}, ticks: {})",
352            self.address, self.token0, self.token1, self.fee, self.tick, self.liquidity, self.sqrt_price_x96, self.ticks.len()
353        )
354    }
355
356    fn contains_token(&self, token: &Address) -> bool {
357        *token == self.token0 || *token == self.token1
358    }
359
360    fn clone_box(&self) -> Box<dyn PoolInterface + Send + Sync> {
361        Box::new(self.clone())
362    }
363
364    fn as_any(&self) -> &dyn Any {
365        self
366    }
367
368    fn as_any_mut(&mut self) -> &mut dyn Any {
369        self
370    }
371}
372
373impl EventApplicable for UniswapV3Pool {
374    fn apply_log(&mut self, log: &Log) -> Result<()> {
375        match log.topic0() {
376            Some(&IUniswapV3Pool::Swap::SIGNATURE_HASH) => {
377                let swap_data: IUniswapV3Pool::Swap = log.log_decode()?.inner.data;
378                debug!(
379                    "Applying V3Swap event to pool {}: sqrt_price_x96={}, tick={}, liquidity={}",
380                    self.address, swap_data.sqrtPriceX96, swap_data.tick, swap_data.liquidity
381                );
382                self.update_state(
383                    swap_data.sqrtPriceX96,
384                    swap_data.tick.as_i32(),
385                    swap_data.liquidity,
386                )
387            }
388            Some(&IPancakeV3Pool::Swap::SIGNATURE_HASH) => {
389                let swap_data: IPancakeV3Pool::Swap = log.log_decode()?.inner.data;
390                debug!(
391                    "Applying V3Swap event to pool {}: sqrt_price_x96={}, tick={}, liquidity={}",
392                    self.address, swap_data.sqrtPriceX96, swap_data.tick, swap_data.liquidity
393                );
394                self.update_state(
395                    swap_data.sqrtPriceX96,
396                    swap_data.tick.as_i32(),
397                    swap_data.liquidity,
398                )
399            }
400            Some(&IAlgebraPoolSei::Swap::SIGNATURE_HASH) => {
401                let swap_data: IAlgebraPoolSei::Swap = log.log_decode()?.inner.data;
402                debug!(
403                    "Applying AlgebraSwap event to pool {}: sqrt_price_x96={}, tick={}, liquidity={}",
404                    self.address, swap_data.price, swap_data.tick, swap_data.liquidity
405                );
406                self.update_state(
407                    swap_data.price,
408                    swap_data.tick.as_i32(),
409                    swap_data.liquidity,
410                )
411            }
412            Some(&IUniswapV3Pool::Mint::SIGNATURE_HASH) => {
413                let mint_data: IUniswapV3Pool::Mint = log.log_decode()?.inner.data;
414                debug!(
415                    "Applying V3Mint event to pool {}: tick_lower={}, tick_upper={}, amount={}",
416                    self.address, mint_data.tickLower, mint_data.tickUpper, mint_data.amount
417                );
418
419                let amount_u128 = mint_data.amount;
420                let tick_lower_i32 = mint_data.tickLower.as_i32();
421                let tick_upper_i32 = mint_data.tickUpper.as_i32();
422
423                if tick_lower_i32 >= tick_upper_i32 {
424                    return Err(anyhow!(
425                        "Invalid tick range: tick_lower {} >= tick_upper {}",
426                        tick_lower_i32,
427                        tick_upper_i32
428                    ));
429                }
430
431                // Update tick_lower
432                if let Some(tick) = self.ticks.get_mut(&tick_lower_i32) {
433                    tick.liquidity_net = tick.liquidity_net.saturating_add(amount_u128 as i128);
434                    tick.liquidity_gross = tick.liquidity_gross.saturating_add(amount_u128);
435                } else {
436                    self.update_tick(tick_lower_i32, amount_u128 as i128, amount_u128)?;
437                }
438
439                // Update tick_upper
440                if let Some(tick) = self.ticks.get_mut(&tick_upper_i32) {
441                    tick.liquidity_net = tick.liquidity_net.saturating_sub(amount_u128 as i128);
442                    tick.liquidity_gross = tick.liquidity_gross.saturating_add(amount_u128);
443                } else {
444                    self.update_tick(tick_upper_i32, -(amount_u128 as i128), amount_u128)?;
445                }
446
447                // Update pool liquidity if current tick is in range [tick_lower, tick_upper)
448                if self.tick >= tick_lower_i32 && self.tick < tick_upper_i32 {
449                    self.liquidity = self.liquidity.saturating_add(amount_u128);
450                }
451
452                Ok(())
453            }
454            // R6: Deduplicated burn event handling
455            Some(&IUniswapV3Pool::Burn::SIGNATURE_HASH) => {
456                let burn_data: IUniswapV3Pool::Burn = log.log_decode()?.inner.data;
457                debug!(
458                    "Applying V3Burn event to pool {}: tick_lower={}, tick_upper={}, amount={}",
459                    self.address, burn_data.tickLower, burn_data.tickUpper, burn_data.amount
460                );
461                self.apply_burn_event(
462                    burn_data.tickLower.as_i32(),
463                    burn_data.tickUpper.as_i32(),
464                    burn_data.amount,
465                )
466            }
467            Some(&IAlgebraPoolSei::Burn::SIGNATURE_HASH) => {
468                let burn_data: IAlgebraPoolSei::Burn = log.log_decode()?.inner.data;
469                debug!(
470                    "Applying AlgebraBurn event to pool {}: tick_lower={}, tick_upper={}, amount={}",
471                    self.address,
472                    burn_data.bottomTick,
473                    burn_data.topTick,
474                    burn_data.liquidityAmount
475                );
476                self.apply_burn_event(
477                    burn_data.bottomTick.as_i32(),
478                    burn_data.topTick.as_i32(),
479                    burn_data.liquidityAmount,
480                )
481            }
482            _ => {
483                trace!("Ignoring non-V3 event for V3 pool");
484                Ok(())
485            }
486        }
487    }
488}
489
490impl TopicList for UniswapV3Pool {
491    fn topics() -> Vec<FixedBytes<32>> {
492        vec![
493            IUniswapV3Pool::Swap::SIGNATURE_HASH,
494            IUniswapV3Pool::Mint::SIGNATURE_HASH,
495            IUniswapV3Pool::Burn::SIGNATURE_HASH,
496            IPancakeV3Pool::Swap::SIGNATURE_HASH,
497            IAlgebraPoolSei::Swap::SIGNATURE_HASH,
498            IAlgebraPoolSei::Burn::SIGNATURE_HASH,
499        ]
500    }
501
502    fn profitable_topics() -> Vec<FixedBytes<32>> {
503        vec![
504            IUniswapV3Pool::Swap::SIGNATURE_HASH,
505            IPancakeV3Pool::Swap::SIGNATURE_HASH,
506            IAlgebraPoolSei::Swap::SIGNATURE_HASH,
507        ]
508    }
509}
510
511impl fmt::Display for UniswapV3Pool {
512    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513        write!(
514            f,
515            "V3 Pool {} - {} <> {} (fee: {:.2}%, tick: {}, liquidity: {})",
516            self.address,
517            self.token0,
518            self.token1,
519            (self.fee.to::<u128>() as f64 / FEE_DENOMINATOR as f64) * 100.0,
520            self.tick,
521            self.liquidity
522        )
523    }
524}
525
526impl PoolTypeTrait for UniswapV3Pool {
527    fn pool_type(&self) -> PoolType {
528        PoolType::UniswapV3
529    }
530}