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