1use std::{
12 any::Any,
13 collections::HashMap,
14 time::{SystemTime, UNIX_EPOCH},
15};
16
17use alloy::primitives::{U256, U512};
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::{
29 Balances, GetAmountOutResult, PoolSwap, Price, ProtocolSim, QueryPoolSwapParams,
30 SwapConstraint,
31 },
32 },
33 Bytes,
34};
35
36use crate::evm::{
37 engine_db::{create_engine, SHARED_TYCHO_DB},
38 protocol::{
39 fluid::{v1::constant::RESERVES_RESOLVER, vm},
40 safe_math::sqrt_u512,
41 u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
42 utils::add_fee_markup,
43 },
44 query_pool_swap::price_to_f64_with_decimals,
45};
46
47mod constant {
48 use alloy::{hex, primitives::U256};
49
50 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;
57 pub const FEE_PERCENT_PRECISION: U256 = U256::from_limbs([10000, 0, 0, 0]);
58 pub const ZERO_ADDRESS: &[u8] = &hex!("0x0000000000000000000000000000000000000000");
59 pub const NATIVE_ADDRESS: &[u8] = &hex!("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE");
60 pub const RESERVES_RESOLVER: &[u8] = &hex!("0xc93876c0eed99645dd53937b25433e311881a27c");
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64pub struct FluidV1 {
65 pool_address: Bytes,
66 token0: Token,
67 token1: Token,
68 collateral_reserves: CollateralReserves,
69 debt_reserves: DebtReserves,
70 dex_limits: DexLimits,
71 center_price: U256,
72 fee: U256,
73 sync_time: u64,
74 pool_reserve0: U256,
75 pool_reserve1: U256,
76}
77
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub(super) struct CollateralReserves {
80 pub(super) token0_real_reserves: U256,
81 pub(super) token1_real_reserves: U256,
82 pub(super) token0_imaginary_reserves: U256,
83 pub(super) token1_imaginary_reserves: U256,
84}
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub(super) struct DebtReserves {
88 pub(super) token0_real_reserves: U256,
89 pub(super) token1_real_reserves: U256,
90 pub(super) token0_imaginary_reserves: U256,
91 pub(super) token1_imaginary_reserves: U256,
92}
93
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub(super) struct DexLimits {
96 pub(super) borrowable_token0: TokenLimit,
97 pub(super) borrowable_token1: TokenLimit,
98 pub(super) withdrawable_token0: TokenLimit,
99 pub(super) withdrawable_token1: TokenLimit,
100}
101
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103pub(super) struct TokenLimit {
104 pub(super) available: U256,
105 pub(super) expands_to: U256,
106 pub(super) expand_duration: U256,
107}
108
109#[derive(Debug, Error)]
110enum SwapError {
111 #[error("Insufficient reserve: tokenOut amount exceeds reserve")]
112 InsufficientReserve,
113 #[error("Insufficient reserve: tokenOut amount exceeds borrowable limit")]
114 InsufficientBorrowable,
115 #[error("Insufficient reserve: tokenOut amount exceeds withdrawable limit")]
116 InsufficientWithdrawable,
117 #[error("Insufficient reserve: tokenOut amount exceeds max price limit")]
118 InsufficientMaxPrice,
119 #[error("Invalid reserves ratio")]
120 VerifyReservesRatiosInvalid,
121 #[error("No pools are enabled")]
122 NoPoolsEnabled,
123 #[error("InvalidAmountIn: Amount too low")]
124 InvalidAmountIn,
125}
126
127impl From<SwapError> for SimulationError {
128 fn from(value: SwapError) -> Self {
129 Self::FatalError(value.to_string())
130 }
131}
132impl FluidV1 {
133 #[allow(clippy::too_many_arguments)]
134 pub(super) fn new(
135 pool_address: &Bytes,
136 token0: &Token,
137 token1: &Token,
138 collateral_reserves: CollateralReserves,
139 debt_reserves: DebtReserves,
140 dex_limits: DexLimits,
141 center_price: U256,
142 fee: U256,
143 sync_time: u64,
144 ) -> Self {
145 let pool_reserve0 = get_max_reserves(
146 token0.decimals as u8,
147 &dex_limits.withdrawable_token0,
148 &dex_limits.borrowable_token0,
149 &collateral_reserves.token0_real_reserves,
150 &debt_reserves.token0_real_reserves,
151 );
152 let pool_reserve1 = get_max_reserves(
153 token1.decimals as u8,
154 &dex_limits.withdrawable_token1,
155 &dex_limits.borrowable_token1,
156 &collateral_reserves.token1_real_reserves,
157 &debt_reserves.token1_real_reserves,
158 );
159
160 let (token0_normalized, token1_normalized) =
163 if FluidV1::normalize_native_address(&token0.address) <
164 FluidV1::normalize_native_address(&token1.address)
165 {
166 (token0.clone(), token1.clone())
167 } else {
168 (token1.clone(), token0.clone())
169 };
170 Self {
171 pool_address: pool_address.clone(),
172 token0: token0_normalized,
173 token1: token1_normalized,
174 collateral_reserves,
175 debt_reserves,
176 dex_limits,
177 center_price,
178 fee,
179 sync_time,
180 pool_reserve0,
181 pool_reserve1,
182 }
183 }
184
185 fn normalize_native_address(address: &Bytes) -> &[u8] {
186 if address == constant::ZERO_ADDRESS {
187 constant::NATIVE_ADDRESS
188 } else {
189 address
190 }
191 }
192
193 fn closed_form_amount_in(
197 &self,
198 target: &Price,
199 token_in: &Token,
200 token_out: &Token,
201 ) -> Option<BigUint> {
202 let (col, debt) = (&self.collateral_reserves, &self.debt_reserves);
203 let sub_pools = if token_in.address == self.token0.address {
204 [
205 (col.token0_imaginary_reserves, col.token1_imaginary_reserves),
206 (debt.token0_imaginary_reserves, debt.token1_imaginary_reserves),
207 ]
208 } else {
209 [
210 (col.token1_imaginary_reserves, col.token0_imaginary_reserves),
211 (debt.token1_imaginary_reserves, debt.token0_imaginary_reserves),
212 ]
213 };
214 let ten_pow = |exponent: i64| U512::from(10u64).pow(U512::from(exponent.unsigned_abs()));
215 let fee_den = U512::from(constant::SIX_DECIMALS);
216 let fee_num = U512::from(constant::SIX_DECIMALS - self.fee);
217
218 let mut ratio_num =
220 U512::try_from_be_slice(&target.numerator.to_bytes_be())?.checked_mul(fee_num)?;
221 let mut ratio_den =
222 U512::try_from_be_slice(&target.denominator.to_bytes_be())?.checked_mul(fee_den)?;
223 let decimals_shift = token_in.decimals as i64 - token_out.decimals as i64;
225 if decimals_shift >= 0 {
226 ratio_num = ratio_num.checked_mul(ten_pow(decimals_shift))?;
227 } else {
228 ratio_den = ratio_den.checked_mul(ten_pow(decimals_shift))?;
229 }
230
231 let mut amount_adjusted = U512::ZERO;
232 for (reserve_in, reserve_out) in sub_pools {
233 let reserve_in = U512::from(reserve_in);
234 let k = reserve_in.checked_mul(U512::from(reserve_out))?;
236 let new_reserve_in = sqrt_u512(
237 k.checked_mul(ratio_den)?
238 .checked_div(ratio_num)?,
239 );
240 amount_adjusted =
241 amount_adjusted.checked_add(new_reserve_in.saturating_sub(reserve_in))?;
242 }
243
244 let dex_shift = token_in.decimals as i64 - constant::DEX_AMOUNT_DECIMALS;
245 let amount_after_fee = if dex_shift >= 0 {
246 amount_adjusted.checked_mul(ten_pow(dex_shift))?
247 } else {
248 amount_adjusted / ten_pow(dex_shift)
249 };
250 let amount_in = amount_after_fee.checked_mul(fee_den)? / fee_num;
251 Some(u256_to_biguint(U256::checked_from_limbs_slice(amount_in.as_limbs())?))
252 }
253
254 fn swap_to_target_price(
257 &self,
258 params: &QueryPoolSwapParams,
259 target: &Price,
260 tolerance: f64,
261 ) -> Result<PoolSwap, SimulationError> {
262 let (token_in, token_out) = (params.token_in(), params.token_out());
263 let spot = self.spot_price(token_in, token_out)?;
264 let target_f64 = price_to_f64_with_decimals(target, token_in.decimals, token_out.decimals)?;
265 if target_f64 > spot {
266 return Err(SimulationError::InvalidInput(
267 format!("Target price {target_f64} is above spot price {spot}"),
268 None,
269 ));
270 }
271 if target_f64 == spot {
272 return Ok(PoolSwap::new(BigUint::ZERO, BigUint::ZERO, self.clone_box(), None));
273 }
274
275 if let Some(amount_in) = self.closed_form_amount_in(target, token_in, token_out) {
278 if let Ok(result) = self.get_amount_out(amount_in.clone(), token_in, token_out) {
279 let new_spot = result
280 .new_state
281 .spot_price(token_in, token_out)?;
282 let lowest = target_f64 * (1.0 - 1e-12);
284 let highest = target_f64 * (1.0 + tolerance) * (1.0 + 1e-12);
285 if (lowest..=highest).contains(&new_spot) {
286 return Ok(PoolSwap::new(amount_in, result.amount, result.new_state, None));
287 }
288 }
289 }
290 crate::evm::query_pool_swap::query_pool_swap(self, params)
291 }
292}
293
294fn decode_block_timestamp(attributes: &HashMap<String, Bytes>) -> Result<u64, TransitionError> {
295 let bytes = attributes
296 .get(vm::BLOCK_TIMESTAMP_ATTRIBUTE)
297 .ok_or_else(|| {
298 TransitionError::MissingAttribute(vm::BLOCK_TIMESTAMP_ATTRIBUTE.to_string())
299 })?;
300 let timestamp = <[u8; 8]>::try_from(bytes.as_ref()).map_err(|_| {
301 TransitionError::DecodeError(format!(
302 "{} must be an 8-byte big-endian u64, got {} bytes",
303 vm::BLOCK_TIMESTAMP_ATTRIBUTE,
304 bytes.len()
305 ))
306 })?;
307 Ok(u64::from_be_bytes(timestamp))
308}
309
310#[typetag::serde]
311impl ProtocolSim for FluidV1 {
312 fn fee(&self) -> f64 {
313 let fee = u256_to_f64(self.fee).expect("Fluid fee values are safe to convert");
314 let precision =
315 u256_to_f64(constant::FEE_PERCENT_PRECISION).expect("FEE_PERCENT_PRECISION is safe");
316 fee / precision / 100.0
319 }
320
321 fn spot_price(&self, base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
322 let price_f64 = if !self
323 .collateral_reserves
324 .token0_imaginary_reserves
325 .is_zero()
326 {
327 u256_to_f64(
328 self.collateral_reserves
329 .token1_imaginary_reserves,
330 )? / u256_to_f64(
331 self.collateral_reserves
332 .token0_imaginary_reserves,
333 )?
334 } else {
335 u256_to_f64(
336 self.debt_reserves
337 .token1_imaginary_reserves,
338 )? / u256_to_f64(
339 self.debt_reserves
340 .token0_imaginary_reserves,
341 )?
342 };
343 let oriented_price_f64 =
344 if base.address == self.token0.address { price_f64 } else { 1.0 / price_f64 };
345
346 Ok(add_fee_markup(oriented_price_f64, self.fee()))
347 }
348
349 fn get_amount_out(
350 &self,
351 amount_in: BigUint,
352 token_in: &Token,
353 token_out: &Token,
354 ) -> Result<GetAmountOutResult, SimulationError> {
355 if amount_in == BigUint::from(0u32) {
356 return Ok(GetAmountOutResult {
357 amount: BigUint::from(0u32),
358 gas: BigUint::from(155433u32),
359 new_state: Box::new(self.clone()),
360 });
361 }
362 let zero2one = self.token0.address == token_in.address;
363
364 let (token_in_decimals, token_out_decimals) = (token_in.decimals, token_out.decimals);
365
366 let amount_in = biguint_to_u256(&amount_in);
367 let fee = amount_in * self.fee / constant::SIX_DECIMALS;
368
369 let amount_in_after_fee = amount_in - fee;
370 let amount_in_adjusted = to_adjusted_amount(amount_in_after_fee, token_in_decimals as i64);
371
372 if amount_in_adjusted < constant::SIX_DECIMALS ||
373 amount_in_after_fee < constant::TWO_DECIMALS
374 {
375 return Err(SwapError::InvalidAmountIn.into());
376 }
377 let mut new_col_reserves = self.collateral_reserves.clone();
378 let mut new_debt_reserves = self.debt_reserves.clone();
379 let mut new_limits = self.dex_limits.clone();
380
381 let amount_out = swap_in_adjusted(
382 zero2one,
383 amount_in_adjusted,
384 &mut new_col_reserves,
385 &mut new_debt_reserves,
386 token_out_decimals as i64,
387 &mut new_limits,
388 self.center_price,
389 self.sync_time,
390 )?;
391
392 let reserve = if zero2one { self.pool_reserve1 } else { self.pool_reserve0 };
393 if amount_out > reserve {
394 return Err(SwapError::InsufficientReserve.into());
395 }
396
397 let result = GetAmountOutResult::new(
398 u256_to_biguint(amount_out),
399 155433.to_biguint().expect("infallible"),
400 Box::new(Self {
401 pool_address: self.pool_address.clone(),
402 token0: self.token0.clone(),
403 token1: self.token1.clone(),
404 collateral_reserves: new_col_reserves,
405 debt_reserves: new_debt_reserves,
406 dex_limits: new_limits,
407 center_price: self.center_price,
408 fee: self.fee,
409 sync_time: self.sync_time,
410 pool_reserve0: self.pool_reserve0,
411 pool_reserve1: self.pool_reserve1,
412 }),
413 );
414 Ok(result)
415 }
416
417 fn get_limits(
418 &self,
419 sell_token: Bytes,
420 buy_token: Bytes,
421 ) -> Result<(BigUint, BigUint), SimulationError> {
422 let zero2one = sell_token == self.token0.address;
423
424 let (upper_bound_out, out_decimals, in_decimals) = if zero2one {
425 (
426 to_adjusted_amount(
427 self.dex_limits
428 .withdrawable_token0
429 .available +
430 self.dex_limits
431 .borrowable_token0
432 .available,
433 self.token0.decimals as i64,
434 ),
435 self.token1.decimals,
436 self.token0.decimals,
437 )
438 } else {
439 (
440 to_adjusted_amount(
441 self.dex_limits
442 .withdrawable_token1
443 .available +
444 self.dex_limits
445 .borrowable_token1
446 .available,
447 self.token1.decimals as i64,
448 ),
449 self.token0.decimals,
450 self.token1.decimals,
451 )
452 };
453 if upper_bound_out == U256::ZERO {
454 trace!("Upper bound is zero for {}", self.pool_address);
455 return Ok((BigUint::ZERO, BigUint::ZERO));
456 }
457 let delta = U256::from(10).pow(U256::from(2));
458 let (max_valid, res) = find_max_valid_u256(upper_bound_out, delta, |amount| {
459 let mut col_clone = self.collateral_reserves.clone();
460 let mut debt_clone = self.debt_reserves.clone();
461 let mut limits_clone = self.dex_limits.clone();
462 swap_in_adjusted(
463 zero2one,
464 amount,
465 &mut col_clone,
466 &mut debt_clone,
467 out_decimals as i64,
468 &mut limits_clone,
469 self.center_price,
470 self.sync_time,
471 )
472 });
473 Ok((
474 u256_to_biguint(from_adjusted_amount(max_valid, in_decimals as i64)),
475 u256_to_biguint(res.unwrap_or_else(|| {
476 trace!(
477 "All evaluations errored during limit search for {} -> {}",
478 sell_token,
479 buy_token
480 );
481 U256::ZERO
482 })),
483 ))
484 }
485
486 fn delta_transition(
489 &mut self,
490 delta: ProtocolStateDelta,
491 _tokens: &HashMap<Bytes, Token>,
492 _balances: &Balances,
493 ) -> Result<(), TransitionError> {
494 let state = match delta
495 .updated_attributes
496 .get(vm::POOL_RESERVES_ADJUSTED_ATTRIBUTE)
497 {
498 Some(reserves) => {
499 let sync_time = decode_block_timestamp(&delta.updated_attributes)?;
500 vm::decode_reserves(reserves, sync_time)?
501 }
502 None => {
503 let engine = create_engine(SHARED_TYCHO_DB.clone(), false).expect("Infallible");
504 vm::fetch_pool_state(&self.pool_address, RESERVES_RESOLVER, &engine)?
505 }
506 };
507
508 trace!(?state, "Calling delta transition for {}", &self.pool_address);
509
510 self.collateral_reserves = state.collateral_reserves;
511 self.debt_reserves = state.debt_reserves;
512 self.dex_limits = state.dex_limits;
513 self.center_price = state.center_price;
514 self.fee = state.fee;
515 self.sync_time = state.sync_time;
516
517 self.pool_reserve0 = get_max_reserves(
518 self.token0.decimals as u8,
519 &self.dex_limits.withdrawable_token0,
520 &self.dex_limits.borrowable_token0,
521 &self
522 .collateral_reserves
523 .token0_real_reserves,
524 &self.debt_reserves.token0_real_reserves,
525 );
526 self.pool_reserve1 = get_max_reserves(
527 self.token1.decimals as u8,
528 &self.dex_limits.withdrawable_token1,
529 &self.dex_limits.borrowable_token1,
530 &self
531 .collateral_reserves
532 .token1_real_reserves,
533 &self.debt_reserves.token1_real_reserves,
534 );
535 Ok(())
536 }
537
538 fn clone_box(&self) -> Box<dyn ProtocolSim> {
539 Box::new(self.clone())
540 }
541
542 fn as_any(&self) -> &dyn Any {
543 self
544 }
545
546 fn as_any_mut(&mut self) -> &mut dyn Any {
547 self
548 }
549
550 fn eq(&self, other: &dyn ProtocolSim) -> bool {
551 if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
552 self == other_state
553 } else {
554 false
555 }
556 }
557
558 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
559 match params.swap_constraint() {
560 SwapConstraint::PoolTargetPrice { target, tolerance, .. } => {
561 self.swap_to_target_price(params, target, *tolerance)
562 }
563 SwapConstraint::TradeLimitPrice { .. } => {
564 crate::evm::query_pool_swap::query_pool_swap(self, params)
565 }
566 }
567 }
568}
569
570pub fn find_max_valid_u256<T, E, F>(upper_bound: U256, delta: U256, mut f: F) -> (U256, Option<T>)
580where
581 F: FnMut(U256) -> Result<T, E>,
582 E: std::fmt::Debug,
583{
584 let mut low = U256::ZERO;
585 let mut high = upper_bound;
586 let mut best = U256::ZERO;
587 let mut best_result: Option<T> = None;
588
589 while high > low + delta {
590 let mid = (low + high) / U256::from(2);
591
592 match f(mid) {
593 Ok(result) => {
594 best = mid;
595 best_result = Some(result);
596 low = mid;
597 }
598 Err(_) => {
599 high = mid;
600 }
601 }
602 }
603
604 (best, best_result)
605}
606
607#[allow(clippy::too_many_arguments)]
608fn swap_in_adjusted(
609 swap0_to_1: bool,
610 amount_to_swap: U256,
611 col_reserves: &mut CollateralReserves,
612 debt_reserves: &mut DebtReserves,
613 out_decimals: i64,
614 current_limits: &mut DexLimits,
615 center_price: U256,
616 sync_time: u64,
617) -> Result<U256, SwapError> {
618 let (
619 col_reserve_in,
620 col_reserve_out,
621 col_i_reserve_in,
622 col_i_reserve_out,
623 debt_reserve_in,
624 debt_reserve_out,
625 debt_i_reserve_in,
626 debt_i_reserve_out,
627 borrowable,
628 withdrawable,
629 ) = if swap0_to_1 {
630 (
631 col_reserves.token0_real_reserves,
632 col_reserves.token1_real_reserves,
633 col_reserves.token0_imaginary_reserves,
634 col_reserves.token1_imaginary_reserves,
635 debt_reserves.token0_real_reserves,
636 debt_reserves.token1_real_reserves,
637 debt_reserves.token0_imaginary_reserves,
638 debt_reserves.token1_imaginary_reserves,
639 get_expanded_limit(sync_time, ¤t_limits.borrowable_token1),
640 get_expanded_limit(sync_time, ¤t_limits.withdrawable_token1),
641 )
642 } else {
643 (
644 col_reserves.token1_real_reserves,
645 col_reserves.token0_real_reserves,
646 col_reserves.token1_imaginary_reserves,
647 col_reserves.token0_imaginary_reserves,
648 debt_reserves.token1_real_reserves,
649 debt_reserves.token0_real_reserves,
650 debt_reserves.token1_imaginary_reserves,
651 debt_reserves.token0_imaginary_reserves,
652 get_expanded_limit(sync_time, ¤t_limits.borrowable_token0),
653 get_expanded_limit(sync_time, ¤t_limits.withdrawable_token0),
654 )
655 };
656
657 let borrowable = to_adjusted_amount(borrowable, out_decimals);
659 let withdrawable = to_adjusted_amount(withdrawable, out_decimals);
660
661 let col_pool_enabled = col_reserves.token0_real_reserves > U256::ZERO &&
663 col_reserves.token1_real_reserves > U256::ZERO &&
664 col_reserves.token0_imaginary_reserves > U256::ZERO &&
665 col_reserves.token1_imaginary_reserves > U256::ZERO;
666
667 let debt_pool_enabled = debt_reserves.token0_real_reserves > U256::ZERO &&
668 debt_reserves.token1_real_reserves > U256::ZERO &&
669 debt_reserves.token0_imaginary_reserves > U256::ZERO &&
670 debt_reserves.token1_imaginary_reserves > U256::ZERO;
671
672 if !col_pool_enabled && !debt_pool_enabled {
673 return Err(SwapError::NoPoolsEnabled);
674 }
675
676 let a = if col_pool_enabled && debt_pool_enabled {
677 swap_routing_in(
678 amount_to_swap,
679 col_i_reserve_out,
680 col_i_reserve_in,
681 debt_i_reserve_out,
682 debt_i_reserve_in,
683 )
684 } else if debt_pool_enabled {
685 U256::MAX } else if col_pool_enabled {
687 amount_to_swap + U256::ONE } else {
689 return Err(SwapError::NoPoolsEnabled);
690 };
691
692 let (amount_in_collateral, amount_out_collateral, amount_in_debt, amount_out_debt) = if a ==
693 U256::ZERO ||
694 a == U256::MAX
695 {
696 let amount_out_debt = get_amount_out(amount_to_swap, debt_i_reserve_in, debt_i_reserve_out);
698 (U256::ZERO, U256::ZERO, amount_to_swap, amount_out_debt)
699 } else if a >= amount_to_swap {
700 let amount_out_collateral =
702 get_amount_out(amount_to_swap, col_i_reserve_in, col_i_reserve_out);
703 (amount_to_swap, amount_out_collateral, U256::ZERO, U256::ZERO)
704 } else {
705 let amount_in_debt = amount_to_swap - a;
707 let amount_out_debt = get_amount_out(amount_in_debt, debt_i_reserve_in, debt_i_reserve_out);
708 let amount_out_collateral = get_amount_out(a, col_i_reserve_in, col_i_reserve_out);
709 (a, amount_out_collateral, amount_in_debt, amount_out_debt)
710 };
711
712 if amount_out_debt > debt_reserve_out {
713 return Err(SwapError::InsufficientReserve);
714 }
715
716 if amount_out_collateral > col_reserve_out {
717 return Err(SwapError::InsufficientReserve);
718 }
719
720 if amount_out_debt > borrowable {
721 return Err(SwapError::InsufficientBorrowable);
722 }
723
724 if amount_out_collateral > withdrawable {
725 return Err(SwapError::InsufficientWithdrawable);
726 }
727
728 if amount_in_collateral > U256::ZERO {
729 let reserves_ratio_valid = if swap0_to_1 {
730 verify_token1_reserves(
731 col_reserve_in + amount_in_collateral,
732 col_reserve_out - amount_out_collateral,
733 center_price,
734 )
735 } else {
736 verify_token0_reserves(
737 col_reserve_out - amount_out_collateral,
738 col_reserve_in + amount_in_collateral,
739 center_price,
740 )
741 };
742 if !reserves_ratio_valid {
743 return Err(SwapError::VerifyReservesRatiosInvalid);
744 }
745 }
746
747 if amount_in_debt > U256::ZERO {
748 let reserves_ratio_valid = if swap0_to_1 {
749 verify_token1_reserves(
750 debt_reserve_in + amount_in_debt,
751 debt_reserve_out - amount_out_debt,
752 center_price,
753 )
754 } else {
755 verify_token0_reserves(
756 debt_reserve_out - amount_out_debt,
757 debt_reserve_in + amount_in_debt,
758 center_price,
759 )
760 };
761 if !reserves_ratio_valid {
762 return Err(SwapError::VerifyReservesRatiosInvalid);
763 }
764 }
765
766 let (old_price, new_price) = if amount_in_collateral > amount_in_debt {
767 if swap0_to_1 {
768 (
769 col_i_reserve_out * constant::B_I1E27 / col_i_reserve_in,
770 (col_i_reserve_out - amount_out_collateral) * constant::B_I1E27 /
771 (col_i_reserve_in + amount_in_collateral),
772 )
773 } else {
774 (
775 col_i_reserve_in * constant::B_I1E27 / col_i_reserve_out,
776 (col_i_reserve_in + amount_in_collateral) * constant::B_I1E27 /
777 (col_i_reserve_out - amount_out_collateral),
778 )
779 }
780 } else if swap0_to_1 {
781 (
782 debt_i_reserve_out * constant::B_I1E27 / debt_i_reserve_in,
783 (debt_i_reserve_out - amount_out_debt) * constant::B_I1E27 /
784 (debt_i_reserve_in + amount_in_debt),
785 )
786 } else {
787 (
788 debt_i_reserve_in * constant::B_I1E27 / debt_i_reserve_out,
789 (debt_i_reserve_in + amount_in_debt) * constant::B_I1E27 /
790 (debt_i_reserve_out - amount_out_debt),
791 )
792 };
793
794 let price_diff = old_price.abs_diff(new_price);
795 let max_price_diff = old_price * constant::MAX_PRICE_DIFF / constant::TWO_DECIMALS;
796
797 if price_diff > max_price_diff {
798 return Err(SwapError::InsufficientMaxPrice);
799 }
800
801 if amount_in_collateral > U256::ZERO {
802 update_collateral_reserves_and_limits(
803 swap0_to_1,
804 amount_in_collateral,
805 amount_out_collateral,
806 col_reserves,
807 current_limits,
808 out_decimals,
809 );
810 }
811
812 if amount_in_debt > U256::ZERO {
813 update_debt_reserves_and_limits(
814 swap0_to_1,
815 amount_in_debt,
816 amount_out_debt,
817 debt_reserves,
818 current_limits,
819 out_decimals,
820 );
821 }
822
823 Ok(from_adjusted_amount(amount_out_collateral + amount_out_debt, out_decimals))
824}
825
826#[allow(clippy::too_many_arguments, dead_code)]
827fn swap_out_adjusted(
828 swap0_to_1: bool,
829 amount_to_receive: U256,
830 col_reserves: &mut CollateralReserves,
831 debt_reserves: &mut DebtReserves,
832 in_decimals: i64,
833 out_decimals: i64,
834 current_limits: &mut DexLimits,
835 center_price: U256,
836 sync_time: u64,
837) -> Result<U256, SwapError> {
838 let (
839 col_reserve_in,
840 col_reserve_out,
841 col_i_reserve_in,
842 col_i_reserve_out,
843 debt_reserve_in,
844 debt_reserve_out,
845 debt_i_reserve_in,
846 debt_i_reserve_out,
847 borrowable,
848 withdrawable,
849 ) = if swap0_to_1 {
850 (
851 col_reserves.token0_real_reserves,
852 col_reserves.token1_real_reserves,
853 col_reserves.token0_imaginary_reserves,
854 col_reserves.token1_imaginary_reserves,
855 debt_reserves.token0_real_reserves,
856 debt_reserves.token1_real_reserves,
857 debt_reserves.token0_imaginary_reserves,
858 debt_reserves.token1_imaginary_reserves,
859 get_expanded_limit(sync_time, ¤t_limits.borrowable_token1),
860 get_expanded_limit(sync_time, ¤t_limits.withdrawable_token1),
861 )
862 } else {
863 (
864 col_reserves.token1_real_reserves,
865 col_reserves.token0_real_reserves,
866 col_reserves.token1_imaginary_reserves,
867 col_reserves.token0_imaginary_reserves,
868 debt_reserves.token1_real_reserves,
869 debt_reserves.token0_real_reserves,
870 debt_reserves.token1_imaginary_reserves,
871 debt_reserves.token0_imaginary_reserves,
872 get_expanded_limit(sync_time, ¤t_limits.borrowable_token0),
873 get_expanded_limit(sync_time, ¤t_limits.withdrawable_token0),
874 )
875 };
876
877 let borrowable = to_adjusted_amount(borrowable, out_decimals);
878 let withdrawable = to_adjusted_amount(withdrawable, out_decimals);
879
880 let col_pool_enabled = col_reserves.token0_real_reserves > U256::ZERO &&
881 col_reserves.token1_real_reserves > U256::ZERO &&
882 col_reserves.token0_imaginary_reserves > U256::ZERO &&
883 col_reserves.token1_imaginary_reserves > U256::ZERO;
884
885 let debt_pool_enabled = debt_reserves.token0_real_reserves > U256::ZERO &&
886 debt_reserves.token1_real_reserves > U256::ZERO &&
887 debt_reserves.token0_imaginary_reserves > U256::ZERO &&
888 debt_reserves.token1_imaginary_reserves > U256::ZERO;
889
890 if !col_pool_enabled && !debt_pool_enabled {
891 return Err(SwapError::NoPoolsEnabled);
892 }
893
894 let a = if col_pool_enabled && debt_pool_enabled {
895 swap_routing_out(
896 amount_to_receive,
897 col_i_reserve_out,
898 col_i_reserve_in,
899 debt_i_reserve_out,
900 debt_i_reserve_in,
901 )
902 } else if debt_pool_enabled {
903 U256::MAX
904 } else if col_pool_enabled {
905 amount_to_receive + U256::ONE
906 } else {
907 return Err(SwapError::NoPoolsEnabled);
908 };
909
910 let mut trigger_update_debt_reserves = false;
911 let mut trigger_update_col_reserves = false;
912
913 let (amount_in_collateral, amount_out_collateral, amount_in_debt, amount_out_debt) =
914 if a == U256::ZERO || a == U256::MAX {
915 let amount_in_debt =
916 get_amount_in(amount_to_receive, debt_i_reserve_in, debt_i_reserve_out);
917 if amount_to_receive > debt_reserve_out {
918 return Err(SwapError::InsufficientReserve);
919 }
920
921 trigger_update_debt_reserves = true;
922 (U256::ZERO, U256::ZERO, amount_in_debt, amount_to_receive)
923 } else if a >= amount_to_receive {
924 let amount_in_collateral =
925 get_amount_in(amount_to_receive, col_i_reserve_in, col_i_reserve_out);
926
927 if amount_to_receive > col_reserve_out {
928 return Err(SwapError::InsufficientReserve);
929 }
930
931 trigger_update_col_reserves = true;
932 (amount_in_collateral, amount_to_receive, U256::ZERO, U256::ZERO)
933 } else {
934 let amount_out_collateral = a;
935 let amount_in_collateral =
936 get_amount_in(amount_out_collateral, col_i_reserve_in, col_i_reserve_out);
937 let amount_out_debt = amount_to_receive - amount_out_collateral;
938 let amount_in_debt =
939 get_amount_in(amount_out_debt, debt_i_reserve_in, debt_i_reserve_out);
940
941 if amount_out_debt > debt_reserve_out || amount_out_collateral > col_reserve_out {
942 return Err(SwapError::InsufficientReserve);
943 }
944
945 (amount_in_collateral, amount_out_collateral, amount_in_debt, amount_out_debt)
946 };
947
948 if amount_in_debt > borrowable {
949 return Err(SwapError::InsufficientBorrowable);
950 }
951
952 if amount_in_collateral > withdrawable {
953 return Err(SwapError::InsufficientWithdrawable);
954 }
955
956 if amount_in_collateral > U256::ZERO {
957 let reserves_ratio_valid = if swap0_to_1 {
958 verify_token1_reserves(
959 col_reserve_in + amount_in_collateral,
960 col_reserve_out - amount_out_collateral,
961 center_price,
962 )
963 } else {
964 verify_token0_reserves(
965 col_reserve_out - amount_out_collateral,
966 col_reserve_in + amount_in_collateral,
967 center_price,
968 )
969 };
970 if !reserves_ratio_valid {
971 return Err(SwapError::VerifyReservesRatiosInvalid);
972 }
973 }
974
975 if amount_in_debt > U256::ZERO {
976 let reserves_ratio_valid = if swap0_to_1 {
977 verify_token1_reserves(
978 debt_reserve_in + amount_in_debt,
979 debt_reserve_out - amount_out_debt,
980 center_price,
981 )
982 } else {
983 verify_token0_reserves(
984 debt_reserve_out - amount_out_debt,
985 debt_reserve_in + amount_in_debt,
986 center_price,
987 )
988 };
989 if !reserves_ratio_valid {
990 return Err(SwapError::VerifyReservesRatiosInvalid);
991 }
992 }
993
994 let (old_price, new_price) = if amount_in_collateral > amount_in_debt {
995 if swap0_to_1 {
996 (
997 col_i_reserve_out * constant::B_I1E27 / col_i_reserve_in,
998 (col_i_reserve_out - amount_out_collateral) * constant::B_I1E27 /
999 (col_i_reserve_in + amount_in_collateral),
1000 )
1001 } else {
1002 (
1003 col_i_reserve_in * constant::B_I1E27 / col_i_reserve_out,
1004 (col_i_reserve_in + amount_in_collateral) * constant::B_I1E27 /
1005 (col_i_reserve_out - amount_out_collateral),
1006 )
1007 }
1008 } else if swap0_to_1 {
1009 (
1010 debt_i_reserve_out * constant::B_I1E27 / debt_i_reserve_in,
1011 (debt_i_reserve_out - amount_out_debt) * constant::B_I1E27 /
1012 (debt_i_reserve_in + amount_in_debt),
1013 )
1014 } else {
1015 (
1016 debt_i_reserve_in * constant::B_I1E27 / debt_i_reserve_out,
1017 (debt_i_reserve_in + amount_in_debt) * constant::B_I1E27 /
1018 (debt_i_reserve_out - amount_out_debt),
1019 )
1020 };
1021
1022 let price_diff = old_price.abs_diff(new_price);
1023 let max_price_diff = old_price * constant::MAX_PRICE_DIFF / constant::TWO_DECIMALS;
1024
1025 if price_diff > max_price_diff {
1026 return Err(SwapError::InsufficientMaxPrice);
1027 }
1028
1029 if trigger_update_col_reserves {
1030 update_collateral_reserves_and_limits(
1031 swap0_to_1,
1032 amount_in_collateral,
1033 amount_out_collateral,
1034 col_reserves,
1035 current_limits,
1036 out_decimals,
1037 );
1038 }
1039
1040 if trigger_update_debt_reserves {
1041 update_debt_reserves_and_limits(
1042 swap0_to_1,
1043 amount_in_debt,
1044 amount_out_debt,
1045 debt_reserves,
1046 current_limits,
1047 out_decimals,
1048 );
1049 }
1050
1051 Ok(from_adjusted_amount(amount_in_collateral + amount_in_debt, in_decimals))
1052}
1053
1054fn swap_routing_in(t: U256, x: U256, y: U256, x2: U256, y2: U256) -> U256 {
1074 let xy_root = (x * y * constant::B_I1E18).root(2);
1075 let x2y2_root = (x2 * y2 * constant::B_I1E18).root(2);
1076
1077 let numerator = y2 * xy_root + t * xy_root - y * x2y2_root;
1078 let denominator = xy_root + x2y2_root;
1079 numerator / denominator
1080}
1081
1082#[allow(dead_code)]
1089fn swap_routing_out(t: U256, x: U256, y: U256, x2: U256, y2: U256) -> U256 {
1090 let xy_root = (x * y * constant::B_I1E18).root(2);
1091 let x2y2_root = (x2 * y2 * constant::B_I1E18).root(2);
1092
1093 let numerator = t * xy_root + y * x2y2_root - y2 * xy_root;
1094 let denominator = xy_root + x2y2_root;
1095
1096 numerator / denominator
1097}
1098
1099fn get_amount_out(amount_in: U256, i_reserve_in: U256, i_reserve_out: U256) -> U256 {
1100 amount_in * i_reserve_out / (i_reserve_in + amount_in)
1101}
1102
1103#[allow(dead_code)]
1107fn get_amount_in(amount_out: U256, i_reserve_in: U256, i_reserve_out: U256) -> U256 {
1108 amount_out * i_reserve_in / (i_reserve_out - amount_out)
1109}
1110
1111fn to_adjusted_amount(amount: U256, decimals: i64) -> U256 {
1112 let diff = decimals - constant::DEX_AMOUNT_DECIMALS;
1113 if diff == 0 {
1114 amount
1115 } else if diff > 0 {
1116 amount / ten_pow(diff)
1117 } else {
1118 amount * ten_pow(-diff)
1119 }
1120}
1121
1122fn from_adjusted_amount(adjusted_amount: U256, decimals: i64) -> U256 {
1132 let diff = decimals - constant::DEX_AMOUNT_DECIMALS;
1133
1134 if diff == 0 {
1135 adjusted_amount
1136 } else if diff < 0 {
1137 let divisor = ten_pow(-diff);
1139 adjusted_amount / divisor
1140 } else {
1141 let multiplier = ten_pow(diff);
1143 adjusted_amount * multiplier
1144 }
1145}
1146
1147fn ten_pow(v: i64) -> U256 {
1148 U256::from(10u64).pow(U256::from((v) as u64))
1149}
1150
1151fn verify_token0_reserves(token0_reserves: U256, token1_reserves: U256, price: U256) -> bool {
1168 let numerator = token1_reserves.saturating_mul(constant::B_I1E27);
1169 let denominator = price.saturating_mul(constant::MIN_SWAP_LIQUIDITY);
1170 token0_reserves >=
1171 numerator
1172 .checked_div(denominator)
1173 .unwrap_or(U256::ZERO)
1174}
1175
1176fn verify_token1_reserves(token0_reserves: U256, token1_reserves: U256, price: U256) -> bool {
1193 let numerator = token0_reserves.saturating_mul(price);
1194 let denominator = constant::B_I1E27.saturating_mul(constant::MIN_SWAP_LIQUIDITY);
1195 token1_reserves >= numerator.div_euclid(&denominator)
1196}
1197
1198fn get_expanded_limit(sync_time: u64, limit: &TokenLimit) -> U256 {
1210 let current_time = SystemTime::now()
1211 .duration_since(UNIX_EPOCH)
1212 .expect("system time before UNIX_EPOCH")
1213 .as_secs();
1214
1215 let elapsed_time = current_time.saturating_sub(sync_time);
1216 let elapsed = U256::from(elapsed_time);
1217
1218 if elapsed_time < 10 {
1219 return limit.available;
1221 }
1222
1223 if elapsed >= limit.expand_duration {
1224 return limit.expands_to;
1226 }
1227
1228 let delta = limit
1231 .expands_to
1232 .saturating_sub(limit.available);
1233 limit
1234 .available
1235 .saturating_add(delta.saturating_mul(elapsed) / limit.expand_duration)
1236}
1237
1238fn update_collateral_reserves_and_limits(
1243 swap0_to_1: bool,
1244 amount_in: U256,
1245 amount_out: U256,
1246 col_reserves: &mut CollateralReserves,
1247 limits: &mut DexLimits,
1248 out_decimals: i64,
1249) {
1250 let unadjusted_amount_out = from_adjusted_amount(amount_out, out_decimals);
1251
1252 if swap0_to_1 {
1253 col_reserves.token0_real_reserves = col_reserves
1255 .token0_real_reserves
1256 .saturating_add(amount_in);
1257 col_reserves.token0_imaginary_reserves = col_reserves
1258 .token0_imaginary_reserves
1259 .saturating_add(amount_in);
1260 col_reserves.token1_real_reserves = col_reserves
1261 .token1_real_reserves
1262 .saturating_sub(amount_out);
1263 col_reserves.token1_imaginary_reserves = col_reserves
1264 .token1_imaginary_reserves
1265 .saturating_sub(amount_out);
1266
1267 limits.withdrawable_token1.available = limits
1268 .withdrawable_token1
1269 .available
1270 .saturating_sub(unadjusted_amount_out);
1271 limits.withdrawable_token1.expands_to = limits
1272 .withdrawable_token1
1273 .expands_to
1274 .saturating_sub(unadjusted_amount_out);
1275 } else {
1276 col_reserves.token0_real_reserves = col_reserves
1278 .token0_real_reserves
1279 .saturating_sub(amount_out);
1280 col_reserves.token0_imaginary_reserves = col_reserves
1281 .token0_imaginary_reserves
1282 .saturating_sub(amount_out);
1283 col_reserves.token1_real_reserves = col_reserves
1284 .token1_real_reserves
1285 .saturating_add(amount_in);
1286 col_reserves.token1_imaginary_reserves = col_reserves
1287 .token1_imaginary_reserves
1288 .saturating_add(amount_in);
1289
1290 limits.withdrawable_token0.available = limits
1291 .withdrawable_token0
1292 .available
1293 .saturating_sub(unadjusted_amount_out);
1294 limits.withdrawable_token0.expands_to = limits
1295 .withdrawable_token0
1296 .expands_to
1297 .saturating_sub(unadjusted_amount_out);
1298 }
1299}
1300
1301fn update_debt_reserves_and_limits(
1302 swap0_to1: bool,
1303 amount_in: U256,
1304 amount_out: U256,
1305 debt_reserves: &mut DebtReserves,
1306 limits: &mut DexLimits,
1307 out_decimals: i64,
1308) {
1309 let unadjusted_amount_out = from_adjusted_amount(amount_out, out_decimals);
1310
1311 if swap0_to1 {
1312 debt_reserves.token0_real_reserves += amount_in;
1313 debt_reserves.token0_imaginary_reserves += amount_in;
1314 debt_reserves.token1_real_reserves -= amount_out;
1315 debt_reserves.token1_imaginary_reserves -= amount_out;
1316
1317 limits.borrowable_token1.available -= unadjusted_amount_out;
1326 limits.borrowable_token1.expands_to -= unadjusted_amount_out;
1327 } else {
1328 debt_reserves.token0_real_reserves -= amount_out;
1329 debt_reserves.token0_imaginary_reserves -= amount_out;
1330 debt_reserves.token1_real_reserves += amount_in;
1331 debt_reserves.token1_imaginary_reserves += amount_in;
1332
1333 limits.borrowable_token0.available -= unadjusted_amount_out;
1334 limits.borrowable_token0.expands_to -= unadjusted_amount_out;
1335 }
1336}
1337
1338fn get_max_reserves(
1339 decimals: u8,
1340 withdrawable_limit: &TokenLimit,
1341 borrowable_limit: &TokenLimit,
1342 real_col_reserves: &U256,
1343 real_debt_reserves: &U256,
1344) -> U256 {
1345 let mut max_limit_reserves = borrowable_limit.expands_to;
1347
1348 if borrowable_limit.expands_to != withdrawable_limit.expands_to {
1349 max_limit_reserves += withdrawable_limit.expands_to;
1350 }
1351
1352 let mut max_real_reserves = *real_col_reserves + *real_debt_reserves;
1354
1355 if decimals > constant::DEX_AMOUNT_DECIMALS as u8 {
1356 let diff = decimals as i64 - constant::DEX_AMOUNT_DECIMALS;
1357 max_real_reserves *= ten_pow(diff);
1358 } else if decimals < constant::DEX_AMOUNT_DECIMALS as u8 {
1359 let diff = constant::DEX_AMOUNT_DECIMALS - decimals as i64;
1360 max_real_reserves /= ten_pow(diff);
1361 }
1362
1363 if max_real_reserves < max_limit_reserves {
1365 max_real_reserves
1366 } else {
1367 max_limit_reserves
1368 }
1369}
1370
1371#[cfg(test)]
1372mod test {
1373 use std::str::FromStr;
1374
1375 use alloy::primitives::I256;
1376 use anyhow::bail;
1377 use num_traits::Num;
1378 use rstest::rstest;
1379 use tycho_common::models::Chain;
1380
1381 use super::*;
1382 use crate::evm::query_pool_swap::test_helpers::{target_price_params, to_price};
1383
1384 fn setup_fluid_pool(center_price: U256) -> (Token, Token, FluidV1) {
1385 let wsteth = Token::new(
1386 &Bytes::from_str("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0").unwrap(),
1387 "wsteth",
1388 18,
1389 0,
1390 &[Some(20000)],
1391 Chain::Ethereum,
1392 100,
1393 );
1394 let eth = Token::new(
1395 &Bytes::from_str("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE").unwrap(),
1396 "ETH",
1397 18,
1398 0,
1399 &[Some(2000)],
1400 Chain::Ethereum,
1401 100,
1402 );
1403
1404 let pool = FluidV1::new(
1405 &Bytes::from_str("0x0B1a513ee24972DAEf112bC777a5610d4325C9e7").unwrap(),
1406 &wsteth,
1407 ð,
1408 CollateralReserves {
1409 token0_real_reserves: U256::from_str("2169934539358").unwrap(),
1410 token1_real_reserves: U256::from_str("19563846299171").unwrap(),
1411 token0_imaginary_reserves: U256::from_str("62490032619260838").unwrap(),
1412 token1_imaginary_reserves: U256::from_str("73741038977020279").unwrap(),
1413 },
1414 DebtReserves {
1415 token0_real_reserves: U256::from_str("2169108220421").unwrap(),
1416 token1_real_reserves: U256::from_str("19572550738602").unwrap(),
1417 token0_imaginary_reserves: U256::from_str("62511862774117387").unwrap(),
1418 token1_imaginary_reserves: U256::from_str("73766803277429176").unwrap(),
1419 },
1420 limits_wide(),
1421 center_price,
1422 U256::from_str("100").unwrap(),
1423 SystemTime::now()
1424 .duration_since(UNIX_EPOCH)
1425 .unwrap()
1426 .as_secs() -
1427 10,
1428 );
1429 (wsteth, eth, pool)
1430 }
1431
1432 fn limits_wide() -> DexLimits {
1433 let limit_wide = U256::from_str("34242332879776515083099999").unwrap();
1434 DexLimits {
1435 withdrawable_token0: TokenLimit {
1436 available: limit_wide,
1437 expands_to: limit_wide,
1438 expand_duration: U256::ZERO,
1439 },
1440 withdrawable_token1: TokenLimit {
1441 available: limit_wide,
1442 expands_to: limit_wide,
1443 expand_duration: U256::from(22),
1444 },
1445 borrowable_token0: TokenLimit {
1446 available: limit_wide,
1447 expands_to: limit_wide,
1448 expand_duration: U256::ZERO,
1449 },
1450 borrowable_token1: TokenLimit {
1451 available: limit_wide,
1452 expands_to: limit_wide,
1453 expand_duration: U256::from(22),
1454 },
1455 }
1456 }
1457
1458 fn limits_tight() -> DexLimits {
1459 let limit_expand_tight = U256::from_str("711907234052361388866").unwrap();
1460
1461 DexLimits {
1462 withdrawable_token0: TokenLimit {
1463 available: U256::from_str("456740438880263").unwrap(),
1464 expands_to: limit_expand_tight,
1465 expand_duration: U256::from(600),
1466 },
1467 withdrawable_token1: TokenLimit {
1468 available: U256::from_str("825179383432029").unwrap(),
1469 expands_to: limit_expand_tight,
1470 expand_duration: U256::from(600),
1471 },
1472 borrowable_token0: TokenLimit {
1473 available: U256::from_str("941825058374170").unwrap(),
1474 expands_to: limit_expand_tight,
1475 expand_duration: U256::from(600),
1476 },
1477 borrowable_token1: TokenLimit {
1478 available: U256::from_str("941825058374170").unwrap(),
1479 expands_to: limit_expand_tight,
1480 expand_duration: U256::from(600),
1481 },
1482 }
1483 }
1484 fn new_col_reserves_one() -> CollateralReserves {
1485 CollateralReserves {
1486 token0_real_reserves: U256::from_str("20000000006000000").unwrap(),
1487 token1_real_reserves: U256::from_str("20000000000500000").unwrap(),
1488 token0_imaginary_reserves: U256::from_str("389736659726997981").unwrap(),
1489 token1_imaginary_reserves: U256::from_str("389736659619871949").unwrap(),
1490 }
1491 }
1492
1493 fn new_col_reserves_empty() -> CollateralReserves {
1494 CollateralReserves {
1495 token0_real_reserves: U256::ZERO,
1496 token1_real_reserves: U256::ZERO,
1497 token0_imaginary_reserves: U256::ZERO,
1498 token1_imaginary_reserves: U256::ZERO,
1499 }
1500 }
1501
1502 fn new_debt_reserves_empty() -> DebtReserves {
1503 DebtReserves {
1504 token0_real_reserves: U256::ZERO,
1505 token1_real_reserves: U256::ZERO,
1506 token0_imaginary_reserves: U256::ZERO,
1507 token1_imaginary_reserves: U256::ZERO,
1508 }
1509 }
1510
1511 fn new_debt_reserves_one() -> DebtReserves {
1512 DebtReserves {
1513 token0_real_reserves: U256::from_str("9486832995556050").unwrap(),
1514 token1_real_reserves: U256::from_str("9486832993079885").unwrap(),
1515 token0_imaginary_reserves: U256::from_str("184868330099560759").unwrap(),
1516 token1_imaginary_reserves: U256::from_str("184868330048879109").unwrap(),
1517 }
1518 }
1519
1520 pub fn get_approx_center_price_in(
1521 amount_to_swap: U256,
1522 swap0_to_1: bool,
1523 col_reserves: &CollateralReserves,
1524 debt_reserves: &DebtReserves,
1525 ) -> Result<U256, anyhow::Error> {
1526 let col_pool_enabled = !col_reserves
1527 .token0_real_reserves
1528 .is_zero() &&
1529 !col_reserves
1530 .token1_real_reserves
1531 .is_zero() &&
1532 !col_reserves
1533 .token0_imaginary_reserves
1534 .is_zero() &&
1535 !col_reserves
1536 .token1_imaginary_reserves
1537 .is_zero();
1538
1539 let debt_pool_enabled = !debt_reserves
1540 .token0_real_reserves
1541 .is_zero() &&
1542 !debt_reserves
1543 .token1_real_reserves
1544 .is_zero() &&
1545 !debt_reserves
1546 .token0_imaginary_reserves
1547 .is_zero() &&
1548 !debt_reserves
1549 .token1_imaginary_reserves
1550 .is_zero();
1551
1552 let (col_i_reserve_in, col_i_reserve_out, debt_i_reserve_in, debt_i_reserve_out) =
1553 if swap0_to_1 {
1554 (
1555 col_reserves.token0_imaginary_reserves,
1556 col_reserves.token1_imaginary_reserves,
1557 debt_reserves.token0_imaginary_reserves,
1558 debt_reserves.token1_imaginary_reserves,
1559 )
1560 } else {
1561 (
1562 col_reserves.token1_imaginary_reserves,
1563 col_reserves.token0_imaginary_reserves,
1564 debt_reserves.token1_imaginary_reserves,
1565 debt_reserves.token0_imaginary_reserves,
1566 )
1567 };
1568
1569 let a = if col_pool_enabled && debt_pool_enabled {
1570 swap_routing_in(
1571 amount_to_swap,
1572 col_i_reserve_out,
1573 col_i_reserve_in,
1574 debt_i_reserve_out,
1575 debt_i_reserve_in,
1576 )
1577 } else if debt_pool_enabled {
1578 U256::MAX } else if col_pool_enabled {
1580 amount_to_swap
1581 .checked_add(U256::from(1))
1582 .unwrap()
1583 } else {
1584 bail!("No pools are enabled");
1585 };
1586
1587 let (amount_in_collateral, amount_in_debt) = if a == U256::MAX || a == U256::ZERO {
1588 (U256::ZERO, amount_to_swap)
1589 } else if a >= amount_to_swap {
1590 (amount_to_swap, U256::ZERO)
1591 } else {
1592 (a, amount_to_swap - a)
1593 };
1594
1595 let price = if amount_in_collateral > amount_in_debt {
1596 if swap0_to_1 {
1597 col_i_reserve_out
1598 .checked_mul(constant::B_I1E27)
1599 .unwrap() /
1600 col_i_reserve_in
1601 } else {
1602 col_i_reserve_in
1603 .checked_mul(constant::B_I1E27)
1604 .unwrap() /
1605 col_i_reserve_out
1606 }
1607 } else if swap0_to_1 {
1608 debt_i_reserve_out
1609 .checked_mul(constant::B_I1E27)
1610 .unwrap() /
1611 debt_i_reserve_in
1612 } else {
1613 debt_i_reserve_in
1614 .checked_mul(constant::B_I1E27)
1615 .unwrap() /
1616 debt_i_reserve_out
1617 };
1618
1619 Ok(price)
1620 }
1621
1622 pub fn get_approx_center_price_out(
1623 amount_out: U256,
1624 swap0_to_1: bool,
1625 col_reserves: &CollateralReserves,
1626 debt_reserves: &DebtReserves,
1627 ) -> Result<U256, SwapError> {
1628 let col_pool_enabled = col_reserves.token0_real_reserves > U256::ZERO &&
1629 col_reserves.token1_real_reserves > U256::ZERO &&
1630 col_reserves.token0_imaginary_reserves > U256::ZERO &&
1631 col_reserves.token1_imaginary_reserves > U256::ZERO;
1632
1633 let debt_pool_enabled = debt_reserves.token0_real_reserves > U256::ZERO &&
1634 debt_reserves.token1_real_reserves > U256::ZERO &&
1635 debt_reserves.token0_imaginary_reserves > U256::ZERO &&
1636 debt_reserves.token1_imaginary_reserves > U256::ZERO;
1637
1638 let (col_i_reserve_in, col_i_reserve_out, debt_i_reserve_in, debt_i_reserve_out) =
1639 if swap0_to_1 {
1640 (
1641 col_reserves.token0_imaginary_reserves,
1642 col_reserves.token1_imaginary_reserves,
1643 debt_reserves.token0_imaginary_reserves,
1644 debt_reserves.token1_imaginary_reserves,
1645 )
1646 } else {
1647 (
1648 col_reserves.token1_imaginary_reserves,
1649 col_reserves.token0_imaginary_reserves,
1650 debt_reserves.token1_imaginary_reserves,
1651 debt_reserves.token0_imaginary_reserves,
1652 )
1653 };
1654
1655 let a = if col_pool_enabled && debt_pool_enabled {
1656 swap_routing_out(
1657 amount_out,
1658 col_i_reserve_in,
1659 col_i_reserve_out,
1660 debt_i_reserve_in,
1661 debt_i_reserve_out,
1662 )
1663 } else if debt_pool_enabled {
1664 U256::MAX } else if col_pool_enabled {
1666 amount_out + U256::ONE } else {
1668 return Err(SwapError::NoPoolsEnabled);
1669 };
1670
1671 let mut amount_in_collateral = U256::ZERO;
1672 let mut amount_in_debt = U256::ZERO;
1673
1674 if a <= U256::ZERO {
1675 amount_in_debt = get_amount_in(amount_out, debt_i_reserve_in, debt_i_reserve_out);
1676 } else if a >= amount_out {
1677 amount_in_collateral = get_amount_in(amount_out, col_i_reserve_in, col_i_reserve_out);
1678 } else {
1679 amount_in_collateral = get_amount_in(a, col_i_reserve_in, col_i_reserve_out);
1680 amount_in_debt = get_amount_in(amount_out - a, debt_i_reserve_in, debt_i_reserve_out);
1681 }
1682
1683 let price = if amount_in_collateral > amount_in_debt {
1684 if swap0_to_1 {
1685 col_i_reserve_out * constant::B_I1E27 / col_i_reserve_in
1686 } else {
1687 col_i_reserve_in * constant::B_I1E27 / col_i_reserve_out
1688 }
1689 } else if swap0_to_1 {
1690 debt_i_reserve_out * constant::B_I1E27 / debt_i_reserve_in
1691 } else {
1692 debt_i_reserve_in * constant::B_I1E27 / debt_i_reserve_out
1693 };
1694
1695 Ok(price)
1696 }
1697
1698 #[test]
1699 fn test_delta_transition_from_attribute() {
1700 let (_, _, mut pool) = setup_fluid_pool(U256::ONE);
1701 let sync_time: u64 = 1_700_000_000;
1702 let delta = ProtocolStateDelta {
1703 updated_attributes: HashMap::from(vm::pending_state_attributes(
1704 alloy::sol_types::SolValue::abi_encode(&vm::sample_pool_with_reserves()),
1705 sync_time,
1706 )),
1707 ..Default::default()
1708 };
1709
1710 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1711 .expect("delta transition from attribute failed");
1712
1713 assert_eq!(
1716 pool.collateral_reserves
1717 .token0_real_reserves,
1718 U256::from(1u64)
1719 );
1720 assert_eq!(
1721 pool.collateral_reserves
1722 .token1_imaginary_reserves,
1723 U256::from(4u64)
1724 );
1725 assert_eq!(pool.debt_reserves.token0_real_reserves, U256::from(13u64));
1726 assert_eq!(
1727 pool.debt_reserves
1728 .token1_imaginary_reserves,
1729 U256::from(16u64)
1730 );
1731 assert_eq!(
1732 pool.dex_limits
1733 .withdrawable_token0
1734 .available,
1735 U256::from(21u64)
1736 );
1737 assert_eq!(
1738 pool.dex_limits
1739 .borrowable_token1
1740 .expand_duration,
1741 U256::from(32u64)
1742 );
1743 assert_eq!(pool.fee, U256::from(41u64));
1744 assert_eq!(pool.center_price, U256::from(42u64));
1745 assert_eq!(pool.sync_time, sync_time);
1746 assert_eq!(pool.pool_reserve0, U256::from(22u64 + 28u64));
1749 assert_eq!(pool.pool_reserve1, U256::from(25u64 + 31u64));
1750 }
1751
1752 #[test]
1753 fn test_delta_transition_attribute_without_timestamp() {
1754 let (_, _, mut pool) = setup_fluid_pool(U256::ONE);
1755 let delta = ProtocolStateDelta {
1756 updated_attributes: HashMap::from([(
1757 vm::POOL_RESERVES_ADJUSTED_ATTRIBUTE.to_string(),
1758 Bytes::from(alloy::sol_types::SolValue::abi_encode(
1759 &vm::sample_pool_with_reserves(),
1760 )),
1761 )]),
1762 ..Default::default()
1763 };
1764
1765 let result = pool.delta_transition(delta, &HashMap::new(), &Balances::default());
1766
1767 match result {
1768 Err(TransitionError::MissingAttribute(attr)) => {
1769 assert_eq!(attr, vm::BLOCK_TIMESTAMP_ATTRIBUTE)
1770 }
1771 other => panic!("expected MissingAttribute error, got {other:?}"),
1772 }
1773 }
1774
1775 #[test]
1776 fn test_delta_transition_rejects_malformed_timestamp() {
1777 for timestamp in [vec![0; 7], vec![0; 9], vec![0; 32]] {
1778 let delta = ProtocolStateDelta {
1779 updated_attributes: HashMap::from([
1780 (
1781 vm::POOL_RESERVES_ADJUSTED_ATTRIBUTE.to_string(),
1782 Bytes::from(alloy::sol_types::SolValue::abi_encode(
1783 &vm::sample_pool_with_reserves(),
1784 )),
1785 ),
1786 (vm::BLOCK_TIMESTAMP_ATTRIBUTE.to_string(), Bytes::from(timestamp.clone())),
1787 ]),
1788 ..Default::default()
1789 };
1790 let (_, _, mut pool) = setup_fluid_pool(U256::ONE);
1791
1792 let result = pool.delta_transition(delta, &HashMap::new(), &Balances::default());
1793
1794 match result {
1795 Err(TransitionError::DecodeError(message)) => {
1796 assert!(message.contains(&format!("got {} bytes", timestamp.len())));
1797 }
1798 other => panic!("expected DecodeError, got {other:?}"),
1799 }
1800 }
1801 }
1802
1803 #[test]
1804 fn test_calc_amount_out_zero2one() {
1805 let (wsteth, eth, pool) = setup_fluid_pool(U256::ONE);
1806 let cases = [
1807 ("1000000000000000000", "1179917402128000000"),
1808 ("500000000000000000", "589961060629000000"),
1809 ];
1810 for (amount_in_str, exp_out_str) in cases.into_iter() {
1811 let exp_out = BigUint::from_str_radix(exp_out_str, 10).unwrap();
1812 let res = pool
1813 .get_amount_out(BigUint::from_str_radix(amount_in_str, 10).unwrap(), &wsteth, ð)
1814 .unwrap();
1815
1816 assert_eq!(res.amount, exp_out);
1817 }
1818 }
1819
1820 #[test]
1821 fn test_calc_amount_out_one2zero() {
1822 let center_price = U256::from_str("1200000000000000000000000000").unwrap();
1823 let (wsteth, eth, pool) = setup_fluid_pool(center_price);
1824 let cases = [("800000000000000000", "677868867152000000")];
1825 for (amount_in_str, exp_out_str) in cases.into_iter() {
1826 let exp_out = BigUint::from_str_radix(exp_out_str, 10).unwrap();
1827 let res = pool
1828 .get_amount_out(BigUint::from_str_radix(amount_in_str, 10).unwrap(), ð, &wsteth)
1829 .unwrap();
1830
1831 assert_eq!(res.amount, exp_out);
1832 }
1833 }
1834
1835 fn setup_pool_with_decimals(center_price: U256, decimals: u32) -> (Token, Token, FluidV1) {
1836 let (mut token0, mut token1, pool) = setup_fluid_pool(center_price);
1837 token0.decimals = decimals;
1838 token1.decimals = decimals;
1839 let pool = FluidV1::new(
1840 &pool.pool_address,
1841 &token0,
1842 &token1,
1843 pool.collateral_reserves.clone(),
1844 pool.debt_reserves.clone(),
1845 limits_wide(),
1846 pool.center_price,
1847 pool.fee,
1848 pool.sync_time,
1849 );
1850 (token0, token1, pool)
1851 }
1852
1853 #[rstest]
1856 #[case::zero2one(true, U256::ONE, 18, 0.9998)]
1857 #[case::one2zero(false, U256::from(12u64) * U256::from(10u64).pow(U256::from(26u64)), 18, 0.99995)]
1858 #[case::six_decimals(true, U256::ONE, 6, 0.9998)]
1859 fn test_swap_to_price_lands_in_band(
1860 #[case] zero2one: bool,
1861 #[case] center_price: U256,
1862 #[case] decimals: u32,
1863 #[case] multiplier: f64,
1864 ) {
1865 let (token0, token1, pool) = setup_pool_with_decimals(center_price, decimals);
1866 let (token_in, token_out) = if zero2one { (token0, token1) } else { (token1, token0) };
1867 let target = pool
1868 .spot_price(&token_in, &token_out)
1869 .unwrap() *
1870 multiplier;
1871 let params = target_price_params(
1872 &token_in,
1873 &token_out,
1874 to_price(target, &token_in, &token_out),
1875 1e-5,
1876 );
1877
1878 let swap = pool.query_pool_swap(¶ms).unwrap();
1879
1880 assert!(swap.price_points().is_none(), "expected the closed form");
1881 let new_spot = swap
1882 .new_state()
1883 .spot_price(&token_in, &token_out)
1884 .unwrap();
1885 assert!(
1886 new_spot >= target * (1.0 - 1e-12) && new_spot <= target * (1.0 + 1e-5),
1887 "{new_spot}"
1888 );
1889 }
1890
1891 #[test]
1892 fn test_swap_to_price_target_above_spot() {
1893 let (wsteth, eth, pool) = setup_fluid_pool(U256::ONE);
1894 let spot = pool.spot_price(&wsteth, ð).unwrap();
1895 let params =
1896 target_price_params(&wsteth, ð, to_price(spot * 1.01, &wsteth, ð), 0.001);
1897
1898 let result = pool.query_pool_swap(¶ms);
1899
1900 assert!(matches!(result, Err(SimulationError::InvalidInput(..))), "{result:?}");
1901 }
1902
1903 #[test]
1904 fn test_swap_to_price_target_equal_to_spot() {
1905 let (wsteth, eth, pool) = setup_fluid_pool(U256::ONE);
1906 let spot = pool.spot_price(&wsteth, ð).unwrap();
1907 let scale = 1u128 << 52;
1909 let target = Price::new(BigUint::from((spot * scale as f64) as u128), BigUint::from(scale));
1910 let params = target_price_params(&wsteth, ð, target, 0.001);
1911
1912 let swap = pool.query_pool_swap(¶ms).unwrap();
1913
1914 assert_eq!(swap.amount_in(), &BigUint::ZERO);
1915 assert!(swap.new_state().eq(&pool));
1916 }
1917
1918 #[test]
1919 fn test_swap_to_price_outside_band_uses_numerical_search() {
1920 let (token0, token1, pool) = setup_pool_with_decimals(U256::ONE, 6);
1923 let spot = pool
1924 .spot_price(&token0, &token1)
1925 .unwrap();
1926 let params =
1927 target_price_params(&token0, &token1, to_price(spot * 0.9998, &token0, &token1), 0.0);
1928
1929 let swap = pool.query_pool_swap(¶ms).unwrap();
1930
1931 assert!(swap.price_points().is_some(), "expected the numerical search");
1932 }
1933
1934 #[test]
1935 fn test_amount_out_exceeds_reserve() {
1936 let (wsteth, eth, mut pool) = setup_fluid_pool(U256::ONE);
1937 pool.pool_reserve0 = U256::from_str("18760613183894").unwrap();
1939 pool.pool_reserve1 = U256::from_str("22123580158026").unwrap();
1940 let amount_in = BigUint::from_str_radix("30000000000000000000", 10).unwrap(); let result = pool.get_amount_out(amount_in, &wsteth, ð);
1942
1943 assert!(result.is_err(), "Expected an error for exceeding reserves");
1944 assert_eq!(
1945 result.unwrap_err().to_string(),
1946 SimulationError::from(SwapError::InsufficientReserve).to_string()
1947 );
1948 }
1949
1950 #[test]
1951 fn test_swap_in() {
1952 let sync_time = SystemTime::now()
1953 .duration_since(UNIX_EPOCH)
1954 .unwrap()
1955 .as_secs();
1956
1957 assert_swap_in_result(
1958 true,
1959 U256::from(1_000_000_000_000_000u128), new_col_reserves_one(),
1961 new_debt_reserves_one(),
1962 "998262697204710000000",
1963 12,
1964 18,
1965 limits_wide(),
1966 sync_time - 10,
1967 );
1968
1969 assert_swap_in_result(
1970 true,
1971 U256::from(1_000_000_000_000_000u128),
1972 new_col_reserves_empty(),
1973 new_debt_reserves_one(),
1974 "994619847016724000000",
1975 12,
1976 18,
1977 limits_wide(),
1978 sync_time - 10,
1979 );
1980
1981 assert_swap_in_result(
1982 true,
1983 U256::from(1_000_000_000_000_000u128),
1984 new_col_reserves_one(),
1985 new_debt_reserves_empty(),
1986 "997440731289905000000",
1987 12,
1988 18,
1989 limits_wide(),
1990 sync_time - 10,
1991 );
1992
1993 assert_swap_in_result(
1994 false,
1995 U256::from(1_000_000_000_000_000u128),
1996 new_col_reserves_one(),
1997 new_debt_reserves_one(),
1998 "998262697752553000000",
1999 12,
2000 18,
2001 limits_wide(),
2002 sync_time - 10,
2003 );
2004
2005 assert_swap_in_result(
2006 false,
2007 U256::from(1_000_000_000_000_000u128),
2008 new_col_reserves_empty(),
2009 new_debt_reserves_one(),
2010 "994619847560607000000",
2011 12,
2012 18,
2013 limits_wide(),
2014 sync_time - 10,
2015 );
2016
2017 assert_swap_in_result(
2018 false,
2019 U256::from(1_000_000_000_000_000u128),
2020 new_col_reserves_one(),
2021 new_debt_reserves_empty(),
2022 "997440731837532000000",
2023 12,
2024 18,
2025 limits_wide(),
2026 sync_time - 10,
2027 );
2028 }
2029
2030 #[allow(clippy::too_many_arguments)]
2043 fn assert_swap_in_result(
2044 swap0_to_1: bool,
2045 amount_in: U256,
2046 mut col_reserves: CollateralReserves,
2047 mut debt_reserves: DebtReserves,
2048 expected_amount_out: &str,
2049 in_decimals: i64,
2050 out_decimals: i64,
2051 mut limits: DexLimits,
2052 sync_time: u64,
2053 ) {
2054 let price =
2055 get_approx_center_price_in(amount_in, swap0_to_1, &col_reserves, &debt_reserves)
2056 .expect("Failed to get approx center price");
2057
2058 let adjusted_amount_in = to_adjusted_amount(amount_in, in_decimals);
2059 let out_amt = swap_in_adjusted(
2060 swap0_to_1,
2061 adjusted_amount_in,
2062 &mut col_reserves,
2063 &mut debt_reserves,
2064 out_decimals,
2065 &mut limits,
2066 price,
2067 sync_time,
2068 )
2069 .expect("Failed to calculate swap in adjusted");
2070
2071 assert_eq!(expected_amount_out, out_amt.to_string(), "Amount out mismatch");
2072 }
2073
2074 #[allow(clippy::too_many_arguments)]
2075 fn assert_swap_out_result(
2076 swap0_to_1: bool,
2077 amount_out: U256,
2078 mut col_reserves: CollateralReserves,
2079 mut debt_reserves: DebtReserves,
2080 expected_amount_in: &str,
2081 in_decimals: i64,
2082 out_decimals: i64,
2083 mut limits: DexLimits,
2084 sync_time: i64,
2085 ) {
2086 let price =
2087 get_approx_center_price_out(amount_out, swap0_to_1, &col_reserves, &debt_reserves)
2088 .expect("failed to get approx center price");
2089
2090 let in_amt = swap_out_adjusted(
2091 swap0_to_1,
2092 to_adjusted_amount(amount_out, out_decimals),
2093 &mut col_reserves,
2094 &mut debt_reserves,
2095 in_decimals,
2096 out_decimals,
2097 &mut limits,
2098 price,
2099 sync_time as u64,
2100 )
2101 .expect("swap_out_adjusted failed");
2102
2103 assert_eq!(expected_amount_in, from_adjusted_amount(in_amt, in_decimals).to_string());
2104 }
2105
2106 #[test]
2107 fn test_swap_in_limits() {
2108 let sync_time = SystemTime::now()
2109 .duration_since(UNIX_EPOCH)
2110 .unwrap()
2111 .as_secs();
2112
2113 let price = get_approx_center_price_in(
2115 U256::from(1_000_000_000_000_000u128),
2116 true,
2117 &new_col_reserves_one(),
2118 &new_debt_reserves_one(),
2119 )
2120 .unwrap();
2121
2122 let res = swap_in_adjusted(
2123 true,
2124 U256::from(1_000_000_000_000_000u128),
2125 &mut new_col_reserves_one(),
2126 &mut new_debt_reserves_one(),
2127 18,
2128 &mut limits_tight(),
2129 price,
2130 sync_time - 10,
2131 );
2132
2133 assert_eq!(res.unwrap_err().to_string(), SwapError::InsufficientBorrowable.to_string());
2134
2135 let price = get_approx_center_price_out(
2137 U256::from(1_000_000_000_000_000u128),
2138 true,
2139 &new_col_reserves_one(),
2140 &new_debt_reserves_one(),
2141 )
2142 .unwrap();
2143
2144 let out_amt = swap_in_adjusted(
2145 true,
2146 U256::from(1_000_000_000_000_000u128),
2147 &mut new_col_reserves_one(),
2148 &mut new_debt_reserves_one(),
2149 18,
2150 &mut limits_tight(),
2151 price,
2152 sync_time - 6000,
2153 )
2154 .unwrap();
2155
2156 assert_eq!(out_amt.to_string(), "998262697204710000000");
2157
2158 let price = get_approx_center_price_out(
2160 U256::from(30_000_000_000_000_000u128),
2161 true,
2162 &new_col_reserves_one(),
2163 &new_debt_reserves_one(),
2164 )
2165 .unwrap();
2166
2167 let res = swap_in_adjusted(
2168 true,
2169 U256::from(30_000_000_000_000_000u128),
2170 &mut new_col_reserves_one(),
2171 &mut new_debt_reserves_one(),
2172 18,
2173 &mut limits_wide(),
2174 price,
2175 sync_time - 10,
2176 );
2177
2178 assert_eq!(res.unwrap_err().to_string(), SwapError::InsufficientMaxPrice.to_string());
2179
2180 let price = get_approx_center_price_out(
2182 U256::from(50_000_000_000_000_000u128),
2183 true,
2184 &new_col_reserves_one(),
2185 &new_debt_reserves_one(),
2186 )
2187 .unwrap();
2188
2189 let res = swap_in_adjusted(
2190 true,
2191 U256::from(50_000_000_000_000_000u128),
2192 &mut new_col_reserves_one(),
2193 &mut new_debt_reserves_one(),
2194 18,
2195 &mut limits_wide(),
2196 price,
2197 sync_time - 10,
2198 );
2199
2200 assert_eq!(res.unwrap_err().to_string(), SwapError::InsufficientReserve.to_string());
2201 }
2202
2203 #[test]
2204 fn test_swap_in_adjusted_compare_estimate_in() {
2205 let now = SystemTime::now()
2206 .duration_since(UNIX_EPOCH)
2207 .unwrap()
2208 .as_secs();
2209 let expected_amount_out = U256::from_str("1180035404724000000").unwrap();
2210 let mut col_reserves = CollateralReserves {
2211 token0_real_reserves: U256::from_str("2169934539358").unwrap(),
2212 token1_real_reserves: U256::from_str("19563846299171").unwrap(),
2213 token0_imaginary_reserves: U256::from_str("62490032619260838").unwrap(),
2214 token1_imaginary_reserves: U256::from_str("73741038977020279").unwrap(),
2215 };
2216 let mut debt_reserves = DebtReserves {
2217 token0_real_reserves: U256::from_str("2169108220421").unwrap(),
2218 token1_real_reserves: U256::from_str("19572550738602").unwrap(),
2219 token0_imaginary_reserves: U256::from_str("62511862774117387").unwrap(),
2220 token1_imaginary_reserves: U256::from_str("73766803277429176").unwrap(),
2221 };
2222 let amount_in = U256::from(1000000000000u128); let price = get_approx_center_price_in(amount_in, true, &col_reserves, &debt_reserves)
2224 .expect("Failed to get approximate center price");
2225
2226 let out_amt = swap_in_adjusted(
2227 true,
2228 amount_in,
2229 &mut col_reserves,
2230 &mut debt_reserves,
2231 18,
2232 &mut limits_wide(),
2233 price,
2234 now - 10,
2235 )
2236 .expect("Failed to swap in adjusted");
2237
2238 assert_eq!(expected_amount_out, out_amt);
2239 }
2240
2241 #[test]
2242 fn test_swap_in_debt_empty() {
2243 let now = SystemTime::now()
2244 .duration_since(UNIX_EPOCH)
2245 .unwrap()
2246 .as_secs();
2247
2248 assert_swap_in_result(
2249 true,
2250 U256::from_str("1000000000000000").unwrap(),
2251 new_col_reserves_empty(),
2252 new_debt_reserves_one(),
2253 "994619847016724",
2254 12,
2255 12,
2256 limits_wide(),
2257 now - 10,
2258 );
2259
2260 assert_swap_in_result(
2261 false,
2262 U256::from_str("1000000000000000").unwrap(),
2263 new_col_reserves_empty(),
2264 new_debt_reserves_one(),
2265 "994619847560607",
2266 12,
2267 12,
2268 limits_wide(),
2269 now - 10,
2270 )
2271 }
2272
2273 #[test]
2274 fn test_swap_in_col_empty() {
2275 let now = SystemTime::now()
2276 .duration_since(UNIX_EPOCH)
2277 .unwrap()
2278 .as_secs();
2279
2280 assert_swap_in_result(
2281 true,
2282 U256::from_str("1000000000000000").unwrap(),
2283 new_col_reserves_one(),
2284 new_debt_reserves_empty(),
2285 "997440731289905",
2286 12,
2287 12,
2288 limits_wide(),
2289 now - 10,
2290 );
2291
2292 assert_swap_in_result(
2293 false,
2294 U256::from_str("1000000000000000").unwrap(),
2295 new_col_reserves_one(),
2296 new_debt_reserves_empty(),
2297 "997440731837532",
2298 12,
2299 12,
2300 limits_wide(),
2301 now - 10,
2302 )
2303 }
2304
2305 #[test]
2306 fn test_swap_out() {
2307 let sync_time = (SystemTime::now()
2308 .duration_since(UNIX_EPOCH)
2309 .unwrap()
2310 .as_secs() as i64) -
2311 10;
2312
2313 assert_swap_out_result(
2314 true,
2315 U256::from(1_000_000_000_000_000u64),
2316 new_col_reserves_one(),
2317 new_debt_reserves_one(),
2318 "1001743360284199",
2319 12,
2320 12,
2321 limits_wide(),
2322 sync_time,
2323 );
2324
2325 assert_swap_out_result(
2326 true,
2327 U256::from(1_000_000_000_000_000u64),
2328 new_col_reserves_empty(),
2329 new_debt_reserves_one(),
2330 "1005438674786548",
2331 12,
2332 12,
2333 limits_wide(),
2334 sync_time,
2335 );
2336
2337 assert_swap_out_result(
2338 true,
2339 U256::from(1_000_000_000_000_000u64),
2340 new_col_reserves_one(),
2341 new_debt_reserves_empty(),
2342 "1002572435818386",
2343 12,
2344 12,
2345 limits_wide(),
2346 sync_time,
2347 );
2348
2349 assert_swap_out_result(
2350 false,
2351 U256::from(1_000_000_000_000_000u64),
2352 new_col_reserves_one(),
2353 new_debt_reserves_one(),
2354 "1001743359733488",
2355 12,
2356 12,
2357 limits_wide(),
2358 sync_time,
2359 );
2360
2361 assert_swap_out_result(
2362 false,
2363 U256::from(1_000_000_000_000_000u64),
2364 new_col_reserves_empty(),
2365 new_debt_reserves_one(),
2366 "1005438674233767",
2367 12,
2368 12,
2369 limits_wide(),
2370 sync_time,
2371 );
2372
2373 assert_swap_out_result(
2374 false,
2375 U256::from(1_000_000_000_000_000u64),
2376 new_col_reserves_one(),
2377 new_debt_reserves_empty(),
2378 "1002572435266527",
2379 12,
2380 12,
2381 limits_wide(),
2382 sync_time,
2383 );
2384 }
2385
2386 #[test]
2387 fn test_swap_out_limits() {
2388 let sync_time_recent = (SystemTime::now()
2389 .duration_since(UNIX_EPOCH)
2390 .unwrap()
2391 .as_secs()) -
2392 10;
2393
2394 let sync_time_expanded = sync_time_recent - 5990; let price = get_approx_center_price_out(
2398 U256::from(1_000_000_000_000_000u64),
2399 true,
2400 &new_col_reserves_one(),
2401 &new_debt_reserves_one(),
2402 )
2403 .unwrap();
2404
2405 let result = swap_out_adjusted(
2406 true,
2407 U256::from(1_000_000_000_000_000u64),
2408 &mut new_col_reserves_one(),
2409 &mut new_debt_reserves_one(),
2410 12,
2411 18,
2412 &mut limits_tight(),
2413 price,
2414 sync_time_recent,
2415 );
2416
2417 assert!(matches!(result, Err(SwapError::InsufficientBorrowable)));
2418
2419 let price = get_approx_center_price_out(
2421 U256::from(1_000_000_000_000_000u64),
2422 true,
2423 &new_col_reserves_one(),
2424 &new_debt_reserves_one(),
2425 )
2426 .unwrap();
2427
2428 let result = swap_out_adjusted(
2429 true,
2430 U256::from(1_000_000_000_000_000u64),
2431 &mut new_col_reserves_one(),
2432 &mut new_debt_reserves_one(),
2433 12,
2434 18,
2435 &mut limits_tight(),
2436 price,
2437 sync_time_expanded,
2438 )
2439 .unwrap();
2440
2441 assert_eq!(from_adjusted_amount(result, 12).to_string(), "1001743360284199");
2442
2443 let price = get_approx_center_price_out(
2445 U256::from(20_000_000_000_000_000u64),
2446 true,
2447 &new_col_reserves_one(),
2448 &new_debt_reserves_one(),
2449 )
2450 .unwrap();
2451
2452 let result = swap_out_adjusted(
2453 true,
2454 U256::from(20_000_000_000_000_000u64),
2455 &mut new_col_reserves_one(),
2456 &mut new_debt_reserves_one(),
2457 12,
2458 18,
2459 &mut limits_wide(),
2460 price,
2461 sync_time_recent,
2462 );
2463
2464 assert!(matches!(result, Err(SwapError::InsufficientMaxPrice)));
2465
2466 let price = get_approx_center_price_out(
2468 U256::from(30_000_000_000_000_000u64),
2469 true,
2470 &new_col_reserves_one(),
2471 &new_debt_reserves_one(),
2472 )
2473 .unwrap();
2474
2475 let result = swap_out_adjusted(
2476 true,
2477 U256::from(30_000_000_000_000_000u64),
2478 &mut new_col_reserves_one(),
2479 &mut new_debt_reserves_one(),
2480 12,
2481 18,
2482 &mut limits_wide(),
2483 price,
2484 sync_time_recent,
2485 );
2486
2487 assert!(matches!(result, Err(SwapError::InsufficientReserve)));
2488 }
2489
2490 #[test]
2491 fn test_swap_out_empty_debt() {
2492 let sync_time = (SystemTime::now()
2493 .duration_since(UNIX_EPOCH)
2494 .unwrap()
2495 .as_secs() as i64) -
2496 10;
2497
2498 assert_swap_out_result(
2500 true,
2501 U256::from(994_619_847_016_724u64),
2502 new_col_reserves_empty(),
2503 new_debt_reserves_one(),
2504 "999999999999999",
2505 12,
2506 12,
2507 limits_wide(),
2508 sync_time,
2509 );
2510
2511 assert_swap_out_result(
2513 false,
2514 U256::from(994_619_847_560_607u64),
2515 new_col_reserves_empty(),
2516 new_debt_reserves_one(),
2517 "999999999999999",
2518 12,
2519 12,
2520 limits_wide(),
2521 sync_time,
2522 );
2523 }
2524
2525 #[test]
2526 fn test_swap_out_empty_collateral() {
2527 let sync_time = (SystemTime::now()
2528 .duration_since(UNIX_EPOCH)
2529 .unwrap()
2530 .as_secs() as i64) -
2531 10;
2532
2533 assert_swap_out_result(
2535 true,
2536 U256::from(997_440_731_289_905u64),
2537 new_col_reserves_one(),
2538 new_debt_reserves_empty(),
2539 "999999999999999",
2540 12,
2541 12,
2542 limits_wide(),
2543 sync_time,
2544 );
2545
2546 assert_swap_out_result(
2548 false,
2549 U256::from(997_440_731_837_532u64),
2550 new_col_reserves_one(),
2551 new_debt_reserves_empty(),
2552 "999999999999999",
2553 12,
2554 12,
2555 limits_wide(),
2556 sync_time,
2557 );
2558 }
2559
2560 pub fn new_verify_ratio_col_reserves() -> CollateralReserves {
2561 CollateralReserves {
2562 token0_real_reserves: U256::from(2_000_000u64) * U256::from(10u64).pow(U256::from(12)),
2563 token1_real_reserves: U256::from(15_000u64) * U256::from(10u64).pow(U256::from(12)),
2564 token0_imaginary_reserves: U256::ZERO,
2565 token1_imaginary_reserves: U256::ZERO,
2566 }
2567 }
2568
2569 pub fn new_verify_ratio_debt_reserves() -> DebtReserves {
2570 DebtReserves {
2571 token0_real_reserves: U256::from(2_000_000u64) * U256::from(10u64).pow(U256::from(12)),
2572 token1_real_reserves: U256::from(15_000u64) * U256::from(10u64).pow(U256::from(12)),
2573 token0_imaginary_reserves: U256::ZERO,
2574 token1_imaginary_reserves: U256::ZERO,
2575 }
2576 }
2577
2578 pub fn calculate_reserves_outside_range(
2580 geometric_mean_price: U256,
2581 price_at_range: U256,
2582 reserve_x: U256,
2583 reserve_y: U256,
2584 ) -> (I256, I256) {
2585 let geometric_mean_price = I256::from(geometric_mean_price);
2586 let price_at_range = I256::from(price_at_range);
2587 let reserve_x = I256::from(reserve_x);
2588 let reserve_y = I256::from(reserve_y);
2589
2590 let one_e27 = I256::from(constant::B_I1E27);
2591 let two = I256::try_from(2i8).unwrap();
2592
2593 let part1 = price_at_range
2595 .checked_sub(geometric_mean_price)
2596 .expect("priceAtRange must be >= geometricMeanPrice");
2597
2598 let part2 = geometric_mean_price
2600 .checked_mul(reserve_x)
2601 .unwrap()
2602 .checked_add(reserve_y.checked_mul(one_e27).unwrap())
2603 .unwrap()
2604 .checked_div(two.checked_mul(part1).unwrap())
2605 .unwrap();
2606
2607 let mut part3 = reserve_x
2609 .checked_mul(reserve_y)
2610 .unwrap();
2611
2612 let one_e50 = I256::try_from(10)
2613 .unwrap()
2614 .pow(U256::from(50));
2615
2616 if part3 < one_e50 {
2618 part3 = part3
2619 .checked_mul(one_e27)
2620 .unwrap()
2621 .checked_div(part1)
2622 .unwrap();
2623 } else {
2624 part3 = part3
2625 .checked_div(part1)
2626 .unwrap()
2627 .checked_mul(one_e27)
2628 .unwrap();
2629 }
2630
2631 let part2_squared = part2.checked_mul(part2).unwrap();
2633 let inside_sqrt = part3
2634 .checked_add(part2_squared)
2635 .unwrap();
2636 let sqrt_value = I256::from(
2637 U256::try_from(inside_sqrt)
2638 .unwrap()
2639 .root(2),
2640 );
2641
2642 let reserve_x_outside = part2.checked_add(sqrt_value).unwrap();
2643
2644 let reserve_y_outside = reserve_x_outside
2646 .checked_mul(geometric_mean_price)
2647 .unwrap()
2648 .checked_div(one_e27)
2649 .unwrap();
2650
2651 (reserve_x_outside, reserve_y_outside)
2652 }
2653
2654 #[test]
2655 fn test_swap_in_verify_reserves_in_range() {
2656 let decimals: i64 = 6;
2657 let mut col_reserves = new_verify_ratio_col_reserves();
2658 let mut debt_reserves = new_verify_ratio_debt_reserves();
2659
2660 let mut price = U256::from_str("1000001000000000000000000000").unwrap();
2661
2662 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2664 constant::B_I1E27,
2665 price,
2666 col_reserves.token0_real_reserves,
2667 col_reserves.token1_real_reserves,
2668 );
2669
2670 col_reserves.token0_imaginary_reserves =
2671 U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2672 col_reserves.token1_imaginary_reserves = U256::from(
2673 I256::from(reserve_y_outside) + I256::from(col_reserves.token1_real_reserves),
2674 );
2675
2676 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2678 constant::B_I1E27,
2679 price,
2680 debt_reserves.token0_real_reserves,
2681 debt_reserves.token1_real_reserves,
2682 );
2683
2684 debt_reserves.token0_imaginary_reserves =
2685 U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2686 debt_reserves.token1_imaginary_reserves = U256::from(
2687 I256::from(reserve_y_outside) + I256::from(debt_reserves.token1_real_reserves),
2688 );
2689
2690 let sync_time = SystemTime::now()
2691 .duration_since(UNIX_EPOCH)
2692 .unwrap()
2693 .as_secs() -
2694 10;
2695
2696 let swap_amount = U256::from(14_905) * U256::from(10).pow(U256::from(12)); price = get_approx_center_price_in(
2699 swap_amount,
2700 true,
2701 &col_reserves,
2702 &new_debt_reserves_empty(),
2703 )
2704 .unwrap();
2705 let result = swap_in_adjusted(
2706 true,
2707 swap_amount,
2708 &mut col_reserves,
2709 &mut new_debt_reserves_empty(),
2710 decimals,
2711 &mut limits_wide(),
2712 price,
2713 sync_time,
2714 );
2715 assert!(
2716 result.is_err(),
2717 "FAIL: reserves ratio revert NOT hit for col reserves when swap amount 14_905"
2718 );
2719
2720 price = get_approx_center_price_in(
2721 swap_amount,
2722 true,
2723 &new_col_reserves_empty(),
2724 &debt_reserves,
2725 )
2726 .unwrap();
2727 let result = swap_in_adjusted(
2728 true,
2729 swap_amount,
2730 &mut new_col_reserves_empty(),
2731 &mut debt_reserves,
2732 decimals,
2733 &mut limits_wide(),
2734 price,
2735 sync_time,
2736 );
2737 assert!(
2738 result.is_err(),
2739 "FAIL: reserves ratio revert NOT hit for debt reserves when swap amount 14_905"
2740 );
2741
2742 col_reserves = new_verify_ratio_col_reserves();
2744 debt_reserves = new_verify_ratio_debt_reserves();
2745
2746 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2747 constant::B_I1E27,
2748 price,
2749 col_reserves.token0_real_reserves,
2750 col_reserves.token1_real_reserves,
2751 );
2752
2753 col_reserves.token0_imaginary_reserves =
2754 U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2755 col_reserves.token1_imaginary_reserves = U256::from(
2756 I256::from(reserve_y_outside) + I256::from(col_reserves.token1_real_reserves),
2757 );
2758
2759 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2760 constant::B_I1E27,
2761 price,
2766 debt_reserves.token0_real_reserves,
2767 debt_reserves.token1_real_reserves,
2768 );
2769 debt_reserves.token0_imaginary_reserves =
2770 U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2771 debt_reserves.token1_imaginary_reserves = U256::from(
2772 I256::from(reserve_y_outside) + I256::from(debt_reserves.token1_real_reserves),
2773 );
2774
2775 let swap_amount = U256::from(14_895) * U256::from(10).pow(U256::from(12));
2777
2778 price = get_approx_center_price_in(
2779 swap_amount,
2780 true,
2781 &col_reserves,
2782 &new_debt_reserves_empty(),
2783 )
2784 .unwrap();
2785 let result = swap_in_adjusted(
2786 true,
2787 swap_amount,
2788 &mut col_reserves,
2789 &mut new_debt_reserves_empty(),
2790 decimals,
2791 &mut limits_wide(),
2792 price,
2793 sync_time,
2794 );
2795 assert!(
2796 result.is_ok(),
2797 "FAIL: reserves ratio revert hit for col reserves when swap amount 14_895"
2798 );
2799
2800 price = get_approx_center_price_in(
2801 swap_amount,
2802 true,
2803 &new_col_reserves_empty(),
2804 &debt_reserves,
2805 )
2806 .unwrap();
2807 let result = swap_in_adjusted(
2808 true,
2809 swap_amount,
2810 &mut new_col_reserves_empty(),
2811 &mut debt_reserves,
2812 decimals,
2813 &mut limits_wide(),
2814 price,
2815 sync_time,
2816 );
2817 assert!(
2818 result.is_ok(),
2819 "FAIL: reserves ratio revert hit for debt reserves when swap amount 14_895"
2820 );
2821 }
2822
2823 pub fn new_verify_ratio_col_reserves_swap_out() -> CollateralReserves {
2824 CollateralReserves {
2825 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,
2828 token1_imaginary_reserves: U256::ZERO,
2829 }
2830 }
2831
2832 pub fn new_verify_ratio_debt_reserves_swap_out() -> DebtReserves {
2833 DebtReserves {
2834 token0_real_reserves: U256::from(15_000u64) * U256::from(10u64).pow(U256::from(12)),
2835 token1_real_reserves: U256::from(2_000_000u64) * U256::from(10u64).pow(U256::from(12)),
2836 token0_imaginary_reserves: U256::ZERO,
2837 token1_imaginary_reserves: U256::ZERO,
2838 }
2839 }
2840
2841 #[test]
2842 fn test_swap_out_verify_reserves_in_range() {
2843 let decimals: i64 = 6;
2844 let sync_time = SystemTime::now()
2845 .duration_since(UNIX_EPOCH)
2846 .unwrap()
2847 .as_secs() -
2848 10;
2849
2850 let mut col_reserves = new_verify_ratio_col_reserves_swap_out();
2851 let mut debt_reserves = new_verify_ratio_debt_reserves_swap_out();
2852
2853 let price = U256::from_str("1000001000000000000000000000").unwrap();
2855
2856 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2858 constant::B_I1E27,
2859 price,
2860 col_reserves.token0_real_reserves,
2861 col_reserves.token1_real_reserves,
2862 );
2863 col_reserves.token0_imaginary_reserves =
2864 U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2865 col_reserves.token1_imaginary_reserves =
2866 U256::from(reserve_y_outside + I256::from(col_reserves.token1_real_reserves));
2867
2868 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2869 constant::B_I1E27,
2870 price,
2871 debt_reserves.token0_real_reserves,
2872 debt_reserves.token1_real_reserves,
2873 );
2874 debt_reserves.token0_imaginary_reserves =
2875 U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2876 debt_reserves.token1_imaginary_reserves =
2877 U256::from(reserve_y_outside + I256::from(debt_reserves.token1_real_reserves));
2878
2879 let swap_amount = U256::from(14_766u64) * U256::from(10u64).pow(U256::from(12));
2881
2882 let price = get_approx_center_price_out(
2883 swap_amount,
2884 false,
2885 &col_reserves,
2886 &new_debt_reserves_empty(),
2887 )
2888 .unwrap();
2889 let result = swap_out_adjusted(
2890 false,
2891 swap_amount,
2892 &mut col_reserves,
2893 &mut new_debt_reserves_empty(),
2894 decimals,
2895 decimals,
2896 &mut limits_wide(),
2897 price,
2898 sync_time,
2899 );
2900 assert!(result.is_err(), "FAIL: reserves ratio verification revert NOT hit for col reserves when swap amount 14_766");
2901
2902 let price = get_approx_center_price_out(
2903 swap_amount,
2904 false,
2905 &new_col_reserves_empty(),
2906 &debt_reserves,
2907 )
2908 .unwrap();
2909 let result = swap_out_adjusted(
2910 false,
2911 swap_amount,
2912 &mut new_col_reserves_empty(),
2913 &mut debt_reserves,
2914 decimals,
2915 decimals,
2916 &mut limits_wide(),
2917 price,
2918 sync_time,
2919 );
2920 assert!(result.is_err(), "FAIL: reserves ratio verification revert NOT hit for debt reserves when swap amount 14_766");
2921
2922 col_reserves = new_verify_ratio_col_reserves_swap_out();
2924 debt_reserves = new_verify_ratio_debt_reserves_swap_out();
2925
2926 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2927 constant::B_I1E27,
2928 price,
2929 col_reserves.token0_real_reserves,
2930 col_reserves.token1_real_reserves,
2931 );
2932 col_reserves.token0_imaginary_reserves =
2933 U256::from(reserve_x_outside + I256::from(col_reserves.token0_real_reserves));
2934 col_reserves.token1_imaginary_reserves =
2935 U256::from(reserve_y_outside + I256::from(col_reserves.token1_real_reserves));
2936
2937 let (reserve_x_outside, reserve_y_outside) = calculate_reserves_outside_range(
2938 constant::B_I1E27,
2939 price,
2940 debt_reserves.token0_real_reserves,
2941 debt_reserves.token1_real_reserves,
2942 );
2943 debt_reserves.token0_imaginary_reserves =
2944 U256::from(reserve_x_outside + I256::from(debt_reserves.token0_real_reserves));
2945 debt_reserves.token1_imaginary_reserves =
2946 U256::from(reserve_y_outside + I256::from(debt_reserves.token1_real_reserves));
2947
2948 let swap_amount = U256::from(14_762u64) * U256::from(10u64).pow(U256::from(12));
2950
2951 let price = get_approx_center_price_out(
2952 swap_amount,
2953 false,
2954 &col_reserves,
2955 &new_debt_reserves_empty(),
2956 )
2957 .unwrap();
2958 let result = swap_out_adjusted(
2959 false,
2960 swap_amount,
2961 &mut col_reserves,
2962 &mut new_debt_reserves_empty(),
2963 decimals,
2964 decimals,
2965 &mut limits_wide(),
2966 price,
2967 sync_time,
2968 );
2969 assert!(
2970 result.is_ok(),
2971 "FAIL: reserves ratio verification revert hit for col reserves when swap amount 14_762"
2972 );
2973
2974 let price = get_approx_center_price_out(
2975 swap_amount,
2976 false,
2977 &new_col_reserves_empty(),
2978 &debt_reserves,
2979 )
2980 .unwrap();
2981 let result = swap_out_adjusted(
2982 false,
2983 swap_amount,
2984 &mut new_col_reserves_empty(),
2985 &mut debt_reserves,
2986 decimals,
2987 decimals,
2988 &mut limits_wide(),
2989 price,
2990 sync_time,
2991 );
2992 assert!(result.is_ok(), "FAIL: reserves ratio verification revert hit for debt reserves when swap amount 14_762");
2993 }
2994
2995 fn hard_limit(l: u128) -> TokenLimit {
3012 TokenLimit {
3013 available: U256::from(l),
3014 expands_to: U256::from(l),
3015 expand_duration: U256::ZERO,
3016 }
3017 }
3018
3019 fn wsteth_eth_pool_23526115() -> (Token, Token, FluidV1) {
3020 let wsteth = Token::new(
3021 &Bytes::from_str("0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0").unwrap(),
3022 "wsteth",
3023 18,
3024 0,
3025 &[Some(20000)],
3026 Chain::Ethereum,
3027 100,
3028 );
3029 let eth = Token::new(
3030 &Bytes::from_str("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE").unwrap(),
3031 "ETH",
3032 18,
3033 0,
3034 &[Some(2000)],
3035 Chain::Ethereum,
3036 100,
3037 );
3038 let pool = FluidV1::new(
3039 &Bytes::from_str("0x0B1a513ee24972DAEf112bC777a5610d4325C9e7").unwrap(),
3040 &wsteth,
3041 ð,
3042 CollateralReserves {
3043 token0_real_reserves: U256::from(4431191840536456u128),
3044 token1_real_reserves: U256::from(13105569017021951u128),
3045 token0_imaginary_reserves: U256::from(20263646714209556492u128),
3046 token1_imaginary_reserves: U256::from(24624319733997222300u128),
3047 },
3048 DebtReserves {
3049 token0_real_reserves: U256::from(3958052320699256u128),
3050 token1_real_reserves: U256::from(11706224851989005u128),
3051 token0_imaginary_reserves: U256::from(18100000404581051720u128),
3052 token1_imaginary_reserves: U256::from(21995063545785045888u128),
3053 },
3054 DexLimits {
3055 borrowable_token0: hard_limit(4431191840536456767040),
3056 borrowable_token1: hard_limit(6552784508510975527319),
3057 withdrawable_token0: hard_limit(4819160955805377144139),
3058 withdrawable_token1: hard_limit(6126272539623278413525),
3059 },
3060 U256::from_str("1215727283480584508000000000").unwrap(),
3061 U256::from(68),
3062 1759795200,
3063 );
3064 (wsteth, eth, pool)
3065 }
3066
3067 #[test]
3068 fn test_spot_price() {
3069 let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
3070 let exp_spot0 = 1.21519682; let exp_spot1 = 0.82291191; let spot0 = pool.spot_price(&wsteth, ð).unwrap();
3075 let spot1 = pool.spot_price(ð, &wsteth).unwrap();
3076
3077 let rel_err0 = (spot0 - exp_spot0).abs() / exp_spot0;
3078 let rel_err1 = (spot1 - exp_spot1).abs() / exp_spot1;
3079
3080 assert!(
3081 rel_err0 < 1e-4,
3082 "spot0 mismatch: got {spot0}, expected {exp_spot0}, relative error: {rel_err0}"
3083 );
3084 assert!(
3085 rel_err1 < 1e-4,
3086 "spot1 mismatch: got {spot1}, expected {exp_spot1}, relative error: {rel_err1}"
3087 );
3088 }
3089
3090 #[test]
3091 fn test_get_amount_out_zero2one() {
3092 let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
3093 let amount_in = BigUint::from_str_radix("100000000000000", 10).unwrap();
3094 let exp_amount_out = BigUint::from_str_radix("121511421000000", 10).unwrap();
3096
3097 let res = pool
3098 .get_amount_out(amount_in, &wsteth, ð)
3099 .unwrap();
3100
3101 assert_eq!(res.amount, exp_amount_out);
3102 }
3103
3104 #[test]
3105 fn test_get_amount_out_one2zero() {
3106 let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
3107 let amount_in = BigUint::from_str_radix("100000000000000", 10).unwrap();
3108 let exp_amount_out = BigUint::from_str_radix("82285598000000", 10).unwrap();
3110
3111 let res = pool
3112 .get_amount_out(amount_in, ð, &wsteth)
3113 .unwrap();
3114 assert_eq!(res.amount, exp_amount_out);
3115 }
3116
3117 #[test]
3118 fn get_limits_zero2one() {
3119 let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
3120
3121 let (max_amount_in, _) = pool
3122 .get_limits(wsteth.address.clone(), eth.address.clone())
3123 .unwrap();
3124 let max_amount_onchain_test =
3125 BigUint::from_str_radix("10200000000000000000000", 10).unwrap();
3127
3128 let _ = pool
3129 .get_amount_out(max_amount_in.clone(), &wsteth, ð)
3130 .unwrap();
3131 assert!(max_amount_in < max_amount_onchain_test);
3132 }
3133
3134 #[test]
3135 fn get_limits_one2zero() {
3136 let (wsteth, eth, pool) = wsteth_eth_pool_23526115();
3137
3138 let (max_amount_in, _) = pool
3139 .get_limits(eth.address.clone(), wsteth.address.clone())
3140 .unwrap();
3141 let max_amount_onchain_test =
3142 BigUint::from_str_radix("10192694739404003000000", 10).unwrap();
3144
3145 let _ = pool
3146 .get_amount_out(max_amount_in.clone(), ð, &wsteth)
3147 .unwrap();
3148
3149 assert!(max_amount_in < max_amount_onchain_test);
3150 }
3151}