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
15pub const Q96_U128: u128 = 1 << 96;
17pub const FEE_DENOMINATOR: u32 = 1000000;
18pub const RAMSES_FACTOR: u128 = 10000000000;
19
20#[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#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct UniswapV3Pool {
34 pub pool_type: V3PoolType,
36 pub address: Address,
38 pub token0: Address,
40 pub token1: Address,
42 pub fee: U24,
44 pub tick_spacing: i32,
46 pub sqrt_price_x96: U160,
48 pub tick: i32,
50 pub liquidity: u128,
52 pub ticks: TickMap,
54 pub ratio_conversion_factor: U256,
56 pub factory: Address,
58 pub last_updated: u64,
60 pub created_at: u64,
62}
63
64impl UniswapV3Pool {
65 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 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 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 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 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 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 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 pub fn has_sufficient_liquidity(&self) -> bool {
203 self.liquidity != 0 && !self.ticks.is_empty()
204 }
205
206 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 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 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 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 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 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 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 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 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 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 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 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}