1use crate::contracts::ILBPair;
4use crate::lb::math::*;
5use crate::pool::base::{
6 EventApplicable, PoolInterface, PoolType, PoolTypeTrait, Topic, TopicList,
7};
8use alloy::primitives::{Address, U256};
9use alloy::rpc::types::Log;
10use alloy::sol_types::SolEvent;
11use anyhow::{anyhow, Result};
12use log::trace;
13use serde::{Deserialize, Serialize};
14use std::any::Any;
15use std::collections::BTreeMap;
16use std::fmt;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct LBPool {
24 pub address: Address,
25 pub token_x: Address,
26 pub token_y: Address,
27 pub bin_step: u16,
28 pub active_id: u32,
29 pub bins: BTreeMap<u32, (u128, u128)>,
31
32 pub base_factor: u16,
34 pub filter_period: u16,
35 pub decay_period: u16,
36 pub reduction_factor: u16,
37 pub variable_fee_control: u32,
38 pub protocol_share: u16,
39 pub max_volatility_accumulator: u32,
40
41 pub volatility_accumulator: u32,
43 #[serde(default)]
44 pub volatility_reference: u32,
45 #[serde(default)]
46 pub id_reference: u32,
47 #[serde(default)]
48 pub time_of_last_update: u64,
49
50 pub last_updated: u64,
51 pub created_at: u64,
52}
53
54impl LBPool {
55 #[allow(clippy::too_many_arguments)]
56 pub fn new(
57 address: Address,
58 token_x: Address,
59 token_y: Address,
60 bin_step: u16,
61 active_id: u32,
62 bins: BTreeMap<u32, (u128, u128)>,
63 base_factor: u16,
64 filter_period: u16,
65 decay_period: u16,
66 reduction_factor: u16,
67 variable_fee_control: u32,
68 protocol_share: u16,
69 max_volatility_accumulator: u32,
70 volatility_accumulator: u32,
71 volatility_reference: u32,
72 id_reference: u32,
73 time_of_last_update: u64,
74 ) -> Self {
75 let now = chrono::Utc::now().timestamp() as u64;
76 Self {
77 address,
78 token_x,
79 token_y,
80 bin_step,
81 active_id,
82 bins,
83 base_factor,
84 filter_period,
85 decay_period,
86 reduction_factor,
87 variable_fee_control,
88 protocol_share,
89 max_volatility_accumulator,
90 volatility_accumulator,
91 volatility_reference,
92 id_reference,
93 time_of_last_update,
94 last_updated: now,
95 created_at: now,
96 }
97 }
98
99 pub fn get_total_fee(&self) -> u128 {
101 get_total_fee(
102 self.base_factor,
103 self.bin_step,
104 self.volatility_accumulator,
105 self.variable_fee_control,
106 )
107 }
108
109 pub fn fee_f64(&self) -> f64 {
111 self.get_total_fee() as f64 / PRECISION as f64
112 }
113
114 fn next_non_empty_bin(&self, swap_for_y: bool, id: u32) -> Option<u32> {
119 if swap_for_y {
120 self.bins.range(..id).next_back().map(|(&k, _)| k)
121 } else {
122 self.bins.range((id + 1)..).next().map(|(&k, _)| k)
123 }
124 }
125
126 pub fn update_bin(&mut self, id: u32, reserve_x: u128, reserve_y: u128) {
128 if reserve_x == 0 && reserve_y == 0 {
129 self.bins.remove(&id);
130 } else {
131 self.bins.insert(id, (reserve_x, reserve_y));
132 }
133 }
134
135 fn update_references(&self, timestamp: u64) -> (u32, u32) {
139 let dt = timestamp.saturating_sub(self.time_of_last_update);
140 let mut vol_ref = self.volatility_reference;
141 let mut id_ref = self.id_reference;
142
143 if dt >= self.filter_period as u64 {
144 id_ref = self.active_id;
146
147 if dt < self.decay_period as u64 {
148 vol_ref = ((self.volatility_accumulator as u64
150 * self.reduction_factor as u64)
151 / 10_000) as u32;
152 } else {
153 vol_ref = 0;
155 }
156 }
157
158 (vol_ref, id_ref)
159 }
160
161 fn compute_volatility_accumulator(&self, id: u32, vol_ref: u32, id_ref: u32) -> u32 {
165 let delta_id = id.abs_diff(id_ref);
166 let vol_acc = vol_ref as u64 + delta_id as u64 * 10_000;
167 vol_acc.min(self.max_volatility_accumulator as u64) as u32
168 }
169
170 pub fn simulate_swap_out(
175 &self,
176 amount_in: u128,
177 swap_for_y: bool,
178 ) -> Result<(u128, u128, u128)> {
179 let timestamp = chrono::Utc::now().timestamp() as u64;
180 self.simulate_swap_out_at(amount_in, swap_for_y, timestamp)
181 }
182
183 pub fn simulate_swap_out_at(
188 &self,
189 amount_in: u128,
190 swap_for_y: bool,
191 timestamp: u64,
192 ) -> Result<(u128, u128, u128)> {
193 let mut amount_in_left = amount_in;
194 let mut amount_out: u128 = 0;
195 let mut total_fee: u128 = 0;
196 let mut id = self.active_id;
197
198 let (vol_ref, id_ref) = self.update_references(timestamp);
200
201 loop {
202 let bin_reserves = self.bins.get(&id);
203
204 if let Some(&(rx, ry)) = bin_reserves {
205 let bin_reserve_out = if swap_for_y { ry } else { rx };
206
207 if bin_reserve_out > 0 {
208 let vol_acc = self.compute_volatility_accumulator(id, vol_ref, id_ref);
210 let fee = get_total_fee(
211 self.base_factor,
212 self.bin_step,
213 vol_acc,
214 self.variable_fee_control,
215 );
216
217 let price = get_price_from_id(id, self.bin_step);
218
219 let max_amount_in = if swap_for_y {
220 shift_div_round_up(U256::from(bin_reserve_out), SCALE_OFFSET, price)
221 .to::<u128>()
222 } else {
223 mul_shift_round_up(U256::from(bin_reserve_out), price, SCALE_OFFSET)
224 .to::<u128>()
225 };
226
227 let max_fee = get_fee_amount(max_amount_in, fee);
228 let max_amount_in_with_fees = max_amount_in.saturating_add(max_fee);
229
230 let (amount_in_bin, fee_bin, amount_out_bin);
231
232 if amount_in_left >= max_amount_in_with_fees {
233 amount_in_bin = max_amount_in_with_fees;
234 fee_bin = max_fee;
235 amount_out_bin = bin_reserve_out;
236 } else {
237 fee_bin = get_fee_amount_from(amount_in_left, fee);
238 let amount_in_no_fee = amount_in_left - fee_bin;
239 amount_in_bin = amount_in_left;
240
241 amount_out_bin = if swap_for_y {
242 mul_shift_round_down(U256::from(amount_in_no_fee), price, SCALE_OFFSET)
243 .to::<u128>()
244 .min(bin_reserve_out)
245 } else {
246 shift_div_round_down(U256::from(amount_in_no_fee), SCALE_OFFSET, price)
247 .to::<u128>()
248 .min(bin_reserve_out)
249 };
250 }
251
252 amount_in_left -= amount_in_bin;
253 amount_out += amount_out_bin;
254 total_fee += fee_bin;
255 }
256 }
257
258 if amount_in_left == 0 {
259 break;
260 }
261
262 match self.next_non_empty_bin(swap_for_y, id) {
263 Some(next_id) => id = next_id,
264 None => break,
265 }
266 }
267
268 Ok((amount_in_left, amount_out, total_fee))
269 }
270
271 pub fn simulate_swap_in(
275 &self,
276 amount_out: u128,
277 swap_for_y: bool,
278 ) -> Result<(u128, u128, u128)> {
279 let timestamp = chrono::Utc::now().timestamp() as u64;
280 self.simulate_swap_in_at(amount_out, swap_for_y, timestamp)
281 }
282
283 pub fn simulate_swap_in_at(
287 &self,
288 amount_out: u128,
289 swap_for_y: bool,
290 timestamp: u64,
291 ) -> Result<(u128, u128, u128)> {
292 let mut amount_out_left = amount_out;
293 let mut amount_in: u128 = 0;
294 let mut total_fee: u128 = 0;
295 let mut id = self.active_id;
296
297 let (vol_ref, id_ref) = self.update_references(timestamp);
298
299 loop {
300 let bin_reserves = self.bins.get(&id);
301
302 if let Some(&(rx, ry)) = bin_reserves {
303 let bin_reserve_out = if swap_for_y { ry } else { rx };
304
305 if bin_reserve_out > 0 {
306 let price = get_price_from_id(id, self.bin_step);
307 let amount_out_of_bin = bin_reserve_out.min(amount_out_left);
308
309 let vol_acc = self.compute_volatility_accumulator(id, vol_ref, id_ref);
310 let fee = get_total_fee(
311 self.base_factor,
312 self.bin_step,
313 vol_acc,
314 self.variable_fee_control,
315 );
316
317 let amount_in_without_fee = if swap_for_y {
318 shift_div_round_up(U256::from(amount_out_of_bin), SCALE_OFFSET, price)
319 .to::<u128>()
320 } else {
321 mul_shift_round_up(U256::from(amount_out_of_bin), price, SCALE_OFFSET)
322 .to::<u128>()
323 };
324
325 let fee_amount = get_fee_amount(amount_in_without_fee, fee);
326
327 amount_in += amount_in_without_fee + fee_amount;
328 amount_out_left -= amount_out_of_bin;
329 total_fee += fee_amount;
330 }
331 }
332
333 if amount_out_left == 0 {
334 break;
335 }
336
337 match self.next_non_empty_bin(swap_for_y, id) {
338 Some(next_id) => id = next_id,
339 None => break,
340 }
341 }
342
343 Ok((amount_in, amount_out_left, total_fee))
344 }
345}
346
347impl PoolInterface for LBPool {
350 fn calculate_output(&self, token_in: &Address, amount_in: U256) -> Result<U256> {
351 let swap_for_y = if token_in == &self.token_x {
352 true
353 } else if token_in == &self.token_y {
354 false
355 } else {
356 return Err(anyhow!(
357 "Token {} not in LB pool {}",
358 token_in,
359 self.address
360 ));
361 };
362
363 let amount_in_128: u128 = amount_in
364 .try_into()
365 .map_err(|_| anyhow!("Amount too large for LB pool (exceeds u128)"))?;
366
367 let (amount_in_left, amount_out, _fee) =
368 self.simulate_swap_out(amount_in_128, swap_for_y)?;
369 if amount_in_left > 0 {
370 return Err(anyhow!(
371 "Insufficient liquidity in LB pool: {} of {} input remaining",
372 amount_in_left,
373 amount_in_128
374 ));
375 }
376 Ok(U256::from(amount_out))
377 }
378
379 fn calculate_input(&self, token_out: &Address, amount_out: U256) -> Result<U256> {
380 let swap_for_y = if token_out == &self.token_y {
381 true
382 } else if token_out == &self.token_x {
383 false
384 } else {
385 return Err(anyhow!(
386 "Token {} not in LB pool {}",
387 token_out,
388 self.address
389 ));
390 };
391
392 let amount_out_128: u128 = amount_out
393 .try_into()
394 .map_err(|_| anyhow!("Amount too large for LB pool (exceeds u128)"))?;
395
396 let (amount_in, amount_out_left, _fee) =
397 self.simulate_swap_in(amount_out_128, swap_for_y)?;
398 if amount_out_left > 0 {
399 return Err(anyhow!(
400 "Insufficient liquidity in LB pool: {} of {} output remaining",
401 amount_out_left,
402 amount_out_128
403 ));
404 }
405 Ok(U256::from(amount_in))
406 }
407
408 fn apply_swap(
409 &mut self,
410 _token_in: &Address,
411 _amount_in: U256,
412 _amount_out: U256,
413 ) -> Result<()> {
414 self.last_updated = chrono::Utc::now().timestamp() as u64;
416 Ok(())
417 }
418
419 fn address(&self) -> Address {
420 self.address
421 }
422
423 fn tokens(&self) -> (Address, Address) {
424 (self.token_x, self.token_y)
425 }
426
427 fn fee(&self) -> f64 {
428 self.fee_f64()
429 }
430
431 fn fee_raw(&self) -> u64 {
432 let fee_1e18 = self.get_total_fee();
435 (fee_1e18 / 1_000_000_000_000) as u64
437 }
438
439 fn id(&self) -> String {
440 format!("lb-{:?}-{}", self.address, self.bin_step)
441 }
442
443 fn contains_token(&self, token: &Address) -> bool {
444 *token == self.token_x || *token == self.token_y
445 }
446
447 fn clone_box(&self) -> Box<dyn PoolInterface + Send + Sync> {
448 Box::new(self.clone())
449 }
450
451 fn log_summary(&self) -> String {
452 format!(
453 "LB Pool {} ({} <> {}, binStep={}, activeId={}, bins={})",
454 self.address,
455 self.token_x,
456 self.token_y,
457 self.bin_step,
458 self.active_id,
459 self.bins.len(),
460 )
461 }
462
463 fn as_any(&self) -> &dyn Any {
464 self
465 }
466
467 fn as_any_mut(&mut self) -> &mut dyn Any {
468 self
469 }
470}
471
472impl EventApplicable for LBPool {
475 fn apply_log(&mut self, event: &Log) -> Result<()> {
476 match event.topic0() {
477 Some(&ILBPair::Swap::SIGNATURE_HASH) => {
478 let swap_data: ILBPair::Swap = event.log_decode()?.inner.data;
479 let id: u32 = swap_data.id.to();
480
481 let (in_x, in_y) = decode_amounts(swap_data.amountsIn);
483 let (out_x, out_y) = decode_amounts(swap_data.amountsOut);
484
485 let (rx, ry) = self.bins.get(&id).copied().unwrap_or((0, 0));
489 let new_rx = rx.saturating_add(in_x).saturating_sub(out_x);
490 let new_ry = ry.saturating_add(in_y).saturating_sub(out_y);
491 self.update_bin(id, new_rx, new_ry);
492
493 self.active_id = id;
496
497 self.volatility_accumulator = swap_data.volatilityAccumulator.to();
499
500 self.id_reference = id;
505 let now = chrono::Utc::now().timestamp() as u64;
506 self.time_of_last_update = now;
507
508 self.last_updated = now;
509 Ok(())
510 }
511 Some(&ILBPair::DepositedToBins::SIGNATURE_HASH) => {
512 let data: ILBPair::DepositedToBins = event.log_decode()?.inner.data;
513 for (i, id_u256) in data.ids.iter().enumerate() {
514 if let Some(amounts_bytes) = data.amounts.get(i) {
515 let id: u32 = (*id_u256).try_into().unwrap_or(u32::MAX);
516 let (add_x, add_y) = decode_amounts(*amounts_bytes);
517 let (rx, ry) = self.bins.get(&id).copied().unwrap_or((0, 0));
518 self.update_bin(id, rx.saturating_add(add_x), ry.saturating_add(add_y));
519 }
520 }
521 self.last_updated = chrono::Utc::now().timestamp() as u64;
522 Ok(())
523 }
524 Some(&ILBPair::WithdrawnFromBins::SIGNATURE_HASH) => {
525 let data: ILBPair::WithdrawnFromBins = event.log_decode()?.inner.data;
526 for (i, id_u256) in data.ids.iter().enumerate() {
527 if let Some(amounts_bytes) = data.amounts.get(i) {
528 let id: u32 = (*id_u256).try_into().unwrap_or(u32::MAX);
529 let (sub_x, sub_y) = decode_amounts(*amounts_bytes);
530 let (rx, ry) = self.bins.get(&id).copied().unwrap_or((0, 0));
531 self.update_bin(id, rx.saturating_sub(sub_x), ry.saturating_sub(sub_y));
532 }
533 }
534 self.last_updated = chrono::Utc::now().timestamp() as u64;
535 Ok(())
536 }
537 Some(&ILBPair::StaticFeeParametersSet::SIGNATURE_HASH) => {
538 let data: ILBPair::StaticFeeParametersSet = event.log_decode()?.inner.data;
539 self.base_factor = data.baseFactor;
540 self.filter_period = data.filterPeriod;
541 self.decay_period = data.decayPeriod;
542 self.reduction_factor = data.reductionFactor;
543 self.variable_fee_control = data.variableFeeControl.to();
544 self.protocol_share = data.protocolShare;
545 self.max_volatility_accumulator = data.maxVolatilityAccumulator.to();
546 Ok(())
547 }
548 _ => {
549 trace!("Ignoring unknown event for LB pool {}", self.address);
550 Ok(())
551 }
552 }
553 }
554}
555
556impl TopicList for LBPool {
559 fn topics() -> Vec<Topic> {
560 vec![
561 ILBPair::Swap::SIGNATURE_HASH,
562 ILBPair::DepositedToBins::SIGNATURE_HASH,
563 ILBPair::WithdrawnFromBins::SIGNATURE_HASH,
564 ILBPair::StaticFeeParametersSet::SIGNATURE_HASH,
565 ]
566 }
567
568 fn profitable_topics() -> Vec<Topic> {
569 vec![ILBPair::Swap::SIGNATURE_HASH]
570 }
571}
572
573impl PoolTypeTrait for LBPool {
576 fn pool_type(&self) -> PoolType {
577 PoolType::TraderJoeLB
578 }
579}
580
581impl fmt::Display for LBPool {
584 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
585 write!(
586 f,
587 "LBPool({}, {}<>{}, binStep={}, activeId={}, bins={})",
588 self.address,
589 self.token_x,
590 self.token_y,
591 self.bin_step,
592 self.active_id,
593 self.bins.len(),
594 )
595 }
596}