1use std::{
12 any::Any,
13 collections::HashMap,
14 time::{SystemTime, UNIX_EPOCH},
15};
16
17use alloy::primitives::U256;
18use num_bigint::{BigUint, ToBigUint};
19use num_traits::Euclid;
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22use tracing::trace;
23use tycho_common::{
24 dto::ProtocolStateDelta,
25 models::token::Token,
26 simulation::{
27 errors::{SimulationError, TransitionError},
28 protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
29 },
30 Bytes,
31};
32
33use crate::evm::{
34 engine_db::{create_engine, SHARED_TYCHO_DB},
35 protocol::{
36 fluid::{v1::constant::RESERVES_RESOLVER, vm},
37 u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
38 utils::add_fee_markup,
39 },
40};
41
42mod constant {
43 use alloy::{hex, primitives::U256};
44
45 pub const MAX_PRICE_DIFF: U256 = U256::from_limbs([5, 0, 0, 0]); pub const MIN_SWAP_LIQUIDITY: U256 = U256::from_limbs([8500, 0, 0, 0]); pub const SIX_DECIMALS: U256 = U256::from_limbs([1000000, 0, 0, 0]); pub const TWO_DECIMALS: U256 = U256::from_limbs([100, 0, 0, 0]); pub const B_I1E18: U256 = U256::from_limbs([0x0DE0B6B3A7640000, 0, 0, 0]); pub const B_I1E27: U256 = U256::from_limbs([0x9fd0803ce8000000, 0x33b2e3c, 0, 0]); pub const DEX_AMOUNT_DECIMALS: i64 = 12;
52 pub const FEE_PERCENT_PRECISION: U256 = U256::from_limbs([10000, 0, 0, 0]);
53 pub const ZERO_ADDRESS: &[u8] = &hex!("0x0000000000000000000000000000000000000000");
54 pub const NATIVE_ADDRESS: &[u8] = &hex!("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE");
55 pub const RESERVES_RESOLVER: &[u8] = &hex!("0xc93876c0eed99645dd53937b25433e311881a27c");
56}
57
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct FluidV1 {
60 pool_address: Bytes,
61 token0: Token,
62 token1: Token,
63 collateral_reserves: CollateralReserves,
64 debt_reserves: DebtReserves,
65 dex_limits: DexLimits,
66 center_price: U256,
67 fee: U256,
68 sync_time: u64,
69 pool_reserve0: U256,
70 pool_reserve1: U256,
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74pub(super) struct CollateralReserves {
75 pub(super) token0_real_reserves: U256,
76 pub(super) token1_real_reserves: U256,
77 pub(super) token0_imaginary_reserves: U256,
78 pub(super) token1_imaginary_reserves: U256,
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub(super) struct DebtReserves {
83 pub(super) token0_real_reserves: U256,
84 pub(super) token1_real_reserves: U256,
85 pub(super) token0_imaginary_reserves: U256,
86 pub(super) token1_imaginary_reserves: U256,
87}
88
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub(super) struct DexLimits {
91 pub(super) borrowable_token0: TokenLimit,
92 pub(super) borrowable_token1: TokenLimit,
93 pub(super) withdrawable_token0: TokenLimit,
94 pub(super) withdrawable_token1: TokenLimit,
95}
96
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98pub(super) struct TokenLimit {
99 pub(super) available: U256,
100 pub(super) expands_to: U256,
101 pub(super) expand_duration: U256,
102}
103
104#[derive(Debug, Error)]
105enum SwapError {
106 #[error("Insufficient reserve: tokenOut amount exceeds reserve")]
107 InsufficientReserve,
108 #[error("Insufficient reserve: tokenOut amount exceeds borrowable limit")]
109 InsufficientBorrowable,
110 #[error("Insufficient reserve: tokenOut amount exceeds withdrawable limit")]
111 InsufficientWithdrawable,
112 #[error("Insufficient reserve: tokenOut amount exceeds max price limit")]
113 InsufficientMaxPrice,
114 #[error("Invalid reserves ratio")]
115 VerifyReservesRatiosInvalid,
116 #[error("No pools are enabled")]
117 NoPoolsEnabled,
118 #[error("InvalidAmountIn: Amount too low")]
119 InvalidAmountIn,
120}
121
122impl From<SwapError> for SimulationError {
123 fn from(value: SwapError) -> Self {
124 Self::FatalError(value.to_string())
125 }
126}
127impl FluidV1 {
128 #[allow(clippy::too_many_arguments)]
129 pub(super) fn new(
130 pool_address: &Bytes,
131 token0: &Token,
132 token1: &Token,
133 collateral_reserves: CollateralReserves,
134 debt_reserves: DebtReserves,
135 dex_limits: DexLimits,
136 center_price: U256,
137 fee: U256,
138 sync_time: u64,
139 ) -> Self {
140 let pool_reserve0 = get_max_reserves(
141 token0.decimals as u8,
142 &dex_limits.withdrawable_token0,
143 &dex_limits.borrowable_token0,
144 &collateral_reserves.token0_real_reserves,
145 &debt_reserves.token0_real_reserves,
146 );
147 let pool_reserve1 = get_max_reserves(
148 token1.decimals as u8,
149 &dex_limits.withdrawable_token1,
150 &dex_limits.borrowable_token1,
151 &collateral_reserves.token1_real_reserves,
152 &debt_reserves.token1_real_reserves,
153 );
154
155 let (token0_normalized, token1_normalized) =
158 if FluidV1::normalize_native_address(&token0.address) <
159 FluidV1::normalize_native_address(&token1.address)
160 {
161 (token0.clone(), token1.clone())
162 } else {
163 (token1.clone(), token0.clone())
164 };
165 Self {
166 pool_address: pool_address.clone(),
167 token0: token0_normalized,
168 token1: token1_normalized,
169 collateral_reserves,
170 debt_reserves,
171 dex_limits,
172 center_price,
173 fee,
174 sync_time,
175 pool_reserve0,
176 pool_reserve1,
177 }
178 }
179
180 fn normalize_native_address(address: &Bytes) -> &[u8] {
181 if address == constant::ZERO_ADDRESS {
182 constant::NATIVE_ADDRESS
183 } else {
184 address
185 }
186 }
187}
188
189#[typetag::serde]
190impl ProtocolSim for FluidV1 {
191 fn fee(&self) -> f64 {
192 let fee = u256_to_f64(self.fee).expect("Fluid fee values are safe to convert");
193 let precision =
194 u256_to_f64(constant::FEE_PERCENT_PRECISION).expect("FEE_PERCENT_PRECISION is safe");
195 fee / precision / 100.0
198 }
199
200 fn spot_price(&self, base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
201 let price_f64 = if !self
202 .collateral_reserves
203 .token0_imaginary_reserves
204 .is_zero()
205 {
206 u256_to_f64(
207 self.collateral_reserves
208 .token1_imaginary_reserves,
209 )? / u256_to_f64(
210 self.collateral_reserves
211 .token0_imaginary_reserves,
212 )?
213 } else {
214 u256_to_f64(
215 self.debt_reserves
216 .token1_imaginary_reserves,
217 )? / u256_to_f64(
218 self.debt_reserves
219 .token0_imaginary_reserves,
220 )?
221 };
222 let oriented_price_f64 =
223 if base.address == self.token0.address { price_f64 } else { 1.0 / price_f64 };
224
225 Ok(add_fee_markup(oriented_price_f64, self.fee()))
226 }
227
228 fn get_amount_out(
229 &self,
230 amount_in: BigUint,
231 token_in: &Token,
232 token_out: &Token,
233 ) -> Result<GetAmountOutResult, SimulationError> {
234 if amount_in == BigUint::from(0u32) {
235 return Ok(GetAmountOutResult {
236 amount: BigUint::from(0u32),
237 gas: BigUint::from(155433u32),
238 new_state: Box::new(self.clone()),
239 });
240 }
241 let zero2one = self.token0.address == token_in.address;
242
243 let (token_in_decimals, token_out_decimals) = (token_in.decimals, token_out.decimals);
244
245 let amount_in = biguint_to_u256(&amount_in);
246 let fee = amount_in * self.fee / constant::SIX_DECIMALS;
247
248 let amount_in_after_fee = amount_in - fee;
249 let amount_in_adjusted = to_adjusted_amount(amount_in_after_fee, token_in_decimals as i64);
250
251 if amount_in_adjusted < constant::SIX_DECIMALS ||
252 amount_in_after_fee < constant::TWO_DECIMALS
253 {
254 return Err(SwapError::InvalidAmountIn.into());
255 }
256 let mut new_col_reserves = self.collateral_reserves.clone();
257 let mut new_debt_reserves = self.debt_reserves.clone();
258 let mut new_limits = self.dex_limits.clone();
259
260 let amount_out = swap_in_adjusted(
261 zero2one,
262 amount_in_adjusted,
263 &mut new_col_reserves,
264 &mut new_debt_reserves,
265 token_out_decimals as i64,
266 &mut new_limits,
267 self.center_price,
268 self.sync_time,
269 )?;
270
271 let reserve = if zero2one { self.pool_reserve1 } else { self.pool_reserve0 };
272 if amount_out > reserve {
273 return Err(SwapError::InsufficientReserve.into());
274 }
275
276 let result = GetAmountOutResult::new(
277 u256_to_biguint(amount_out),
278 155433.to_biguint().expect("infallible"),
279 Box::new(Self {
280 pool_address: self.pool_address.clone(),
281 token0: self.token0.clone(),
282 token1: self.token1.clone(),
283 collateral_reserves: new_col_reserves,
284 debt_reserves: new_debt_reserves,
285 dex_limits: new_limits,
286 center_price: self.center_price,
287 fee: self.fee,
288 sync_time: self.sync_time,
289 pool_reserve0: self.pool_reserve0,
290 pool_reserve1: self.pool_reserve1,
291 }),
292 );
293 Ok(result)
294 }
295
296 fn get_limits(
297 &self,
298 sell_token: Bytes,
299 buy_token: Bytes,
300 ) -> Result<(BigUint, BigUint), SimulationError> {
301 let zero2one = sell_token == self.token0.address;
302
303 let (upper_bound_out, out_decimals, in_decimals) = if zero2one {
304 (
305 to_adjusted_amount(
306 self.dex_limits
307 .withdrawable_token0
308 .available +
309 self.dex_limits
310 .borrowable_token0
311 .available,
312 self.token0.decimals as i64,
313 ),
314 self.token1.decimals,
315 self.token0.decimals,
316 )
317 } else {
318 (
319 to_adjusted_amount(
320 self.dex_limits
321 .withdrawable_token1
322 .available +
323 self.dex_limits
324 .borrowable_token1
325 .available,
326 self.token1.decimals as i64,
327 ),
328 self.token0.decimals,
329 self.token1.decimals,
330 )
331 };
332 if upper_bound_out == U256::ZERO {
333 trace!("Upper bound is zero for {}", self.pool_address);
334 return Ok((BigUint::ZERO, BigUint::ZERO));
335 }
336 let delta = U256::from(10).pow(U256::from(2));
337 let (max_valid, res) = find_max_valid_u256(upper_bound_out, delta, |amount| {
338 let mut col_clone = self.collateral_reserves.clone();
339 let mut debt_clone = self.debt_reserves.clone();
340 let mut limits_clone = self.dex_limits.clone();
341 swap_in_adjusted(
342 zero2one,
343 amount,
344 &mut col_clone,
345 &mut debt_clone,
346 out_decimals as i64,
347 &mut limits_clone,
348 self.center_price,
349 self.sync_time,
350 )
351 });
352 Ok((
353 u256_to_biguint(from_adjusted_amount(max_valid, in_decimals as i64)),
354 u256_to_biguint(res.unwrap_or_else(|| {
355 trace!(
356 "All evaluations errored during limit search for {} -> {}",
357 sell_token,
358 buy_token
359 );
360 U256::ZERO
361 })),
362 ))
363 }
364
365 fn delta_transition(
366 &mut self,
367 _delta: ProtocolStateDelta,
368 _tokens: &HashMap<Bytes, Token>,
369 _balances: &Balances,
370 ) -> Result<(), TransitionError> {
371 let engine = create_engine(SHARED_TYCHO_DB.clone(), false).expect("Infallible");
372
373 let state = vm::decode_from_vm(
374 &self.pool_address,
375 &self.token0,
376 &self.token1,
377 RESERVES_RESOLVER,
378 engine,
379 )?;
380
381 trace!(?state, "Calling delta transition for {}", &self.pool_address);
382
383 self.collateral_reserves = state.collateral_reserves;
384 self.debt_reserves = state.debt_reserves;
385 self.dex_limits = state.dex_limits;
386 self.center_price = state.center_price;
387 self.fee = state.fee;
388 self.sync_time = state.sync_time;
389
390 self.pool_reserve0 = get_max_reserves(
391 self.token0.decimals as u8,
392 &self.dex_limits.withdrawable_token0,
393 &self.dex_limits.borrowable_token0,
394 &self
395 .collateral_reserves
396 .token0_real_reserves,
397 &self.debt_reserves.token0_real_reserves,
398 );
399 self.pool_reserve1 = get_max_reserves(
400 self.token1.decimals as u8,
401 &self.dex_limits.withdrawable_token1,
402 &self.dex_limits.borrowable_token1,
403 &self
404 .collateral_reserves
405 .token1_real_reserves,
406 &self.debt_reserves.token1_real_reserves,
407 );
408 Ok(())
409 }
410
411 fn clone_box(&self) -> Box<dyn ProtocolSim> {
412 Box::new(self.clone())
413 }
414
415 fn as_any(&self) -> &dyn Any {
416 self
417 }
418
419 fn as_any_mut(&mut self) -> &mut dyn Any {
420 self
421 }
422
423 fn eq(&self, other: &dyn ProtocolSim) -> bool {
424 if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
425 self == other_state
426 } else {
427 false
428 }
429 }
430
431 fn query_pool_swap(
432 &self,
433 params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
434 ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
435 crate::evm::query_pool_swap::query_pool_swap(self, params)
436 }
437}
438
439pub fn find_max_valid_u256<T, E, F>(upper_bound: U256, delta: U256, mut f: F) -> (U256, Option<T>)
449where
450 F: FnMut(U256) -> Result<T, E>,
451 E: std::fmt::Debug,
452{
453 let mut low = U256::ZERO;
454 let mut high = upper_bound;
455 let mut best = U256::ZERO;
456 let mut best_result: Option<T> = None;
457
458 while high > low + delta {
459 let mid = (low + high) / U256::from(2);
460
461 match f(mid) {
462 Ok(result) => {
463 best = mid;
464 best_result = Some(result);
465 low = mid;
466 }
467 Err(_) => {
468 high = mid;
469 }
470 }
471 }
472
473 (best, best_result)
474}
475
476#[allow(clippy::too_many_arguments)]
477fn swap_in_adjusted(
478 swap0_to_1: bool,
479 amount_to_swap: U256,
480 col_reserves: &mut CollateralReserves,
481 debt_reserves: &mut DebtReserves,
482 out_decimals: i64,
483 current_limits: &mut DexLimits,
484 center_price: U256,
485 sync_time: u64,
486) -> Result<U256, SwapError> {
487 let (
488 col_reserve_in,
489 col_reserve_out,
490 col_i_reserve_in,
491 col_i_reserve_out,
492 debt_reserve_in,
493 debt_reserve_out,
494 debt_i_reserve_in,
495 debt_i_reserve_out,
496 borrowable,
497 withdrawable,
498 ) = if swap0_to_1 {
499 (
500 col_reserves.token0_real_reserves,
501 col_reserves.token1_real_reserves,
502 col_reserves.token0_imaginary_reserves,
503 col_reserves.token1_imaginary_reserves,
504 debt_reserves.token0_real_reserves,
505 debt_reserves.token1_real_reserves,
506 debt_reserves.token0_imaginary_reserves,
507 debt_reserves.token1_imaginary_reserves,
508 get_expanded_limit(sync_time, ¤t_limits.borrowable_token1),
509 get_expanded_limit(sync_time, ¤t_limits.withdrawable_token1),
510 )
511 } else {
512 (
513 col_reserves.token1_real_reserves,
514 col_reserves.token0_real_reserves,
515 col_reserves.token1_imaginary_reserves,
516 col_reserves.token0_imaginary_reserves,
517 debt_reserves.token1_real_reserves,
518 debt_reserves.token0_real_reserves,
519 debt_reserves.token1_imaginary_reserves,
520 debt_reserves.token0_imaginary_reserves,
521 get_expanded_limit(sync_time, ¤t_limits.borrowable_token0),
522 get_expanded_limit(sync_time, ¤t_limits.withdrawable_token0),
523 )
524 };
525
526 let borrowable = to_adjusted_amount(borrowable, out_decimals);
528 let withdrawable = to_adjusted_amount(withdrawable, out_decimals);
529
530 let col_pool_enabled = col_reserves.token0_real_reserves > U256::ZERO &&
532 col_reserves.token1_real_reserves > U256::ZERO &&
533 col_reserves.token0_imaginary_reserves > U256::ZERO &&
534 col_reserves.token1_imaginary_reserves > U256::ZERO;
535
536 let debt_pool_enabled = debt_reserves.token0_real_reserves > U256::ZERO &&
537 debt_reserves.token1_real_reserves > U256::ZERO &&
538 debt_reserves.token0_imaginary_reserves > U256::ZERO &&
539 debt_reserves.token1_imaginary_reserves > U256::ZERO;
540
541 if !col_pool_enabled && !debt_pool_enabled {
542 return Err(SwapError::NoPoolsEnabled);
543 }
544
545 let a = if col_pool_enabled && debt_pool_enabled {
546 swap_routing_in(
547 amount_to_swap,
548 col_i_reserve_out,
549 col_i_reserve_in,
550 debt_i_reserve_out,
551 debt_i_reserve_in,
552 )
553 } else if debt_pool_enabled {
554 U256::MAX } else if col_pool_enabled {
556 amount_to_swap + U256::ONE } else {
558 return Err(SwapError::NoPoolsEnabled);
559 };
560
561 let (amount_in_collateral, amount_out_collateral, amount_in_debt, amount_out_debt) = if a ==
562 U256::ZERO ||
563 a == U256::MAX
564 {
565 let amount_out_debt = get_amount_out(amount_to_swap, debt_i_reserve_in, debt_i_reserve_out);
567 (U256::ZERO, U256::ZERO, amount_to_swap, amount_out_debt)
568 } else if a >= amount_to_swap {
569 let amount_out_collateral =
571 get_amount_out(amount_to_swap, col_i_reserve_in, col_i_reserve_out);
572 (amount_to_swap, amount_out_collateral, U256::ZERO, U256::ZERO)
573 } else {
574 let amount_in_debt = amount_to_swap - a;
576 let amount_out_debt = get_amount_out(amount_in_debt, debt_i_reserve_in, debt_i_reserve_out);
577 let amount_out_collateral = get_amount_out(a, col_i_reserve_in, col_i_reserve_out);
578 (a, amount_out_collateral, amount_in_debt, amount_out_debt)
579 };
580
581 if amount_out_debt > debt_reserve_out {
582 return Err(SwapError::InsufficientReserve);
583 }
584
585 if amount_out_collateral > col_reserve_out {
586 return Err(SwapError::InsufficientReserve);
587 }
588
589 if amount_out_debt > borrowable {
590 return Err(SwapError::InsufficientBorrowable);
591 }
592
593 if amount_out_collateral > withdrawable {
594 return Err(SwapError::InsufficientWithdrawable);
595 }
596
597 if amount_in_collateral > U256::ZERO {
598 let reserves_ratio_valid = if swap0_to_1 {
599 verify_token1_reserves(
600 col_reserve_in + amount_in_collateral,
601 col_reserve_out - amount_out_collateral,
602 center_price,
603 )
604 } else {
605 verify_token0_reserves(
606 col_reserve_out - amount_out_collateral,
607 col_reserve_in + amount_in_collateral,
608 center_price,
609 )
610 };
611 if !reserves_ratio_valid {
612 return Err(SwapError::VerifyReservesRatiosInvalid);
613 }
614 }
615
616 if amount_in_debt > U256::ZERO {
617 let reserves_ratio_valid = if swap0_to_1 {
618 verify_token1_reserves(
619 debt_reserve_in + amount_in_debt,
620 debt_reserve_out - amount_out_debt,
621 center_price,
622 )
623 } else {
624 verify_token0_reserves(
625 debt_reserve_out - amount_out_debt,
626 debt_reserve_in + amount_in_debt,
627 center_price,
628 )
629 };
630 if !reserves_ratio_valid {
631 return Err(SwapError::VerifyReservesRatiosInvalid);
632 }
633 }
634
635 let (old_price, new_price) = if amount_in_collateral > amount_in_debt {
636 if swap0_to_1 {
637 (
638 col_i_reserve_out * constant::B_I1E27 / col_i_reserve_in,
639 (col_i_reserve_out - amount_out_collateral) * constant::B_I1E27 /
640 (col_i_reserve_in + amount_in_collateral),
641 )
642 } else {
643 (
644 col_i_reserve_in * constant::B_I1E27 / col_i_reserve_out,
645 (col_i_reserve_in + amount_in_collateral) * constant::B_I1E27 /
646 (col_i_reserve_out - amount_out_collateral),
647 )
648 }
649 } else if swap0_to_1 {
650 (
651 debt_i_reserve_out * constant::B_I1E27 / debt_i_reserve_in,
652 (debt_i_reserve_out - amount_out_debt) * constant::B_I1E27 /
653 (debt_i_reserve_in + amount_in_debt),
654 )
655 } else {
656 (
657 debt_i_reserve_in * constant::B_I1E27 / debt_i_reserve_out,
658 (debt_i_reserve_in + amount_in_debt) * constant::B_I1E27 /
659 (debt_i_reserve_out - amount_out_debt),
660 )
661 };
662
663 let price_diff = old_price.abs_diff(new_price);
664 let max_price_diff = old_price * constant::MAX_PRICE_DIFF / constant::TWO_DECIMALS;
665
666 if price_diff > max_price_diff {
667 return Err(SwapError::InsufficientMaxPrice);
668 }
669
670 if amount_in_collateral > U256::ZERO {
671 update_collateral_reserves_and_limits(
672 swap0_to_1,
673 amount_in_collateral,
674 amount_out_collateral,
675 col_reserves,
676 current_limits,
677 out_decimals,
678 );
679 }
680
681 if amount_in_debt > U256::ZERO {
682 update_debt_reserves_and_limits(
683 swap0_to_1,
684 amount_in_debt,
685 amount_out_debt,
686 debt_reserves,
687 current_limits,
688 out_decimals,
689 );
690 }
691
692 Ok(from_adjusted_amount(amount_out_collateral + amount_out_debt, out_decimals))
693}
694
695#[allow(clippy::too_many_arguments, dead_code)]
696fn swap_out_adjusted(
697 swap0_to_1: bool,
698 amount_to_receive: U256,
699 col_reserves: &mut CollateralReserves,
700 debt_reserves: &mut DebtReserves,
701 in_decimals: i64,
702 out_decimals: i64,
703 current_limits: &mut DexLimits,
704 center_price: U256,
705 sync_time: u64,
706) -> Result<U256, SwapError> {
707 let (
708 col_reserve_in,
709 col_reserve_out,
710 col_i_reserve_in,
711 col_i_reserve_out,
712 debt_reserve_in,
713 debt_reserve_out,
714 debt_i_reserve_in,
715 debt_i_reserve_out,
716 borrowable,
717 withdrawable,
718 ) = if swap0_to_1 {
719 (
720 col_reserves.token0_real_reserves,
721 col_reserves.token1_real_reserves,
722 col_reserves.token0_imaginary_reserves,
723 col_reserves.token1_imaginary_reserves,
724 debt_reserves.token0_real_reserves,
725 debt_reserves.token1_real_reserves,
726 debt_reserves.token0_imaginary_reserves,
727 debt_reserves.token1_imaginary_reserves,
728 get_expanded_limit(sync_time, ¤t_limits.borrowable_token1),
729 get_expanded_limit(sync_time, ¤t_limits.withdrawable_token1),
730 )
731 } else {
732 (
733 col_reserves.token1_real_reserves,
734 col_reserves.token0_real_reserves,
735 col_reserves.token1_imaginary_reserves,
736 col_reserves.token0_imaginary_reserves,
737 debt_reserves.token1_real_reserves,
738 debt_reserves.token0_real_reserves,
739 debt_reserves.token1_imaginary_reserves,
740 debt_reserves.token0_imaginary_reserves,
741 get_expanded_limit(sync_time, ¤t_limits.borrowable_token0),
742 get_expanded_limit(sync_time, ¤t_limits.withdrawable_token0),
743 )
744 };
745
746 let borrowable = to_adjusted_amount(borrowable, out_decimals);
747 let withdrawable = to_adjusted_amount(withdrawable, out_decimals);
748
749 let col_pool_enabled = col_reserves.token0_real_reserves > U256::ZERO &&
750 col_reserves.token1_real_reserves > U256::ZERO &&
751 col_reserves.token0_imaginary_reserves > U256::ZERO &&
752 col_reserves.token1_imaginary_reserves > U256::ZERO;
753
754 let debt_pool_enabled = debt_reserves.token0_real_reserves > U256::ZERO &&
755 debt_reserves.token1_real_reserves > U256::ZERO &&
756 debt_reserves.token0_imaginary_reserves > U256::ZERO &&
757 debt_reserves.token1_imaginary_reserves > U256::ZERO;
758
759 if !col_pool_enabled && !debt_pool_enabled {
760 return Err(SwapError::NoPoolsEnabled);
761 }
762
763 let a = if col_pool_enabled && debt_pool_enabled {
764 swap_routing_out(
765 amount_to_receive,
766 col_i_reserve_out,
767 col_i_reserve_in,
768 debt_i_reserve_out,
769 debt_i_reserve_in,
770 )
771 } else if debt_pool_enabled {
772 U256::MAX
773 } else if col_pool_enabled {
774 amount_to_receive + U256::ONE
775 } else {
776 return Err(SwapError::NoPoolsEnabled);
777 };
778
779 let mut trigger_update_debt_reserves = false;
780 let mut trigger_update_col_reserves = false;
781
782 let (amount_in_collateral, amount_out_collateral, amount_in_debt, amount_out_debt) =
783 if a == U256::ZERO || a == U256::MAX {
784 let amount_in_debt =
785 get_amount_in(amount_to_receive, debt_i_reserve_in, debt_i_reserve_out);
786 if amount_to_receive > debt_reserve_out {
787 return Err(SwapError::InsufficientReserve);
788 }
789
790 trigger_update_debt_reserves = true;
791 (U256::ZERO, U256::ZERO, amount_in_debt, amount_to_receive)
792 } else if a >= amount_to_receive {
793 let amount_in_collateral =
794 get_amount_in(amount_to_receive, col_i_reserve_in, col_i_reserve_out);
795
796 if amount_to_receive > col_reserve_out {
797 return Err(SwapError::InsufficientReserve);
798 }
799
800 trigger_update_col_reserves = true;
801 (amount_in_collateral, amount_to_receive, U256::ZERO, U256::ZERO)
802 } else {
803 let amount_out_collateral = a;
804 let amount_in_collateral =
805 get_amount_in(amount_out_collateral, col_i_reserve_in, col_i_reserve_out);
806 let amount_out_debt = amount_to_receive - amount_out_collateral;
807 let amount_in_debt =
808 get_amount_in(amount_out_debt, debt_i_reserve_in, debt_i_reserve_out);
809
810 if amount_out_debt > debt_reserve_out || amount_out_collateral > col_reserve_out {
811 return Err(SwapError::InsufficientReserve);
812 }
813
814 (amount_in_collateral, amount_out_collateral, amount_in_debt, amount_out_debt)
815 };
816
817 if amount_in_debt > borrowable {
818 return Err(SwapError::InsufficientBorrowable);
819 }
820
821 if amount_in_collateral > withdrawable {
822 return Err(SwapError::InsufficientWithdrawable);
823 }
824
825 if amount_in_collateral > U256::ZERO {
826 let reserves_ratio_valid = if swap0_to_1 {
827 verify_token1_reserves(
828 col_reserve_in + amount_in_collateral,
829 col_reserve_out - amount_out_collateral,
830 center_price,
831 )
832 } else {
833 verify_token0_reserves(
834 col_reserve_out - amount_out_collateral,
835 col_reserve_in + amount_in_collateral,
836 center_price,
837 )
838 };
839 if !reserves_ratio_valid {
840 return Err(SwapError::VerifyReservesRatiosInvalid);
841 }
842 }
843
844 if amount_in_debt > U256::ZERO {
845 let reserves_ratio_valid = if swap0_to_1 {
846 verify_token1_reserves(
847 debt_reserve_in + amount_in_debt,
848 debt_reserve_out - amount_out_debt,
849 center_price,
850 )
851 } else {
852 verify_token0_reserves(
853 debt_reserve_out - amount_out_debt,
854 debt_reserve_in + amount_in_debt,
855 center_price,
856 )
857 };
858 if !reserves_ratio_valid {
859 return Err(SwapError::VerifyReservesRatiosInvalid);
860 }
861 }
862
863 let (old_price, new_price) = if amount_in_collateral > amount_in_debt {
864 if swap0_to_1 {
865 (
866 col_i_reserve_out * constant::B_I1E27 / col_i_reserve_in,
867 (col_i_reserve_out - amount_out_collateral) * constant::B_I1E27 /
868 (col_i_reserve_in + amount_in_collateral),
869 )
870 } else {
871 (
872 col_i_reserve_in * constant::B_I1E27 / col_i_reserve_out,
873 (col_i_reserve_in + amount_in_collateral) * constant::B_I1E27 /
874 (col_i_reserve_out - amount_out_collateral),
875 )
876 }
877 } else if swap0_to_1 {
878 (
879 debt_i_reserve_out * constant::B_I1E27 / debt_i_reserve_in,
880 (debt_i_reserve_out - amount_out_debt) * constant::B_I1E27 /
881 (debt_i_reserve_in + amount_in_debt),
882 )
883 } else {
884 (
885 debt_i_reserve_in * constant::B_I1E27 / debt_i_reserve_out,
886 (debt_i_reserve_in + amount_in_debt) * constant::B_I1E27 /
887 (debt_i_reserve_out - amount_out_debt),
888 )
889 };
890
891 let price_diff = old_price.abs_diff(new_price);
892 let max_price_diff = old_price * constant::MAX_PRICE_DIFF / constant::TWO_DECIMALS;
893
894 if price_diff > max_price_diff {
895 return Err(SwapError::InsufficientMaxPrice);
896 }
897
898 if trigger_update_col_reserves {
899 update_collateral_reserves_and_limits(
900 swap0_to_1,
901 amount_in_collateral,
902 amount_out_collateral,
903 col_reserves,
904 current_limits,
905 out_decimals,
906 );
907 }
908
909 if trigger_update_debt_reserves {
910 update_debt_reserves_and_limits(
911 swap0_to_1,
912 amount_in_debt,
913 amount_out_debt,
914 debt_reserves,
915 current_limits,
916 out_decimals,
917 );
918 }
919
920 Ok(from_adjusted_amount(amount_in_collateral + amount_in_debt, in_decimals))
921}
922
923fn swap_routing_in(t: U256, x: U256, y: U256, x2: U256, y2: U256) -> U256 {
943 let xy_root = (x * y * constant::B_I1E18).root(2);
944 let x2y2_root = (x2 * y2 * constant::B_I1E18).root(2);
945
946 let numerator = y2 * xy_root + t * xy_root - y * x2y2_root;
947 let denominator = xy_root + x2y2_root;
948 numerator / denominator
949}
950
951#[allow(dead_code)]
958fn swap_routing_out(t: U256, x: U256, y: U256, x2: U256, y2: U256) -> U256 {
959 let xy_root = (x * y * constant::B_I1E18).root(2);
960 let x2y2_root = (x2 * y2 * constant::B_I1E18).root(2);
961
962 let numerator = t * xy_root + y * x2y2_root - y2 * xy_root;
963 let denominator = xy_root + x2y2_root;
964
965 numerator / denominator
966}
967
968fn get_amount_out(amount_in: U256, i_reserve_in: U256, i_reserve_out: U256) -> U256 {
969 amount_in * i_reserve_out / (i_reserve_in + amount_in)
970}
971
972#[allow(dead_code)]
976fn get_amount_in(amount_out: U256, i_reserve_in: U256, i_reserve_out: U256) -> U256 {
977 amount_out * i_reserve_in / (i_reserve_out - amount_out)
978}
979
980fn to_adjusted_amount(amount: U256, decimals: i64) -> U256 {
981 let diff = decimals - constant::DEX_AMOUNT_DECIMALS;
982 if diff == 0 {
983 amount
984 } else if diff > 0 {
985 amount / ten_pow(diff)
986 } else {
987 amount * ten_pow(-diff)
988 }
989}
990
991fn from_adjusted_amount(adjusted_amount: U256, decimals: i64) -> U256 {
1001 let diff = decimals - constant::DEX_AMOUNT_DECIMALS;
1002
1003 if diff == 0 {
1004 adjusted_amount
1005 } else if diff < 0 {
1006 let divisor = ten_pow(-diff);
1008 adjusted_amount / divisor
1009 } else {
1010 let multiplier = ten_pow(diff);
1012 adjusted_amount * multiplier
1013 }
1014}
1015
1016fn ten_pow(v: i64) -> U256 {
1017 U256::from(10u64).pow(U256::from((v) as u64))
1018}
1019
1020fn verify_token0_reserves(token0_reserves: U256, token1_reserves: U256, price: U256) -> bool {
1037 let numerator = token1_reserves.saturating_mul(constant::B_I1E27);
1038 let denominator = price.saturating_mul(constant::MIN_SWAP_LIQUIDITY);
1039 token0_reserves >=
1040 numerator
1041 .checked_div(denominator)
1042 .unwrap_or(U256::ZERO)
1043}
1044
1045fn verify_token1_reserves(token0_reserves: U256, token1_reserves: U256, price: U256) -> bool {
1062 let numerator = token0_reserves.saturating_mul(price);
1063 let denominator = constant::B_I1E27.saturating_mul(constant::MIN_SWAP_LIQUIDITY);
1064 token1_reserves >= numerator.div_euclid(&denominator)
1065}
1066
1067fn get_expanded_limit(sync_time: u64, limit: &TokenLimit) -> U256 {
1079 let current_time = SystemTime::now()
1080 .duration_since(UNIX_EPOCH)
1081 .expect("system time before UNIX_EPOCH")
1082 .as_secs();
1083
1084 let elapsed_time = current_time.saturating_sub(sync_time);
1085 let elapsed = U256::from(elapsed_time);
1086
1087 if elapsed_time < 10 {
1088 return limit.available;
1090 }
1091
1092 if elapsed >= limit.expand_duration {
1093 return limit.expands_to;
1095 }
1096
1097 let delta = limit
1100 .expands_to
1101 .saturating_sub(limit.available);
1102 limit
1103 .available
1104 .saturating_add(delta.saturating_mul(elapsed) / limit.expand_duration)
1105}
1106
1107fn update_collateral_reserves_and_limits(
1112 swap0_to_1: bool,
1113 amount_in: U256,
1114 amount_out: U256,
1115 col_reserves: &mut CollateralReserves,
1116 limits: &mut DexLimits,
1117 out_decimals: i64,
1118) {
1119 let unadjusted_amount_out = from_adjusted_amount(amount_out, out_decimals);
1120
1121 if swap0_to_1 {
1122 col_reserves.token0_real_reserves = col_reserves
1124 .token0_real_reserves
1125 .saturating_add(amount_in);
1126 col_reserves.token0_imaginary_reserves = col_reserves
1127 .token0_imaginary_reserves
1128 .saturating_add(amount_in);
1129 col_reserves.token1_real_reserves = col_reserves
1130 .token1_real_reserves
1131 .saturating_sub(amount_out);
1132 col_reserves.token1_imaginary_reserves = col_reserves
1133 .token1_imaginary_reserves
1134 .saturating_sub(amount_out);
1135
1136 limits.withdrawable_token1.available = limits
1137 .withdrawable_token1
1138 .available
1139 .saturating_sub(unadjusted_amount_out);
1140 limits.withdrawable_token1.expands_to = limits
1141 .withdrawable_token1
1142 .expands_to
1143 .saturating_sub(unadjusted_amount_out);
1144 } else {
1145 col_reserves.token0_real_reserves = col_reserves
1147 .token0_real_reserves
1148 .saturating_sub(amount_out);
1149 col_reserves.token0_imaginary_reserves = col_reserves
1150 .token0_imaginary_reserves
1151 .saturating_sub(amount_out);
1152 col_reserves.token1_real_reserves = col_reserves
1153 .token1_real_reserves
1154 .saturating_add(amount_in);
1155 col_reserves.token1_imaginary_reserves = col_reserves
1156 .token1_imaginary_reserves
1157 .saturating_add(amount_in);
1158
1159 limits.withdrawable_token0.available = limits
1160 .withdrawable_token0
1161 .available
1162 .saturating_sub(unadjusted_amount_out);
1163 limits.withdrawable_token0.expands_to = limits
1164 .withdrawable_token0
1165 .expands_to
1166 .saturating_sub(unadjusted_amount_out);
1167 }
1168}
1169
1170fn update_debt_reserves_and_limits(
1171 swap0_to1: bool,
1172 amount_in: U256,
1173 amount_out: U256,
1174 debt_reserves: &mut DebtReserves,
1175 limits: &mut DexLimits,
1176 out_decimals: i64,
1177) {
1178 let unadjusted_amount_out = from_adjusted_amount(amount_out, out_decimals);
1179
1180 if swap0_to1 {
1181 debt_reserves.token0_real_reserves += amount_in;
1182 debt_reserves.token0_imaginary_reserves += amount_in;
1183 debt_reserves.token1_real_reserves -= amount_out;
1184 debt_reserves.token1_imaginary_reserves -= amount_out;
1185
1186 limits.borrowable_token1.available -= unadjusted_amount_out;
1195 limits.borrowable_token1.expands_to -= unadjusted_amount_out;
1196 } else {
1197 debt_reserves.token0_real_reserves -= amount_out;
1198 debt_reserves.token0_imaginary_reserves -= amount_out;
1199 debt_reserves.token1_real_reserves += amount_in;
1200 debt_reserves.token1_imaginary_reserves += amount_in;
1201
1202 limits.borrowable_token0.available -= unadjusted_amount_out;
1203 limits.borrowable_token0.expands_to -= unadjusted_amount_out;
1204 }
1205}
1206
1207fn get_max_reserves(
1208 decimals: u8,
1209 withdrawable_limit: &TokenLimit,
1210 borrowable_limit: &TokenLimit,
1211 real_col_reserves: &U256,
1212 real_debt_reserves: &U256,
1213) -> U256 {
1214 let mut max_limit_reserves = borrowable_limit.expands_to;
1216
1217 if borrowable_limit.expands_to != withdrawable_limit.expands_to {
1218 max_limit_reserves += withdrawable_limit.expands_to;
1219 }
1220
1221 let mut max_real_reserves = *real_col_reserves + *real_debt_reserves;
1223
1224 if decimals > constant::DEX_AMOUNT_DECIMALS as u8 {
1225 let diff = decimals as i64 - constant::DEX_AMOUNT_DECIMALS;
1226 max_real_reserves *= ten_pow(diff);
1227 } else if decimals < constant::DEX_AMOUNT_DECIMALS as u8 {
1228 let diff = constant::DEX_AMOUNT_DECIMALS - decimals as i64;
1229 max_real_reserves /= ten_pow(diff);
1230 }
1231
1232 if max_real_reserves < max_limit_reserves {
1234 max_real_reserves
1235 } else {
1236 max_limit_reserves
1237 }
1238}
1239
1240#[cfg(test)]
1241mod test {
1242 use std::str::FromStr;
1243
1244 use alloy::primitives::I256;
1245 use anyhow::bail;
1246 use num_traits::Num;
1247 use tycho_common::models::Chain;
1248
1249 use super::*;
1250
1251 fn setup_fluid_pool(center_price: U256) -> (Token, Token, FluidV1) {
1252 let wsteth = Token::new(
1253 &Bytes::from_str("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0").unwrap(),
1254 "wsteth",
1255 18,
1256 0,
1257 &[Some(20000)],
1258 Chain::Ethereum,
1259 100,
1260 );
1261 let eth = Token::new(
1262 &Bytes::from_str("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE").unwrap(),
1263 "ETH",
1264 18,
1265 0,
1266 &[Some(2000)],
1267 Chain::Ethereum,
1268 100,
1269 );
1270
1271 let pool = FluidV1::new(
1272 &Bytes::from_str("0x0B1a513ee24972DAEf112bC777a5610d4325C9e7").unwrap(),
1273 &wsteth,
1274 ð,
1275 CollateralReserves {
1276 token0_real_reserves: U256::from_str("2169934539358").unwrap(),
1277 token1_real_reserves: U256::from_str("19563846299171").unwrap(),
1278 token0_imaginary_reserves: U256::from_str("62490032619260838").unwrap(),
1279 token1_imaginary_reserves: U256::from_str("73741038977020279").unwrap(),
1280 },
1281 DebtReserves {
1282 token0_real_reserves: U256::from_str("2169108220421").unwrap(),
1283 token1_real_reserves: U256::from_str("19572550738602").unwrap(),
1284 token0_imaginary_reserves: U256::from_str("62511862774117387").unwrap(),
1285 token1_imaginary_reserves: U256::from_str("73766803277429176").unwrap(),
1286 },
1287 limits_wide(),
1288 center_price,
1289 U256::from_str("100").unwrap(),
1290 SystemTime::now()
1291 .duration_since(UNIX_EPOCH)
1292 .unwrap()
1293 .as_secs() -
1294 10,
1295 );
1296 (wsteth, eth, pool)
1297 }
1298
1299 fn limits_wide() -> DexLimits {
1300 let limit_wide = U256::from_str("34242332879776515083099999").unwrap();
1301 DexLimits {
1302 withdrawable_token0: TokenLimit {
1303 available: limit_wide,
1304 expands_to: limit_wide,
1305 expand_duration: U256::ZERO,
1306 },
1307 withdrawable_token1: TokenLimit {
1308 available: limit_wide,
1309 expands_to: limit_wide,
1310 expand_duration: U256::from(22),
1311 },
1312 borrowable_token0: TokenLimit {
1313 available: limit_wide,
1314 expands_to: limit_wide,
1315 expand_duration: U256::ZERO,
1316 },
1317 borrowable_token1: TokenLimit {
1318 available: limit_wide,
1319 expands_to: limit_wide,
1320 expand_duration: U256::from(22),
1321 },
1322 }
1323 }
1324
1325 fn limits_tight() -> DexLimits {
1326 let limit_expand_tight = U256::from_str("711907234052361388866").unwrap();
1327
1328 DexLimits {
1329 withdrawable_token0: TokenLimit {
1330 available: U256::from_str("456740438880263").unwrap(),
1331 expands_to: limit_expand_tight,
1332 expand_duration: U256::from(600),
1333 },
1334 withdrawable_token1: TokenLimit {
1335 available: U256::from_str("825179383432029").unwrap(),
1336 expands_to: limit_expand_tight,
1337 expand_duration: U256::from(600),
1338 },
1339 borrowable_token0: TokenLimit {
1340 available: U256::from_str("941825058374170").unwrap(),
1341 expands_to: limit_expand_tight,
1342 expand_duration: U256::from(600),
1343 },
1344 borrowable_token1: TokenLimit {
1345 available: U256::from_str("941825058374170").unwrap(),
1346 expands_to: limit_expand_tight,
1347 expand_duration: U256::from(600),
1348 },
1349 }
1350 }
1351 fn new_col_reserves_one() -> CollateralReserves {
1352 CollateralReserves {
1353 token0_real_reserves: U256::from_str("20000000006000000").unwrap(),
1354 token1_real_reserves: U256::from_str("20000000000500000").unwrap(),
1355 token0_imaginary_reserves: U256::from_str("389736659726997981").unwrap(),
1356 token1_imaginary_reserves: U256::from_str("389736659619871949").unwrap(),
1357 }
1358 }
1359
1360 fn new_col_reserves_empty() -> CollateralReserves {
1361 CollateralReserves {
1362 token0_real_reserves: U256::ZERO,
1363 token1_real_reserves: U256::ZERO,
1364 token0_imaginary_reserves: U256::ZERO,
1365 token1_imaginary_reserves: U256::ZERO,
1366 }
1367 }
1368
1369 fn new_debt_reserves_empty() -> DebtReserves {
1370 DebtReserves {
1371 token0_real_reserves: U256::ZERO,
1372 token1_real_reserves: U256::ZERO,
1373 token0_imaginary_reserves: U256::ZERO,
1374 token1_imaginary_reserves: U256::ZERO,
1375 }
1376 }
1377
1378 fn new_debt_reserves_one() -> DebtReserves {
1379 DebtReserves {
1380 token0_real_reserves: U256::from_str("9486832995556050").unwrap(),
1381 token1_real_reserves: U256::from_str("9486832993079885").unwrap(),
1382 token0_imaginary_reserves: U256::from_str("184868330099560759").unwrap(),
1383 token1_imaginary_reserves: U256::from_str("184868330048879109").unwrap(),
1384 }
1385 }
1386
1387 pub fn get_approx_center_price_in(
1388 amount_to_swap: U256,
1389 swap0_to_1: bool,
1390 col_reserves: &CollateralReserves,
1391 debt_reserves: &DebtReserves,
1392 ) -> Result<U256, anyhow::Error> {
1393 let col_pool_enabled = !col_reserves
1394 .token0_real_reserves
1395 .is_zero() &&
1396 !col_reserves
1397 .token1_real_reserves
1398 .is_zero() &&
1399 !col_reserves
1400 .token0_imaginary_reserves
1401 .is_zero() &&
1402 !col_reserves
1403 .token1_imaginary_reserves
1404 .is_zero();
1405
1406 let debt_pool_enabled = !debt_reserves
1407 .token0_real_reserves
1408 .is_zero() &&
1409 !debt_reserves
1410 .token1_real_reserves
1411 .is_zero() &&
1412 !debt_reserves
1413 .token0_imaginary_reserves
1414 .is_zero() &&
1415 !debt_reserves
1416 .token1_imaginary_reserves
1417 .is_zero();
1418
1419 let (col_i_reserve_in, col_i_reserve_out, debt_i_reserve_in, debt_i_reserve_out) =
1420 if swap0_to_1 {
1421 (
1422 col_reserves.token0_imaginary_reserves,
1423 col_reserves.token1_imaginary_reserves,
1424 debt_reserves.token0_imaginary_reserves,
1425 debt_reserves.token1_imaginary_reserves,
1426 )
1427 } else {
1428 (
1429 col_reserves.token1_imaginary_reserves,
1430 col_reserves.token0_imaginary_reserves,
1431 debt_reserves.token1_imaginary_reserves,
1432 debt_reserves.token0_imaginary_reserves,
1433 )
1434 };
1435
1436 let a = if col_pool_enabled && debt_pool_enabled {
1437 swap_routing_in(
1438 amount_to_swap,
1439 col_i_reserve_out,
1440 col_i_reserve_in,
1441 debt_i_reserve_out,
1442 debt_i_reserve_in,
1443 )
1444 } else if debt_pool_enabled {
1445 U256::MAX } else if col_pool_enabled {
1447 amount_to_swap
1448 .checked_add(U256::from(1))
1449 .unwrap()
1450 } else {
1451 bail!("No pools are enabled");
1452 };
1453
1454 let (amount_in_collateral, amount_in_debt) = if a == U256::MAX || a == U256::ZERO {
1455 (U256::ZERO, amount_to_swap)
1456 } else if a >= amount_to_swap {
1457 (amount_to_swap, U256::ZERO)
1458 } else {
1459 (a, amount_to_swap - a)
1460 };
1461
1462 let price = if amount_in_collateral > amount_in_debt {
1463 if swap0_to_1 {
1464 col_i_reserve_out
1465 .checked_mul(constant::B_I1E27)
1466 .unwrap() /
1467 col_i_reserve_in
1468 } else {
1469 col_i_reserve_in
1470 .checked_mul(constant::B_I1E27)
1471 .unwrap() /
1472 col_i_reserve_out
1473 }
1474 } else if swap0_to_1 {
1475 debt_i_reserve_out
1476 .checked_mul(constant::B_I1E27)
1477 .unwrap() /
1478 debt_i_reserve_in
1479 } else {
1480 debt_i_reserve_in
1481 .checked_mul(constant::B_I1E27)
1482 .unwrap() /
1483 debt_i_reserve_out
1484 };
1485
1486 Ok(price)
1487 }
1488
1489 pub fn get_approx_center_price_out(
1490 amount_out: U256,
1491 swap0_to_1: bool,
1492 col_reserves: &CollateralReserves,
1493 debt_reserves: &DebtReserves,
1494 ) -> Result<U256, SwapError> {
1495 let col_pool_enabled = col_reserves.token0_real_reserves > U256::ZERO &&
1496 col_reserves.token1_real_reserves > U256::ZERO &&
1497 col_reserves.token0_imaginary_reserves > U256::ZERO &&
1498 col_reserves.token1_imaginary_reserves > U256::ZERO;
1499
1500 let debt_pool_enabled = debt_reserves.token0_real_reserves > U256::ZERO &&
1501 debt_reserves.token1_real_reserves > U256::ZERO &&
1502 debt_reserves.token0_imaginary_reserves > U256::ZERO &&
1503 debt_reserves.token1_imaginary_reserves > U256::ZERO;
1504
1505 let (col_i_reserve_in, col_i_reserve_out, debt_i_reserve_in, debt_i_reserve_out) =
1506 if swap0_to_1 {
1507 (
1508 col_reserves.token0_imaginary_reserves,
1509 col_reserves.token1_imaginary_reserves,
1510 debt_reserves.token0_imaginary_reserves,
1511 debt_reserves.token1_imaginary_reserves,
1512 )
1513 } else {
1514 (
1515 col_reserves.token1_imaginary_reserves,
1516 col_reserves.token0_imaginary_reserves,
1517 debt_reserves.token1_imaginary_reserves,
1518 debt_reserves.token0_imaginary_reserves,
1519 )
1520 };
1521
1522 let a = if col_pool_enabled && debt_pool_enabled {
1523 swap_routing_out(
1524 amount_out,
1525 col_i_reserve_in,
1526 col_i_reserve_out,
1527 debt_i_reserve_in,
1528 debt_i_reserve_out,
1529 )
1530 } else if debt_pool_enabled {
1531 U256::MAX } else if col_pool_enabled {
1533 amount_out + U256::ONE } else {
1535 return Err(SwapError::NoPoolsEnabled);
1536 };
1537
1538 let mut amount_in_collateral = U256::ZERO;
1539 let mut amount_in_debt = U256::ZERO;
1540
1541 if a <= U256::ZERO {
1542 amount_in_debt = get_amount_in(amount_out, debt_i_reserve_in, debt_i_reserve_out);
1543 } else if a >= amount_out {
1544 amount_in_collateral = get_amount_in(amount_out, col_i_reserve_in, col_i_reserve_out);
1545 } else {
1546 amount_in_collateral = get_amount_in(a, col_i_reserve_in, col_i_reserve_out);
1547 amount_in_debt = get_amount_in(amount_out - a, debt_i_reserve_in, debt_i_reserve_out);
1548 }
1549
1550 let price = if amount_in_collateral > amount_in_debt {
1551 if swap0_to_1 {
1552 col_i_reserve_out * constant::B_I1E27 / col_i_reserve_in
1553 } else {
1554 col_i_reserve_in * constant::B_I1E27 / col_i_reserve_out
1555 }
1556 } else if swap0_to_1 {
1557 debt_i_reserve_out * constant::B_I1E27 / debt_i_reserve_in
1558 } else {
1559 debt_i_reserve_in * constant::B_I1E27 / debt_i_reserve_out
1560 };
1561
1562 Ok(price)
1563 }
1564
1565 #[test]
1566 fn test_calc_amount_out_zero2one() {
1567 let (wsteth, eth, pool) = setup_fluid_pool(U256::ONE);
1568 let cases = [
1569 ("1000000000000000000", "1179917402128000000"),
1570 ("500000000000000000", "589961060629000000"),
1571 ];
1572 for (amount_in_str, exp_out_str) in cases.into_iter() {
1573 let exp_out = BigUint::from_str_radix(exp_out_str, 10).unwrap();
1574 let res = pool
1575 .get_amount_out(BigUint::from_str_radix(amount_in_str, 10).unwrap(), &wsteth, ð)
1576 .unwrap();
1577
1578 assert_eq!(res.amount, exp_out);
1579 }
1580 }
1581
1582 #[test]
1583 fn test_calc_amount_out_one2zero() {
1584 let center_price = U256::from_str("1200000000000000000000000000").unwrap();
1585 let (wsteth, eth, pool) = setup_fluid_pool(center_price);
1586 let cases = [("800000000000000000", "677868867152000000")];
1587 for (amount_in_str, exp_out_str) in cases.into_iter() {
1588 let exp_out = BigUint::from_str_radix(exp_out_str, 10).unwrap();
1589 let res = pool
1590 .get_amount_out(BigUint::from_str_radix(amount_in_str, 10).unwrap(), ð, &wsteth)
1591 .unwrap();
1592
1593 assert_eq!(res.amount, exp_out);
1594 }
1595 }
1596
1597 #[test]
1598 fn test_amount_out_exceeds_reserve() {
1599 let (wsteth, eth, mut pool) = setup_fluid_pool(U256::ONE);
1600 pool.pool_reserve0 = U256::from_str("18760613183894").unwrap();
1602 pool.pool_reserve1 = U256::from_str("22123580158026").unwrap();
1603 let amount_in = BigUint::from_str_radix("30000000000000000000", 10).unwrap(); let result = pool.get_amount_out(amount_in, &wsteth, ð);
1605
1606 assert!(result.is_err(), "Expected an error for exceeding reserves");
1607 assert_eq!(
1608 result.unwrap_err().to_string(),
1609 SimulationError::from(SwapError::InsufficientReserve).to_string()
1610 );
1611 }
1612
1613 #[test]
1614 fn test_swap_in() {
1615 let sync_time = SystemTime::now()
1616 .duration_since(UNIX_EPOCH)
1617 .unwrap()
1618 .as_secs();
1619
1620 assert_swap_in_result(
1621 true,
1622 U256::from(1_000_000_000_000_000u128), new_col_reserves_one(),
1624 new_debt_reserves_one(),
1625 "998262697204710000000",
1626 12,
1627 18,
1628 limits_wide(),
1629 sync_time - 10,
1630 );
1631
1632 assert_swap_in_result(
1633 true,
1634 U256::from(1_000_000_000_000_000u128),
1635 new_col_reserves_empty(),
1636 new_debt_reserves_one(),
1637 "994619847016724000000",
1638 12,
1639 18,
1640 limits_wide(),
1641 sync_time - 10,
1642 );
1643
1644 assert_swap_in_result(
1645 true,
1646 U256::from(1_000_000_000_000_000u128),
1647 new_col_reserves_one(),
1648 new_debt_reserves_empty(),
1649 "997440731289905000000",
1650 12,
1651 18,
1652 limits_wide(),
1653 sync_time - 10,
1654 );
1655
1656 assert_swap_in_result(
1657 false,
1658 U256::from(1_000_000_000_000_000u128),
1659 new_col_reserves_one(),
1660 new_debt_reserves_one(),
1661 "998262697752553000000",
1662 12,
1663 18,
1664 limits_wide(),
1665 sync_time - 10,
1666 );
1667
1668 assert_swap_in_result(
1669 false,
1670 U256::from(1_000_000_000_000_000u128),
1671 new_col_reserves_empty(),
1672 new_debt_reserves_one(),
1673 "994619847560607000000",
1674 12,
1675 18,
1676 limits_wide(),
1677 sync_time - 10,
1678 );
1679
1680 assert_swap_in_result(
1681 false,
1682 U256::from(1_000_000_000_000_000u128),
1683 new_col_reserves_one(),
1684 new_debt_reserves_empty(),
1685 "997440731837532000000",
1686 12,
1687 18,
1688 limits_wide(),
1689 sync_time - 10,
1690 );
1691 }
1692
1693 #[allow(clippy::too_many_arguments)]
1706 fn assert_swap_in_result(
1707 swap0_to_1: bool,
1708 amount_in: U256,
1709 mut col_reserves: CollateralReserves,
1710 mut debt_reserves: DebtReserves,
1711 expected_amount_out: &str,
1712 in_decimals: i64,
1713 out_decimals: i64,
1714 mut limits: DexLimits,
1715 sync_time: u64,
1716 ) {
1717 let price =
1718 get_approx_center_price_in(amount_in, swap0_to_1, &col_reserves, &debt_reserves)
1719 .expect("Failed to get approx center price");
1720
1721 let adjusted_amount_in = to_adjusted_amount(amount_in, in_decimals);
1722 let out_amt = swap_in_adjusted(
1723 swap0_to_1,
1724 adjusted_amount_in,
1725 &mut col_reserves,
1726 &mut debt_reserves,
1727 out_decimals,
1728 &mut limits,
1729 price,
1730 sync_time,
1731 )
1732 .expect("Failed to calculate swap in adjusted");
1733
1734 assert_eq!(expected_amount_out, out_amt.to_string(), "Amount out mismatch");
1735 }
1736
1737 #[allow(clippy::too_many_arguments)]
1738 fn assert_swap_out_result(
1739 swap0_to_1: bool,
1740 amount_out: U256,
1741 mut col_reserves: CollateralReserves,
1742 mut debt_reserves: DebtReserves,
1743 expected_amount_in: &str,
1744 in_decimals: i64,
1745 out_decimals: i64,
1746 mut limits: DexLimits,
1747 sync_time: i64,
1748 ) {
1749 let price =
1750 get_approx_center_price_out(amount_out, swap0_to_1, &col_reserves, &debt_reserves)
1751 .expect("failed to get approx center price");
1752
1753 let in_amt = swap_out_adjusted(
1754 swap0_to_1,
1755 to_adjusted_amount(amount_out, out_decimals),
1756 &mut col_reserves,
1757 &mut debt_reserves,
1758 in_decimals,
1759 out_decimals,
1760 &mut limits,
1761 price,
1762 sync_time as u64,
1763 )
1764 .expect("swap_out_adjusted failed");
1765
1766 assert_eq!(expected_amount_in, from_adjusted_amount(in_amt, in_decimals).to_string());
1767 }
1768
1769 #[test]
1770 fn test_swap_in_limits() {
1771 let sync_time = SystemTime::now()
1772 .duration_since(UNIX_EPOCH)
1773 .unwrap()
1774 .as_secs();
1775
1776 let price = get_approx_center_price_in(
1778 U256::from(1_000_000_000_000_000u128),
1779 true,
1780 &new_col_reserves_one(),
1781 &new_debt_reserves_one(),
1782 )
1783 .unwrap();
1784
1785 let res = swap_in_adjusted(
1786 true,
1787 U256::from(1_000_000_000_000_000u128),
1788 &mut new_col_reserves_one(),
1789 &mut new_debt_reserves_one(),
1790 18,
1791 &mut limits_tight(),
1792 price,
1793 sync_time - 10,
1794 );
1795
1796 assert_eq!(res.unwrap_err().to_string(), SwapError::InsufficientBorrowable.to_string());
1797
1798 let price = get_approx_center_price_out(
1800 U256::from(1_000_000_000_000_000u128),
1801 true,
1802 &new_col_reserves_one(),
1803 &new_debt_reserves_one(),
1804 )
1805 .unwrap();
1806
1807 let out_amt = swap_in_adjusted(
1808 true,
1809 U256::from(1_000_000_000_000_000u128),
1810 &mut new_col_reserves_one(),
1811 &mut new_debt_reserves_one(),
1812 18,
1813 &mut limits_tight(),
1814 price,
1815 sync_time - 6000,
1816 )
1817 .unwrap();
1818
1819 assert_eq!(out_amt.to_string(), "998262697204710000000");
1820
1821 let price = get_approx_center_price_out(
1823 U256::from(30_000_000_000_000_000u128),
1824 true,
1825 &new_col_reserves_one(),
1826 &new_debt_reserves_one(),
1827 )
1828 .unwrap();
1829
1830 let res = swap_in_adjusted(
1831 true,
1832 U256::from(30_000_000_000_000_000u128),
1833 &mut new_col_reserves_one(),
1834 &mut new_debt_reserves_one(),
1835 18,
1836 &mut limits_wide(),
1837 price,
1838 sync_time - 10,
1839 );
1840
1841 assert_eq!(res.unwrap_err().to_string(), SwapError::InsufficientMaxPrice.to_string());
1842
1843 let price = get_approx_center_price_out(
1845 U256::from(50_000_000_000_000_000u128),
1846 true,
1847 &new_col_reserves_one(),
1848 &new_debt_reserves_one(),
1849 )
1850 .unwrap();
1851
1852 let res = swap_in_adjusted(
1853 true,
1854 U256::from(50_000_000_000_000_000u128),
1855 &mut new_col_reserves_one(),
1856 &mut new_debt_reserves_one(),
1857 18,
1858 &mut limits_wide(),
1859 price,
1860 sync_time - 10,
1861 );
1862
1863 assert_eq!(res.unwrap_err().to_string(), SwapError::InsufficientReserve.to_string());
1864 }
1865
1866 #[test]
1867 fn test_swap_in_adjusted_compare_estimate_in() {
1868 let now = SystemTime::now()
1869 .duration_since(UNIX_EPOCH)
1870 .unwrap()
1871 .as_secs();
1872 let expected_amount_out = U256::from_str("1180035404724000000").unwrap();
1873 let mut col_reserves = CollateralReserves {
1874 token0_real_reserves: U256::from_str("2169934539358").unwrap(),
1875 token1_real_reserves: U256::from_str("19563846299171").unwrap(),
1876 token0_imaginary_reserves: U256::from_str("62490032619260838").unwrap(),
1877 token1_imaginary_reserves: U256::from_str("73741038977020279").unwrap(),
1878 };
1879 let mut debt_reserves = DebtReserves {
1880 token0_real_reserves: U256::from_str("2169108220421").unwrap(),
1881 token1_real_reserves: U256::from_str("19572550738602").unwrap(),
1882 token0_imaginary_reserves: U256::from_str("62511862774117387").unwrap(),
1883 token1_imaginary_reserves: U256::from_str("73766803277429176").unwrap(),
1884 };
1885 let amount_in = U256::from(1000000000000u128); let price = get_approx_center_price_in(amount_in, true, &col_reserves, &debt_reserves)
1887 .expect("Failed to get approximate center price");
1888
1889 let out_amt = swap_in_adjusted(
1890 true,
1891 amount_in,
1892 &mut col_reserves,
1893 &mut debt_reserves,
1894 18,
1895 &mut limits_wide(),
1896 price,
1897 now - 10,
1898 )
1899 .expect("Failed to swap in adjusted");
1900
1901 assert_eq!(expected_amount_out, out_amt);
1902 }
1903
1904 #[test]
1905 fn test_swap_in_debt_empty() {
1906 let now = SystemTime::now()
1907 .duration_since(UNIX_EPOCH)
1908 .unwrap()
1909 .as_secs();
1910
1911 assert_swap_in_result(
1912 true,
1913 U256::from_str("1000000000000000").unwrap(),
1914 new_col_reserves_empty(),
1915 new_debt_reserves_one(),
1916 "994619847016724",
1917 12,
1918 12,
1919 limits_wide(),
1920 now - 10,
1921 );
1922
1923 assert_swap_in_result(
1924 false,
1925 U256::from_str("1000000000000000").unwrap(),
1926 new_col_reserves_empty(),
1927 new_debt_reserves_one(),
1928 "994619847560607",
1929 12,
1930 12,
1931 limits_wide(),
1932 now - 10,
1933 )
1934 }
1935
1936 #[test]
1937 fn test_swap_in_col_empty() {
1938 let now = SystemTime::now()
1939 .duration_since(UNIX_EPOCH)
1940 .unwrap()
1941 .as_secs();
1942
1943 assert_swap_in_result(
1944 true,
1945 U256::from_str("1000000000000000").unwrap(),
1946 new_col_reserves_one(),
1947 new_debt_reserves_empty(),
1948 "997440731289905",
1949 12,
1950 12,
1951 limits_wide(),
1952 now - 10,
1953 );
1954
1955 assert_swap_in_result(
1956 false,
1957 U256::from_str("1000000000000000").unwrap(),
1958 new_col_reserves_one(),
1959 new_debt_reserves_empty(),
1960 "997440731837532",
1961 12,
1962 12,
1963 limits_wide(),
1964 now - 10,
1965 )
1966 }
1967
1968 #[test]
1969 fn test_swap_out() {
1970 let sync_time = (SystemTime::now()
1971 .duration_since(UNIX_EPOCH)
1972 .unwrap()
1973 .as_secs() as i64) -
1974 10;
1975
1976 assert_swap_out_result(
1977 true,
1978 U256::from(1_000_000_000_000_000u64),
1979 new_col_reserves_one(),
1980 new_debt_reserves_one(),
1981 "1001743360284199",
1982 12,
1983 12,
1984 limits_wide(),
1985 sync_time,
1986 );
1987
1988 assert_swap_out_result(
1989 true,
1990 U256::from(1_000_000_000_000_000u64),
1991 new_col_reserves_empty(),
1992 new_debt_reserves_one(),
1993 "1005438674786548",
1994 12,
1995 12,
1996 limits_wide(),
1997 sync_time,
1998 );
1999
2000 assert_swap_out_result(
2001 true,
2002 U256::from(1_000_000_000_000_000u64),
2003 new_col_reserves_one(),
2004 new_debt_reserves_empty(),
2005 "1002572435818386",
2006 12,
2007 12,
2008 limits_wide(),
2009 sync_time,
2010 );
2011
2012 assert_swap_out_result(
2013 false,
2014 U256::from(1_000_000_000_000_000u64),
2015 new_col_reserves_one(),
2016 new_debt_reserves_one(),
2017 "1001743359733488",
2018 12,
2019 12,
2020 limits_wide(),
2021 sync_time,
2022 );
2023
2024 assert_swap_out_result(
2025 false,
2026 U256::from(1_000_000_000_000_000u64),
2027 new_col_reserves_empty(),
2028 new_debt_reserves_one(),
2029 "1005438674233767",
2030 12,
2031 12,
2032 limits_wide(),
2033 sync_time,
2034 );
2035
2036 assert_swap_out_result(
2037 false,
2038 U256::from(1_000_000_000_000_000u64),
2039 new_col_reserves_one(),
2040 new_debt_reserves_empty(),
2041 "1002572435266527",
2042 12,
2043 12,
2044 limits_wide(),
2045 sync_time,
2046 );
2047 }
2048
2049 #[test]
2050 fn test_swap_out_limits() {
2051 let sync_time_recent = (SystemTime::now()
2052 .duration_since(UNIX_EPOCH)
2053 .unwrap()
2054 .as_secs()) -
2055 10;
2056
2057 let sync_time_expanded = sync_time_recent - 5990; let price = get_approx_center_price_out(
2061 U256::from(1_000_000_000_000_000u64),
2062 true,
2063 &new_col_reserves_one(),
2064 &new_debt_reserves_one(),
2065 )
2066 .unwrap();
2067
2068 let result = swap_out_adjusted(
2069 true,
2070 U256::from(1_000_000_000_000_000u64),
2071 &mut new_col_reserves_one(),
2072 &mut new_debt_reserves_one(),
2073 12,
2074 18,
2075 &mut limits_tight(),
2076 price,
2077 sync_time_recent,
2078 );
2079
2080 assert!(matches!(result, Err(SwapError::InsufficientBorrowable)));
2081
2082 let price = get_approx_center_price_out(
2084 U256::from(1_000_000_000_000_000u64),
2085 true,
2086 &new_col_reserves_one(),
2087 &new_debt_reserves_one(),
2088 )
2089 .unwrap();
2090
2091 let result = swap_out_adjusted(
2092 true,
2093 U256::from(1_000_000_000_000_000u64),
2094 &mut new_col_reserves_one(),
2095 &mut new_debt_reserves_one(),
2096 12,
2097 18,
2098 &mut limits_tight(),
2099 price,
2100 sync_time_expanded,
2101 )
2102 .unwrap();
2103
2104 assert_eq!(from_adjusted_amount(result, 12).to_string(), "1001743360284199");
2105
2106 let price = get_approx_center_price_out(
2108 U256::from(20_000_000_000_000_000u64),
2109 true,
2110 &new_col_reserves_one(),
2111 &new_debt_reserves_one(),
2112 )
2113 .unwrap();
2114
2115 let result = swap_out_adjusted(
2116 true,
2117 U256::from(20_000_000_000_000_000u64),
2118 &mut new_col_reserves_one(),
2119 &mut new_debt_reserves_one(),
2120 12,
2121 18,
2122 &mut limits_wide(),
2123 price,
2124 sync_time_recent,
2125 );
2126
2127 assert!(matches!(result, Err(SwapError::InsufficientMaxPrice)));
2128
2129 let price = get_approx_center_price_out(
2131 U256::from(30_000_000_000_000_000u64),
2132 true,
2133 &new_col_reserves_one(),
2134 &new_debt_reserves_one(),
2135 )
2136 .unwrap();
2137
2138 let result = swap_out_adjusted(
2139 true,
2140 U256::from(30_000_000_000_000_000u64),
2141 &mut new_col_reserves_one(),
2142 &mut new_debt_reserves_one(),
2143 12,
2144 18,
2145 &mut limits_wide(),
2146 price,
2147 sync_time_recent,
2148 );
2149
2150 assert!(matches!(result, Err(SwapError::InsufficientReserve)));
2151 }
2152
2153 #[test]
2154 fn test_swap_out_empty_debt() {
2155 let sync_time = (SystemTime::now()
2156 .duration_since(UNIX_EPOCH)
2157 .unwrap()
2158 .as_secs() as i64) -
2159 10;
2160
2161 assert_swap_out_result(
2163 true,
2164 U256::from(994_619_847_016_724u64),
2165 new_col_reserves_empty(),
2166 new_debt_reserves_one(),
2167 "999999999999999",
2168 12,
2169 12,
2170 limits_wide(),
2171 sync_time,
2172 );
2173
2174 assert_swap_out_result(
2176 false,
2177 U256::from(994_619_847_560_607u64),
2178 new_col_reserves_empty(),
2179 new_debt_reserves_one(),
2180 "999999999999999",
2181 12,
2182 12,
2183 limits_wide(),
2184 sync_time,
2185 );
2186 }
2187
2188 #[test]
2189 fn test_swap_out_empty_collateral() {
2190 let sync_time = (SystemTime::now()
2191 .duration_since(UNIX_EPOCH)
2192 .unwrap()
2193 .as_secs() as i64) -
2194 10;
2195
2196 assert_swap_out_result(
2198 true,
2199 U256::from(997_440_731_289_905u64),
2200 new_col_reserves_one(),
2201 new_debt_reserves_empty(),
2202 "999999999999999",
2203 12,
2204 12,
2205 limits_wide(),
2206 sync_time,
2207 );
2208
2209 assert_swap_out_result(
2211 false,
2212 U256::from(997_440_731_837_532u64),
2213 new_col_reserves_one(),
2214 new_debt_reserves_empty(),
2215 "999999999999999",
2216 12,
2217 12,
2218 limits_wide(),
2219 sync_time,
2220 );
2221 }
2222
2223 pub fn new_verify_ratio_col_reserves() -> CollateralReserves {
2224 CollateralReserves {
2225 token0_real_reserves: U256::from(2_000_000u64) * U256::from(10u64).pow(U256::from(12)),
2226 token1_real_reserves: U256::from(15_000u64) * U256::from(10u64).pow(U256::from(12)),
2227 token0_imaginary_reserves: U256::ZERO,
2228 token1_imaginary_reserves: U256::ZERO,
2229 }
2230 }
2231
2232 pub fn new_verify_ratio_debt_reserves() -> DebtReserves {
2233 DebtReserves {
2234 token0_real_reserves: U256::from(2_000_000u64) * U256::from(10u64).pow(U256::from(12)),
2235 token1_real_reserves: U256::from(15_000u64) * U256::from(10u64).pow(U256::from(12)),
2236 token0_imaginary_reserves: U256::ZERO,
2237 token1_imaginary_reserves: U256::ZERO,
2238 }
2239 }
2240
2241 pub fn calculate_reserves_outside_range(
2243 geometric_mean_price: U256,
2244 price_at_range: U256,
2245 reserve_x: U256,
2246 reserve_y: U256,
2247 ) -> (I256, I256) {
2248 let geometric_mean_price = I256::from(geometric_mean_price);
2249 let price_at_range = I256::from(price_at_range);
2250 let reserve_x = I256::from(reserve_x);
2251 let reserve_y = I256::from(reserve_y);
2252
2253 let one_e27 = I256::from(constant::B_I1E27);
2254 let two = I256::try_from(2i8).unwrap();
2255
2256 let part1 = price_at_range
2258 .checked_sub(geometric_mean_price)
2259 .expect("priceAtRange must be >= geometricMeanPrice");
2260
2261 let part2 = geometric_mean_price
2263 .checked_mul(reserve_x)
2264 .unwrap()
2265 .checked_add(reserve_y.checked_mul(one_e27).unwrap())
2266 .unwrap()
2267 .checked_div(two.checked_mul(part1).unwrap())
2268 .unwrap();
2269
2270 let mut part3 = reserve_x
2272 .checked_mul(reserve_y)
2273 .unwrap();
2274
2275 let one_e50 = I256::try_from(10)
2276 .unwrap()
2277 .pow(U256::from(50));
2278
2279 if part3 < one_e50 {
2281 part3 = part3
2282 .checked_mul(one_e27)
2283 .unwrap()
2284 .checked_div(part1)
2285 .unwrap();
2286 } else {
2287 part3 = part3
2288 .checked_div(part1)
2289 .unwrap()
2290 .checked_mul(one_e27)
2291 .unwrap();
2292 }
2293
2294 let part2_squared = part2.checked_mul(part2).unwrap();
2296 let inside_sqrt = part3
2297 .checked_add(part2_squared)
2298 .unwrap();
2299 let sqrt_value = I256::from(
2300 U256::try_from(inside_sqrt)
2301 .unwrap()
2302 .root(2),
2303 );
2304
2305 let reserve_x_outside = part2.checked_add(sqrt_value).unwrap();
2306
2307 let reserve_y_outside = reserve_x_outside
2309 .checked_mul(geometric_mean_price)
2310 .unwrap()
2311 .checked_div(one_e27)
2312 .unwrap();
2313
2314 (reserve_x_outside, reserve_y_outside)
2315 }
2316
2317 #[test]
2318 fn test_swap_in_verify_reserves_in_range() {
2319 let decimals: i64 = 6;
2320 let mut col_reserves = new_verify_ratio_col_reserves();
2321 let mut debt_reserves = new_verify_ratio_debt_reserves();
2322
2323 let mut price = U256::from_str("1000001000000000000000000000").unwrap();
2324
2325 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2327 constant::B_I1E27,
2328 price,
2329 col_reserves.token0_real_reserves,
2330 col_reserves.token1_real_reserves,
2331 );
2332
2333 col_reserves.token0_imaginary_reserves =
2334 U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2335 col_reserves.token1_imaginary_reserves = U256::from(
2336 I256::from(reserve_y_outside) + I256::from(col_reserves.token1_real_reserves),
2337 );
2338
2339 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2341 constant::B_I1E27,
2342 price,
2343 debt_reserves.token0_real_reserves,
2344 debt_reserves.token1_real_reserves,
2345 );
2346
2347 debt_reserves.token0_imaginary_reserves =
2348 U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2349 debt_reserves.token1_imaginary_reserves = U256::from(
2350 I256::from(reserve_y_outside) + I256::from(debt_reserves.token1_real_reserves),
2351 );
2352
2353 let sync_time = SystemTime::now()
2354 .duration_since(UNIX_EPOCH)
2355 .unwrap()
2356 .as_secs() -
2357 10;
2358
2359 let swap_amount = U256::from(14_905) * U256::from(10).pow(U256::from(12)); price = get_approx_center_price_in(
2362 swap_amount,
2363 true,
2364 &col_reserves,
2365 &new_debt_reserves_empty(),
2366 )
2367 .unwrap();
2368 let result = swap_in_adjusted(
2369 true,
2370 swap_amount,
2371 &mut col_reserves,
2372 &mut new_debt_reserves_empty(),
2373 decimals,
2374 &mut limits_wide(),
2375 price,
2376 sync_time,
2377 );
2378 assert!(
2379 result.is_err(),
2380 "FAIL: reserves ratio revert NOT hit for col reserves when swap amount 14_905"
2381 );
2382
2383 price = get_approx_center_price_in(
2384 swap_amount,
2385 true,
2386 &new_col_reserves_empty(),
2387 &debt_reserves,
2388 )
2389 .unwrap();
2390 let result = swap_in_adjusted(
2391 true,
2392 swap_amount,
2393 &mut new_col_reserves_empty(),
2394 &mut debt_reserves,
2395 decimals,
2396 &mut limits_wide(),
2397 price,
2398 sync_time,
2399 );
2400 assert!(
2401 result.is_err(),
2402 "FAIL: reserves ratio revert NOT hit for debt reserves when swap amount 14_905"
2403 );
2404
2405 col_reserves = new_verify_ratio_col_reserves();
2407 debt_reserves = new_verify_ratio_debt_reserves();
2408
2409 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2410 constant::B_I1E27,
2411 price,
2412 col_reserves.token0_real_reserves,
2413 col_reserves.token1_real_reserves,
2414 );
2415
2416 col_reserves.token0_imaginary_reserves =
2417 U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2418 col_reserves.token1_imaginary_reserves = U256::from(
2419 I256::from(reserve_y_outside) + I256::from(col_reserves.token1_real_reserves),
2420 );
2421
2422 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2423 constant::B_I1E27,
2424 price,
2429 debt_reserves.token0_real_reserves,
2430 debt_reserves.token1_real_reserves,
2431 );
2432 debt_reserves.token0_imaginary_reserves =
2433 U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2434 debt_reserves.token1_imaginary_reserves = U256::from(
2435 I256::from(reserve_y_outside) + I256::from(debt_reserves.token1_real_reserves),
2436 );
2437
2438 let swap_amount = U256::from(14_895) * U256::from(10).pow(U256::from(12));
2440
2441 price = get_approx_center_price_in(
2442 swap_amount,
2443 true,
2444 &col_reserves,
2445 &new_debt_reserves_empty(),
2446 )
2447 .unwrap();
2448 let result = swap_in_adjusted(
2449 true,
2450 swap_amount,
2451 &mut col_reserves,
2452 &mut new_debt_reserves_empty(),
2453 decimals,
2454 &mut limits_wide(),
2455 price,
2456 sync_time,
2457 );
2458 assert!(
2459 result.is_ok(),
2460 "FAIL: reserves ratio revert hit for col reserves when swap amount 14_895"
2461 );
2462
2463 price = get_approx_center_price_in(
2464 swap_amount,
2465 true,
2466 &new_col_reserves_empty(),
2467 &debt_reserves,
2468 )
2469 .unwrap();
2470 let result = swap_in_adjusted(
2471 true,
2472 swap_amount,
2473 &mut new_col_reserves_empty(),
2474 &mut debt_reserves,
2475 decimals,
2476 &mut limits_wide(),
2477 price,
2478 sync_time,
2479 );
2480 assert!(
2481 result.is_ok(),
2482 "FAIL: reserves ratio revert hit for debt reserves when swap amount 14_895"
2483 );
2484 }
2485
2486 pub fn new_verify_ratio_col_reserves_swap_out() -> CollateralReserves {
2487 CollateralReserves {
2488 token0_real_reserves: U256::from(15_000u64) * U256::from(10u64).pow(U256::from(12)), token1_real_reserves: U256::from(2_000_000u64) * U256::from(10u64).pow(U256::from(12)), token0_imaginary_reserves: U256::ZERO,
2491 token1_imaginary_reserves: U256::ZERO,
2492 }
2493 }
2494
2495 pub fn new_verify_ratio_debt_reserves_swap_out() -> DebtReserves {
2496 DebtReserves {
2497 token0_real_reserves: U256::from(15_000u64) * U256::from(10u64).pow(U256::from(12)),
2498 token1_real_reserves: U256::from(2_000_000u64) * U256::from(10u64).pow(U256::from(12)),
2499 token0_imaginary_reserves: U256::ZERO,
2500 token1_imaginary_reserves: U256::ZERO,
2501 }
2502 }
2503
2504 #[test]
2505 fn test_swap_out_verify_reserves_in_range() {
2506 let decimals: i64 = 6;
2507 let sync_time = SystemTime::now()
2508 .duration_since(UNIX_EPOCH)
2509 .unwrap()
2510 .as_secs() -
2511 10;
2512
2513 let mut col_reserves = new_verify_ratio_col_reserves_swap_out();
2514 let mut debt_reserves = new_verify_ratio_debt_reserves_swap_out();
2515
2516 let price = U256::from_str("1000001000000000000000000000").unwrap();
2518
2519 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2521 constant::B_I1E27,
2522 price,
2523 col_reserves.token0_real_reserves,
2524 col_reserves.token1_real_reserves,
2525 );
2526 col_reserves.token0_imaginary_reserves =
2527 U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2528 col_reserves.token1_imaginary_reserves =
2529 U256::from(reserve_y_outside + I256::from(col_reserves.token1_real_reserves));
2530
2531 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2532 constant::B_I1E27,
2533 price,
2534 debt_reserves.token0_real_reserves,
2535 debt_reserves.token1_real_reserves,
2536 );
2537 debt_reserves.token0_imaginary_reserves =
2538 U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2539 debt_reserves.token1_imaginary_reserves =
2540 U256::from(reserve_y_outside + I256::from(debt_reserves.token1_real_reserves));
2541
2542 let swap_amount = U256::from(14_766u64) * U256::from(10u64).pow(U256::from(12));
2544
2545 let price = get_approx_center_price_out(
2546 swap_amount,
2547 false,
2548 &col_reserves,
2549 &new_debt_reserves_empty(),
2550 )
2551 .unwrap();
2552 let result = swap_out_adjusted(
2553 false,
2554 swap_amount,
2555 &mut col_reserves,
2556 &mut new_debt_reserves_empty(),
2557 decimals,
2558 decimals,
2559 &mut limits_wide(),
2560 price,
2561 sync_time,
2562 );
2563 assert!(result.is_err(), "FAIL: reserves ratio verification revert NOT hit for col reserves when swap amount 14_766");
2564
2565 let price = get_approx_center_price_out(
2566 swap_amount,
2567 false,
2568 &new_col_reserves_empty(),
2569 &debt_reserves,
2570 )
2571 .unwrap();
2572 let result = swap_out_adjusted(
2573 false,
2574 swap_amount,
2575 &mut new_col_reserves_empty(),
2576 &mut debt_reserves,
2577 decimals,
2578 decimals,
2579 &mut limits_wide(),
2580 price,
2581 sync_time,
2582 );
2583 assert!(result.is_err(), "FAIL: reserves ratio verification revert NOT hit for debt reserves when swap amount 14_766");
2584
2585 col_reserves = new_verify_ratio_col_reserves_swap_out();
2587 debt_reserves = new_verify_ratio_debt_reserves_swap_out();
2588
2589 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2590 constant::B_I1E27,
2591 price,
2592 col_reserves.token0_real_reserves,
2593 col_reserves.token1_real_reserves,
2594 );
2595 col_reserves.token0_imaginary_reserves =
2596 U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2597 col_reserves.token1_imaginary_reserves =
2598 U256::from(reserve_y_outside + I256::from(col_reserves.token1_real_reserves));
2599
2600 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2601 constant::B_I1E27,
2602 price,
2603 debt_reserves.token0_real_reserves,
2604 debt_reserves.token1_real_reserves,
2605 );
2606 debt_reserves.token0_imaginary_reserves =
2607 U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2608 debt_reserves.token1_imaginary_reserves =
2609 U256::from(reserve_y_outside + I256::from(debt_reserves.token1_real_reserves));
2610
2611 let swap_amount = U256::from(14_762u64) * U256::from(10u64).pow(U256::from(12));
2613
2614 let price = get_approx_center_price_out(
2615 swap_amount,
2616 false,
2617 &col_reserves,
2618 &new_debt_reserves_empty(),
2619 )
2620 .unwrap();
2621 let result = swap_out_adjusted(
2622 false,
2623 swap_amount,
2624 &mut col_reserves,
2625 &mut new_debt_reserves_empty(),
2626 decimals,
2627 decimals,
2628 &mut limits_wide(),
2629 price,
2630 sync_time,
2631 );
2632 assert!(
2633 result.is_ok(),
2634 "FAIL: reserves ratio verification revert hit for col reserves when swap amount 14_762"
2635 );
2636
2637 let price = get_approx_center_price_out(
2638 swap_amount,
2639 false,
2640 &new_col_reserves_empty(),
2641 &debt_reserves,
2642 )
2643 .unwrap();
2644 let result = swap_out_adjusted(
2645 false,
2646 swap_amount,
2647 &mut new_col_reserves_empty(),
2648 &mut debt_reserves,
2649 decimals,
2650 decimals,
2651 &mut limits_wide(),
2652 price,
2653 sync_time,
2654 );
2655 assert!(result.is_ok(), "FAIL: reserves ratio verification revert hit for debt reserves when swap amount 14_762");
2656 }
2657
2658 fn hard_limit(l: u128) -> TokenLimit {
2675 TokenLimit {
2676 available: U256::from(l),
2677 expands_to: U256::from(l),
2678 expand_duration: U256::ZERO,
2679 }
2680 }
2681
2682 fn wsteth_eth_pool_23526115() -> (Token, Token, FluidV1) {
2683 let wsteth = Token::new(
2684 &Bytes::from_str("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0").unwrap(),
2685 "wsteth",
2686 18,
2687 0,
2688 &[Some(20000)],
2689 Chain::Ethereum,
2690 100,
2691 );
2692 let eth = Token::new(
2693 &Bytes::from_str("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE").unwrap(),
2694 "ETH",
2695 18,
2696 0,
2697 &[Some(2000)],
2698 Chain::Ethereum,
2699 100,
2700 );
2701 let pool = FluidV1::new(
2702 &Bytes::from_str("0x0B1a513ee24972DAEf112bC777a5610d4325C9e7").unwrap(),
2703 &wsteth,
2704 ð,
2705 CollateralReserves {
2706 token0_real_reserves: U256::from(4431191840536456u128),
2707 token1_real_reserves: U256::from(13105569017021951u128),
2708 token0_imaginary_reserves: U256::from(20263646714209556492u128),
2709 token1_imaginary_reserves: U256::from(24624319733997222300u128),
2710 },
2711 DebtReserves {
2712 token0_real_reserves: U256::from(3958052320699256u128),
2713 token1_real_reserves: U256::from(11706224851989005u128),
2714 token0_imaginary_reserves: U256::from(18100000404581051720u128),
2715 token1_imaginary_reserves: U256::from(21995063545785045888u128),
2716 },
2717 DexLimits {
2718 borrowable_token0: hard_limit(4431191840536456767040),
2719 borrowable_token1: hard_limit(6552784508510975527319),
2720 withdrawable_token0: hard_limit(4819160955805377144139),
2721 withdrawable_token1: hard_limit(6126272539623278413525),
2722 },
2723 U256::from_str("1215727283480584508000000000").unwrap(),
2724 U256::from(68),
2725 1759795200,
2726 );
2727 (wsteth, eth, pool)
2728 }
2729
2730 #[test]
2731 fn test_spot_price() {
2732 let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
2733 let exp_spot0 = 1.21519682; let exp_spot1 = 0.82291191; let spot0 = pool.spot_price(&wsteth, ð).unwrap();
2738 let spot1 = pool.spot_price(ð, &wsteth).unwrap();
2739
2740 let rel_err0 = (spot0 - exp_spot0).abs() / exp_spot0;
2741 let rel_err1 = (spot1 - exp_spot1).abs() / exp_spot1;
2742
2743 assert!(
2744 rel_err0 < 1e-4,
2745 "spot0 mismatch: got {spot0}, expected {exp_spot0}, relative error: {rel_err0}"
2746 );
2747 assert!(
2748 rel_err1 < 1e-4,
2749 "spot1 mismatch: got {spot1}, expected {exp_spot1}, relative error: {rel_err1}"
2750 );
2751 }
2752
2753 #[test]
2754 fn test_get_amount_out_zero2one() {
2755 let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
2756 let amount_in = BigUint::from_str_radix("100000000000000", 10).unwrap();
2757 let exp_amount_out = BigUint::from_str_radix("121511421000000", 10).unwrap();
2759
2760 let res = pool
2761 .get_amount_out(amount_in, &wsteth, ð)
2762 .unwrap();
2763
2764 assert_eq!(res.amount, exp_amount_out);
2765 }
2766
2767 #[test]
2768 fn test_get_amount_out_one2zero() {
2769 let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
2770 let amount_in = BigUint::from_str_radix("100000000000000", 10).unwrap();
2771 let exp_amount_out = BigUint::from_str_radix("82285598000000", 10).unwrap();
2773
2774 let res = pool
2775 .get_amount_out(amount_in, ð, &wsteth)
2776 .unwrap();
2777 assert_eq!(res.amount, exp_amount_out);
2778 }
2779
2780 #[test]
2781 fn get_limits_zero2one() {
2782 let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
2783
2784 let (max_amount_in, _) = pool
2785 .get_limits(wsteth.address.clone(), eth.address.clone())
2786 .unwrap();
2787 let max_amount_onchain_test =
2788 BigUint::from_str_radix("10200000000000000000000", 10).unwrap();
2790
2791 let _ = pool
2792 .get_amount_out(max_amount_in.clone(), &wsteth, ð)
2793 .unwrap();
2794 assert!(max_amount_in < max_amount_onchain_test);
2795 }
2796
2797 #[test]
2798 fn get_limits_one2zero() {
2799 let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
2800
2801 let (max_amount_in, _) = pool
2802 .get_limits(eth.address.clone(), wsteth.address.clone())
2803 .unwrap();
2804 let max_amount_onchain_test =
2805 BigUint::from_str_radix("10192694739404003000000", 10).unwrap();
2807
2808 let _ = pool
2809 .get_amount_out(max_amount_in.clone(), ð, &wsteth)
2810 .unwrap();
2811
2812 assert!(max_amount_in < max_amount_onchain_test);
2813 }
2814}