1use std::{any::Any, collections::HashMap, fmt};
2
3use alloy::primitives::{Address, Sign, I256, U256};
4use num_bigint::BigUint;
5use num_traits::{CheckedSub, ToPrimitive, Zero};
6use revm::primitives::I128;
7use tracing::trace;
8use tycho_common::{
9 dto::ProtocolStateDelta,
10 models::token::Token,
11 simulation::{
12 errors::{SimulationError, TransitionError},
13 protocol_sim::{
14 Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
15 SwapConstraint,
16 },
17 },
18 Bytes,
19};
20
21use super::hooks::utils::{has_permission, HookOptions};
22use crate::{
23 evm::protocol::{
24 clmm::clmm_swap_to_price,
25 safe_math::{safe_add_u256, safe_sub_u256},
26 u256_num::u256_to_biguint,
27 uniswap_v4::hooks::{
28 hook_handler::HookHandler,
29 models::{
30 AfterSwapParameters, BalanceDelta, BeforeSwapDelta, BeforeSwapParameters,
31 StateContext, SwapParams,
32 },
33 },
34 utils::{
35 add_fee_markup,
36 uniswap::{
37 i24_be_bytes_to_i32, liquidity_math,
38 lp_fee::{self, is_dynamic},
39 sqrt_price_math::{get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64},
40 swap_math,
41 tick_list::{TickInfo, TickList, TickListErrorKind},
42 tick_math::{
43 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK,
44 MIN_SQRT_RATIO, MIN_TICK,
45 },
46 StepComputation, SwapResults, SwapState,
47 },
48 },
49 vm::constants::EXTERNAL_ACCOUNT,
50 },
51 impl_non_serializable_protocol,
52};
53
54const SWAP_BASE_GAS: u64 = 130_000;
57const GAS_PER_TICK: u64 = 17_540;
59const MAX_SWAP_GAS: u64 = 16_700_000;
61const MAX_TICKS_CROSSED: u64 = (MAX_SWAP_GAS - SWAP_BASE_GAS) / GAS_PER_TICK;
62
63#[derive(Clone)]
64pub struct UniswapV4State {
65 liquidity: u128,
66 sqrt_price: U256,
67 fees: UniswapV4Fees,
68 tick: i32,
69 ticks: TickList,
70 tick_spacing: i32,
71 pub hook: Option<Box<dyn HookHandler>>,
72}
73
74impl_non_serializable_protocol!(UniswapV4State, "not supported due vm state deps");
75
76impl fmt::Debug for UniswapV4State {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 f.debug_struct("UniswapV4State")
79 .field("liquidity", &self.liquidity)
80 .field("sqrt_price", &self.sqrt_price)
81 .field("fees", &self.fees)
82 .field("tick", &self.tick)
83 .field("tick_spacing", &self.tick_spacing)
84 .finish_non_exhaustive()
85 }
86}
87
88impl PartialEq for UniswapV4State {
89 fn eq(&self, other: &Self) -> bool {
90 match (&self.hook, &other.hook) {
91 (Some(a), Some(b)) => a.is_equal(&**b),
92 (None, None) => true,
93 _ => false,
94 }
95 }
96}
97
98impl Eq for UniswapV4State {}
99
100#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct UniswapV4Fees {
102 pub zero_for_one: u32,
104 pub one_for_zero: u32,
106 pub lp_fee: u32,
108}
109
110impl UniswapV4Fees {
111 pub fn new(zero_for_one: u32, one_for_zero: u32, lp_fee: u32) -> Self {
112 Self { zero_for_one, one_for_zero, lp_fee }
113 }
114
115 fn calculate_swap_fees_pips(&self, zero_for_one: bool, lp_fee_override: Option<u32>) -> u32 {
116 let protocol_fee = if zero_for_one { self.zero_for_one } else { self.one_for_zero };
117 let lp_fee = lp_fee_override.unwrap_or_else(|| {
118 if is_dynamic(self.lp_fee) {
120 0
121 } else {
122 self.lp_fee
123 }
124 });
125
126 protocol_fee + lp_fee - ((protocol_fee as u64 * lp_fee as u64 / 1_000_000) as u32)
131 }
132}
133
134impl UniswapV4State {
135 pub fn new(
137 liquidity: u128,
138 sqrt_price: U256,
139 fees: UniswapV4Fees,
140 tick: i32,
141 tick_spacing: i32,
142 ticks: Vec<TickInfo>,
143 ) -> Result<Self, SimulationError> {
144 let tick_spacing_u16 = tick_spacing.try_into().map_err(|_| {
145 SimulationError::FatalError(format!(
148 "tick_spacing {} must be positive (int24 -> u16 conversion failed)",
149 tick_spacing
150 ))
151 })?;
152 let tick_list = TickList::from(tick_spacing_u16, ticks)?;
153 Ok(UniswapV4State {
154 liquidity,
155 sqrt_price,
156 fees,
157 tick,
158 ticks: tick_list,
159 tick_spacing,
160 hook: None,
161 })
162 }
163
164 fn swap(
165 &self,
166 zero_for_one: bool,
167 amount_specified: I256,
168 sqrt_price_limit: Option<U256>,
169 lp_fee_override: Option<u32>,
170 ) -> Result<SwapResults, SimulationError> {
171 if amount_specified == I256::ZERO {
172 return Ok(SwapResults {
173 amount_calculated: I256::ZERO,
174 amount_specified: I256::ZERO,
175 amount_remaining: I256::ZERO,
176 sqrt_price: self.sqrt_price,
177 liquidity: self.liquidity,
178 tick: self.tick,
179 gas_used: U256::from(3_000), });
181 }
182
183 if self.liquidity == 0 {
184 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
185 }
186 let price_limit = if let Some(limit) = sqrt_price_limit {
187 limit
188 } else if zero_for_one {
189 safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
190 } else {
191 safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
192 };
193
194 let price_limit_valid = if zero_for_one {
195 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
196 } else {
197 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
198 };
199 if !price_limit_valid {
200 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
201 }
202
203 let exact_input = amount_specified < I256::ZERO;
204
205 let mut state = SwapState {
206 amount_remaining: amount_specified,
207 amount_calculated: I256::ZERO,
208 sqrt_price: self.sqrt_price,
209 tick: self.tick,
210 liquidity: self.liquidity,
211 };
212 let mut gas_used = U256::from(130_000);
213
214 while state.amount_remaining != I256::ZERO && state.sqrt_price != price_limit {
215 let (mut next_tick, initialized) = match self
216 .ticks
217 .next_initialized_tick_within_one_word(state.tick, zero_for_one)
218 {
219 Ok((tick, init)) => (tick, init),
220 Err(tick_err) => match tick_err.kind {
221 TickListErrorKind::TicksExeeded => {
222 let mut new_state = self.clone();
223 new_state.liquidity = state.liquidity;
224 new_state.tick = state.tick;
225 new_state.sqrt_price = state.sqrt_price;
226 return Err(SimulationError::InvalidInput(
227 "Ticks exceeded".into(),
228 Some(GetAmountOutResult::new(
229 u256_to_biguint(state.amount_calculated.abs().into_raw()),
230 u256_to_biguint(gas_used),
231 Box::new(new_state),
232 )),
233 ));
234 }
235 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
236 },
237 };
238
239 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
240
241 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
242 let fee_pips = self
243 .fees
244 .calculate_swap_fees_pips(zero_for_one, lp_fee_override);
245
246 let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
247 state.sqrt_price,
248 UniswapV4State::get_sqrt_ratio_target(sqrt_price_next, price_limit, zero_for_one),
249 state.liquidity,
250 -state.amount_remaining,
254 fee_pips,
255 )?;
256 state.sqrt_price = sqrt_price;
257
258 let step = StepComputation {
259 sqrt_price_start: state.sqrt_price,
260 tick_next: next_tick,
261 initialized,
262 sqrt_price_next,
263 amount_in,
264 amount_out,
265 fee_amount,
266 };
267 if exact_input {
268 state.amount_remaining += I256::checked_from_sign_and_abs(
269 Sign::Positive,
270 safe_add_u256(step.amount_in, step.fee_amount)?,
271 )
272 .unwrap();
273 state.amount_calculated -=
274 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
275 } else {
276 state.amount_remaining -=
277 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
278 state.amount_calculated += I256::checked_from_sign_and_abs(
279 Sign::Positive,
280 safe_add_u256(step.amount_in, step.fee_amount)?,
281 )
282 .unwrap();
283 }
284 if state.sqrt_price == step.sqrt_price_next {
285 if step.initialized {
286 let liquidity_raw = self
287 .ticks
288 .get_tick(step.tick_next)
289 .unwrap()
290 .net_liquidity;
291 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
292 state.liquidity =
293 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
294 }
295 state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
296 } else if state.sqrt_price != step.sqrt_price_start {
297 state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
298 }
299 gas_used = safe_add_u256(gas_used, U256::from(2000))?;
300 }
301 Ok(SwapResults {
302 amount_calculated: state.amount_calculated,
303 amount_specified,
304 amount_remaining: state.amount_remaining,
305 sqrt_price: state.sqrt_price,
306 liquidity: state.liquidity,
307 tick: state.tick,
308 gas_used,
309 })
310 }
311
312 pub fn set_hook_handler(&mut self, handler: Box<dyn HookHandler>) {
313 self.hook = Some(handler);
314 }
315
316 fn get_sqrt_ratio_target(
317 sqrt_price_next: U256,
318 sqrt_price_limit: U256,
319 zero_for_one: bool,
320 ) -> U256 {
321 let cond1 = if zero_for_one {
322 sqrt_price_next < sqrt_price_limit
323 } else {
324 sqrt_price_next > sqrt_price_limit
325 };
326
327 if cond1 {
328 sqrt_price_limit
329 } else {
330 sqrt_price_next
331 }
332 }
333
334 fn find_limits_experimentally(
335 &self,
336 token_in: Bytes,
337 token_out: Bytes,
338 ) -> Result<(BigUint, BigUint), SimulationError> {
339 let token_in_obj =
342 Token::new(&token_in, "TOKEN_IN", 18, 0, &[Some(10_000)], Default::default(), 100);
343 let token_out_obj =
344 Token::new(&token_out, "TOKEN_OUT", 18, 0, &[Some(10_000)], Default::default(), 100);
345
346 self.find_max_amount(&token_in_obj, &token_out_obj)
347 }
348
349 fn find_max_amount(
362 &self,
363 token_in: &Token,
364 token_out: &Token,
365 ) -> Result<(BigUint, BigUint), SimulationError> {
366 let mut low = BigUint::from(1u64);
367
368 let mut high = BigUint::from(10u64).pow(18); let mut last_successful_amount_in = BigUint::from(1u64);
372 let mut last_successful_amount_out = BigUint::from(0u64);
373
374 while let Ok(result) = self.get_amount_out(high.clone(), token_in, token_out) {
377 low = last_successful_amount_in.clone();
380 last_successful_amount_in = high.clone();
381 last_successful_amount_out = result.amount;
382 high *= BigUint::from(10u64);
383
384 if high > BigUint::from(10u64).pow(75) {
386 return Ok((last_successful_amount_in, last_successful_amount_out));
387 }
388 }
389
390 while &high - &low > BigUint::from(1u64) {
392 let mid = (&low + &high) / BigUint::from(2u64);
393
394 match self.get_amount_out(mid.clone(), token_in, token_out) {
395 Ok(result) => {
396 last_successful_amount_in = mid.clone();
397 last_successful_amount_out = result.amount;
398 low = mid;
399 }
400 Err(_) => {
401 high = mid;
402 }
403 }
404 }
405
406 Ok((last_successful_amount_in, last_successful_amount_out))
407 }
408
409 fn has_no_initialized_ticks(&self) -> bool {
411 !self.ticks.has_initialized_ticks()
412 }
413}
414
415#[typetag::serde]
416impl ProtocolSim for UniswapV4State {
417 fn fee(&self) -> f64 {
420 todo!()
421 }
422
423 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
424 if let Some(hook) = &self.hook {
425 match hook.spot_price(base, quote) {
426 Ok(price) => return Ok(price),
427 Err(SimulationError::RecoverableError(_)) => {
428 let x1 = BigUint::from(10u64).pow(base.decimals) / BigUint::from(100u64); let x2 = &x1 + (&x1 / BigUint::from(100u64));
436
437 let y1 = self.get_amount_out(x1.clone(), base, quote)?;
439 let y2 = self.get_amount_out(x2.clone(), base, quote)?;
440
441 let num = y2
443 .amount
444 .checked_sub(&y1.amount)
445 .ok_or_else(|| {
446 SimulationError::FatalError(
447 "Cannot calculate spot price: y2 < y1".to_string(),
448 )
449 })?;
450 let den = x2.checked_sub(&x1).ok_or_else(|| {
451 SimulationError::FatalError(
452 "Cannot calculate spot price: x2 < x1".to_string(),
453 )
454 })?;
455
456 if den == BigUint::from(0u64) {
457 return Err(SimulationError::FatalError(
458 "Cannot calculate spot price: denominator is zero".to_string(),
459 ));
460 }
461
462 let num_f64 = num.to_f64().ok_or_else(|| {
464 SimulationError::FatalError(
465 "Failed to convert numerator to f64".to_string(),
466 )
467 })?;
468 let den_f64 = den.to_f64().ok_or_else(|| {
469 SimulationError::FatalError(
470 "Failed to convert denominator to f64".to_string(),
471 )
472 })?;
473
474 let token_correction = 10f64.powi(base.decimals as i32 - quote.decimals as i32);
475
476 return Ok(num_f64 / den_f64 * token_correction);
477 }
478 Err(e) => return Err(e),
479 }
480 }
481
482 let zero_for_one = base < quote;
483 let fee_pips = self
484 .fees
485 .calculate_swap_fees_pips(zero_for_one, None);
486 let fee = fee_pips as f64 / 1_000_000.0;
487
488 let price = if zero_for_one {
489 sqrt_price_q96_to_f64(self.sqrt_price, base.decimals, quote.decimals)?
490 } else {
491 1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, quote.decimals, base.decimals)?
492 };
493
494 Ok(add_fee_markup(price, fee))
495 }
496
497 fn get_amount_out(
498 &self,
499 amount_in: BigUint,
500 token_in: &Token,
501 token_out: &Token,
502 ) -> Result<GetAmountOutResult, SimulationError> {
503 let zero_for_one = token_in < token_out;
504 let amount_specified = I256::checked_from_sign_and_abs(
505 Sign::Negative,
506 U256::from_be_slice(&amount_in.to_bytes_be()),
507 )
508 .ok_or_else(|| {
509 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
510 })?;
511
512 let mut amount_to_swap = amount_specified;
513 let mut lp_fee_override: Option<u32> = None;
514 let mut before_swap_gas = 0u64;
515 let mut after_swap_gas = 0u64;
516 let mut before_swap_delta = BeforeSwapDelta(I256::ZERO);
517 let mut storage_overwrites = None;
518
519 let token_in_address = Address::from_slice(&token_in.address);
520 let token_out_address = Address::from_slice(&token_out.address);
521
522 let state_context = StateContext {
523 currency_0: if zero_for_one { token_in_address } else { token_out_address },
524 currency_1: if zero_for_one { token_out_address } else { token_in_address },
525 fees: self.fees.clone(),
526 tick_spacing: self.tick_spacing,
527 };
528
529 let swap_params = SwapParams {
530 zero_for_one,
531 amount_specified: amount_to_swap,
532 sqrt_price_limit: self.sqrt_price,
533 };
534
535 if let Some(ref hook) = self.hook {
537 if has_permission(hook.address(), HookOptions::BeforeSwap) {
538 let before_swap_params = BeforeSwapParameters {
539 context: state_context.clone(),
540 sender: *EXTERNAL_ACCOUNT,
541 swap_params: swap_params.clone(),
542 hook_data: Bytes::new(),
543 };
544
545 let before_swap_result = hook
546 .before_swap(before_swap_params, None, None)
547 .map_err(|e| {
548 SimulationError::FatalError(format!(
549 "BeforeSwap hook simulation failed: {e:?}"
550 ))
551 })?;
552
553 before_swap_gas = before_swap_result.gas_estimate;
554 before_swap_delta = before_swap_result.result.amount_delta;
555 storage_overwrites = Some(before_swap_result.result.overwrites);
556
557 if before_swap_delta.as_i256() != I256::ZERO {
560 amount_to_swap += I256::from(before_swap_delta.get_specified_delta());
561 if amount_to_swap > I256::ZERO {
562 return Err(SimulationError::FatalError(
563 "Hook delta exceeds swap amount".into(),
564 ));
565 }
566 }
567
568 let hook_fee = before_swap_result
573 .result
574 .fee
575 .to::<u32>();
576 if hook_fee != 0 {
577 let cleaned_fee = lp_fee::remove_override_flag(hook_fee);
579
580 if !lp_fee::is_valid(cleaned_fee) {
582 return Err(SimulationError::FatalError(format!(
583 "LP fee override {} exceeds maximum {} pips",
584 cleaned_fee,
585 lp_fee::MAX_LP_FEE
586 )));
587 }
588
589 lp_fee_override = Some(cleaned_fee);
590 }
591 }
592 }
593
594 let result = self.swap(zero_for_one, amount_to_swap, None, lp_fee_override)?;
596
597 let mut swap_delta = BalanceDelta::from_swap_result(result.amount_calculated, zero_for_one);
599
600 let hook_delta_specified = before_swap_delta.get_specified_delta();
603 let mut hook_delta_unspecified = before_swap_delta.get_unspecified_delta();
604
605 if let Some(ref hook) = self.hook {
606 if has_permission(hook.address(), HookOptions::AfterSwap) {
607 let after_swap_params = AfterSwapParameters {
608 context: state_context,
609 sender: *EXTERNAL_ACCOUNT,
610 swap_params,
611 delta: swap_delta,
612 hook_data: Bytes::new(),
613 };
614
615 let after_swap_result = hook
616 .after_swap(after_swap_params, storage_overwrites, None)
617 .map_err(|e| {
618 SimulationError::FatalError(format!(
619 "AfterSwap hook simulation failed: {e:?}"
620 ))
621 })?;
622 after_swap_gas = after_swap_result.gas_estimate;
623 hook_delta_unspecified += after_swap_result.result;
624 }
625 }
626
627 if (hook_delta_specified != I128::ZERO) || (hook_delta_unspecified != I128::ZERO) {
630 let hook_delta = if (amount_specified < I256::ZERO) == zero_for_one {
631 BalanceDelta::new(hook_delta_specified, hook_delta_unspecified)
632 } else {
633 BalanceDelta::new(hook_delta_unspecified, hook_delta_specified)
634 };
635 swap_delta = swap_delta - hook_delta
637 }
638
639 let amount_out = if (amount_specified < I256::ZERO) == zero_for_one {
640 swap_delta.amount1()
641 } else {
642 swap_delta.amount0()
643 };
644
645 trace!(?amount_in, ?token_in, ?token_out, ?zero_for_one, ?result, "V4 SWAP");
646 let mut new_state = self.clone();
647 new_state.liquidity = result.liquidity;
648 new_state.tick = result.tick;
649 new_state.sqrt_price = result.sqrt_price;
650
651 let total_gas_used = result.gas_used + U256::from(before_swap_gas + after_swap_gas);
653 Ok(GetAmountOutResult::new(
654 u256_to_biguint(U256::from(amount_out.abs())),
655 u256_to_biguint(total_gas_used),
656 Box::new(new_state),
657 ))
658 }
659
660 fn get_limits(
661 &self,
662 token_in: Bytes,
663 token_out: Bytes,
664 ) -> Result<(BigUint, BigUint), SimulationError> {
665 if let Some(hook) = &self.hook {
666 if self.liquidity == 0 && self.has_no_initialized_ticks() {
668 match hook.get_amount_ranges(token_in.clone(), token_out.clone()) {
670 Ok(amount_ranges) => {
671 return Ok((
672 u256_to_biguint(amount_ranges.amount_in_range.1),
673 u256_to_biguint(amount_ranges.amount_out_range.1),
674 ))
675 }
676 Err(SimulationError::RecoverableError(msg))
679 if msg.contains("not implemented") || msg.contains("not set") =>
680 {
681 return self.find_limits_experimentally(token_in, token_out);
684 }
686 Err(e) => return Err(e),
687 }
688 }
689 }
690
691 if self.liquidity == 0 {
693 return Ok((BigUint::zero(), BigUint::zero()));
694 }
695
696 let zero_for_one = token_in < token_out;
697 let mut current_tick = self.tick;
698 let mut current_sqrt_price = self.sqrt_price;
699 let mut current_liquidity = self.liquidity;
700 let mut total_amount_in = U256::ZERO;
701 let mut total_amount_out = U256::ZERO;
702 let mut ticks_crossed: u64 = 0;
703
704 while let Ok((tick, initialized)) = self
707 .ticks
708 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
709 {
710 if ticks_crossed >= MAX_TICKS_CROSSED {
712 break;
713 }
714 ticks_crossed += 1;
715
716 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
718
719 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
721
722 let (amount_in, amount_out) = if zero_for_one {
725 let amount0 = get_amount0_delta(
726 sqrt_price_next,
727 current_sqrt_price,
728 current_liquidity,
729 true,
730 )?;
731 let amount1 = get_amount1_delta(
732 sqrt_price_next,
733 current_sqrt_price,
734 current_liquidity,
735 false,
736 )?;
737 (amount0, amount1)
738 } else {
739 let amount0 = get_amount0_delta(
740 sqrt_price_next,
741 current_sqrt_price,
742 current_liquidity,
743 false,
744 )?;
745 let amount1 = get_amount1_delta(
746 sqrt_price_next,
747 current_sqrt_price,
748 current_liquidity,
749 true,
750 )?;
751 (amount1, amount0)
752 };
753
754 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
756 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
757
758 if initialized {
763 let liquidity_raw = self
764 .ticks
765 .get_tick(next_tick)
766 .unwrap()
767 .net_liquidity;
768 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
769
770 match liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta) {
773 Ok(new_liquidity) => {
774 current_liquidity = new_liquidity;
775 }
776 Err(_) => {
777 break;
780 }
781 }
782 }
783
784 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
786 current_sqrt_price = sqrt_price_next;
787
788 if current_liquidity == 0 {
790 break;
791 }
792 }
793
794 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
795 }
796
797 fn delta_transition(
798 &mut self,
799 delta: ProtocolStateDelta,
800 tokens: &HashMap<Bytes, Token>,
801 balances: &Balances,
802 ) -> Result<(), TransitionError> {
803 if let Some(mut hook) = self.hook.clone() {
804 match hook.delta_transition(delta.clone(), tokens, balances) {
805 Ok(()) => self.set_hook_handler(hook),
806 Err(TransitionError::SimulationError(SimulationError::RecoverableError(msg)))
807 if msg.contains("not implemented") =>
808 {
809 }
811 Err(e) => return Err(e),
812 }
813 }
814
815 if let Some(liquidity) = delta
817 .updated_attributes
818 .get("liquidity")
819 {
820 self.liquidity = u128::from(liquidity.clone());
821 }
822 if let Some(sqrt_price) = delta
823 .updated_attributes
824 .get("sqrt_price_x96")
825 {
826 self.sqrt_price = U256::from_be_slice(sqrt_price);
827 }
828 if let Some(tick) = delta.updated_attributes.get("tick") {
829 self.tick = i24_be_bytes_to_i32(tick);
830 }
831 if let Some(lp_fee) = delta.updated_attributes.get("fee") {
832 self.fees.lp_fee = u32::from(lp_fee.clone());
833 }
834 if let Some(zero2one_protocol_fee) = delta
835 .updated_attributes
836 .get("protocol_fees/zero2one")
837 {
838 self.fees.zero_for_one = u32::from(zero2one_protocol_fee.clone());
839 }
840 if let Some(one2zero_protocol_fee) = delta
841 .updated_attributes
842 .get("protocol_fees/one2zero")
843 {
844 self.fees.one_for_zero = u32::from(one2zero_protocol_fee.clone());
845 }
846
847 for (key, value) in delta.updated_attributes.iter() {
849 if key.starts_with("ticks/") {
851 let parts: Vec<&str> = key.split('/').collect();
852 self.ticks
853 .set_tick_liquidity(
854 parts[1]
855 .parse::<i32>()
856 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
857 i128::from(value.clone()),
858 )
859 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
860 }
861 }
862 for key in delta.deleted_attributes.iter() {
864 if key.starts_with("ticks/") {
866 let parts: Vec<&str> = key.split('/').collect();
867 self.ticks
868 .set_tick_liquidity(
869 parts[1]
870 .parse::<i32>()
871 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
872 0,
873 )
874 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
875 }
876 }
877
878 Ok(())
879 }
880
881 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
890 if self.liquidity == 0 {
891 return Err(SimulationError::FatalError("No liquidity".to_string()));
892 }
893
894 let zero_for_one = params.token_in().address < params.token_out().address;
896 let fee_pips = self
897 .fees
898 .calculate_swap_fees_pips(zero_for_one, None);
899
900 match params.swap_constraint() {
901 SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
902 "Uniswap V4 does not support TradeLimitPrice constraint in query_pool_swap"
903 .to_string(),
904 None,
905 )),
906 SwapConstraint::PoolTargetPrice {
907 target,
908 tolerance: _,
909 min_amount_in: _,
910 max_amount_in: _,
911 } => {
912 if self.liquidity == 0 {
913 return Err(SimulationError::FatalError("No liquidity".to_string()));
914 }
915
916 let (amount_in, amount_out, swap_result) = clmm_swap_to_price(
917 self.sqrt_price,
918 ¶ms.token_in().address,
919 ¶ms.token_out().address,
920 target,
921 fee_pips,
922 Sign::Negative, |zero_for_one, amount_specified, sqrt_price_limit| {
924 self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit), None)
925 },
926 )?;
927
928 let mut new_state = self.clone();
929 new_state.liquidity = swap_result.liquidity;
930 new_state.tick = swap_result.tick;
931 new_state.sqrt_price = swap_result.sqrt_price;
932
933 Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
934 }
935 }
936 }
937
938 fn clone_box(&self) -> Box<dyn ProtocolSim> {
939 Box::new(self.clone())
940 }
941
942 fn as_any(&self) -> &dyn Any {
943 self
944 }
945
946 fn as_any_mut(&mut self) -> &mut dyn Any {
947 self
948 }
949
950 fn eq(&self, other: &dyn ProtocolSim) -> bool {
951 if let Some(other_state) = other
952 .as_any()
953 .downcast_ref::<UniswapV4State>()
954 {
955 self.liquidity == other_state.liquidity &&
956 self.sqrt_price == other_state.sqrt_price &&
957 self.fees == other_state.fees &&
958 self.tick == other_state.tick &&
959 self.ticks == other_state.ticks
960 } else {
961 false
962 }
963 }
964}
965
966#[cfg(test)]
967mod tests {
968 use std::{collections::HashSet, fs, path::Path, str::FromStr};
969
970 use alloy::primitives::aliases::U24;
971 use num_traits::FromPrimitive;
972 use rstest::rstest;
973 use serde_json::Value;
974 use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
975 use tycho_common::{models::Chain, simulation::protocol_sim::Price};
976
977 use super::*;
978 use crate::{
979 evm::{
980 engine_db::{
981 create_engine,
982 simulation_db::SimulationDB,
983 utils::{get_client, get_runtime},
984 },
985 protocol::{
986 uniswap_v4::hooks::{
987 angstrom::hook_handler::{AngstromFees, AngstromHookHandler},
988 generic_vm_hook_handler::GenericVMHookHandler,
989 },
990 utils::uniswap::{lp_fee, sqrt_price_math::get_sqrt_price_q96},
991 },
992 },
993 protocol::models::{DecoderContext, TryFromWithBlock},
994 };
995
996 fn usdc() -> Token {
998 Token::new(
999 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1000 "USDC",
1001 6,
1002 0,
1003 &[Some(10_000)],
1004 Default::default(),
1005 100,
1006 )
1007 }
1008
1009 fn weth() -> Token {
1010 Token::new(
1011 &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1012 "WETH",
1013 18,
1014 0,
1015 &[Some(10_000)],
1016 Default::default(),
1017 100,
1018 )
1019 }
1020
1021 fn eth() -> Token {
1022 Token::new(
1023 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
1024 "ETH",
1025 18,
1026 0,
1027 &[Some(10_000)],
1028 Default::default(),
1029 100,
1030 )
1031 }
1032
1033 fn token_x() -> Token {
1034 Token::new(
1035 &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
1036 "X",
1037 18,
1038 0,
1039 &[Some(10_000)],
1040 Default::default(),
1041 100,
1042 )
1043 }
1044
1045 fn token_y() -> Token {
1046 Token::new(
1047 &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
1048 "Y",
1049 18,
1050 0,
1051 &[Some(10_000)],
1052 Default::default(),
1053 100,
1054 )
1055 }
1056
1057 #[test]
1058 fn test_delta_transition() {
1059 let mut pool = UniswapV4State::new(
1060 1000,
1061 U256::from_str("1000").unwrap(),
1062 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 700 },
1063 100,
1064 60,
1065 vec![TickInfo::new(120, 10000).unwrap(), TickInfo::new(180, -10000).unwrap()],
1066 )
1067 .unwrap();
1068
1069 let attributes: HashMap<String, Bytes> = [
1070 ("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1071 ("sqrt_price_x96".to_string(), Bytes::from(1001_u64.to_be_bytes().to_vec())),
1072 ("tick".to_string(), Bytes::from(120_i32.to_be_bytes().to_vec())),
1073 ("protocol_fees/zero2one".to_string(), Bytes::from(50_u32.to_be_bytes().to_vec())),
1074 ("protocol_fees/one2zero".to_string(), Bytes::from(75_u32.to_be_bytes().to_vec())),
1075 ("fee".to_string(), Bytes::from(100_u32.to_be_bytes().to_vec())),
1076 ("ticks/-120/net_liquidity".to_string(), Bytes::from(10200_u64.to_be_bytes().to_vec())),
1077 ("ticks/120/net_liquidity".to_string(), Bytes::from(9800_u64.to_be_bytes().to_vec())),
1078 ("block_number".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1079 ("block_timestamp".to_string(), Bytes::from(1758201935_u64.to_be_bytes().to_vec())),
1080 ]
1081 .into_iter()
1082 .collect();
1083
1084 let delta = ProtocolStateDelta {
1085 component_id: "State1".to_owned(),
1086 updated_attributes: attributes,
1087 deleted_attributes: HashSet::new(),
1088 };
1089
1090 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1091 .unwrap();
1092
1093 assert_eq!(pool.liquidity, 2000);
1094 assert_eq!(pool.sqrt_price, U256::from(1001));
1095 assert_eq!(pool.tick, 120);
1096 assert_eq!(pool.fees.zero_for_one, 50);
1097 assert_eq!(pool.fees.one_for_zero, 75);
1098 assert_eq!(pool.fees.lp_fee, 100);
1099 assert_eq!(
1100 pool.ticks
1101 .get_tick(-120)
1102 .unwrap()
1103 .net_liquidity,
1104 10200
1105 );
1106 assert_eq!(
1107 pool.ticks
1108 .get_tick(120)
1109 .unwrap()
1110 .net_liquidity,
1111 9800
1112 );
1113 }
1114
1115 #[tokio::test]
1116 async fn test_swap_sim() {
1119 let project_root = env!("CARGO_MANIFEST_DIR");
1120
1121 let asset_path = Path::new(project_root)
1122 .join("tests/assets/decoder/uniswap_v4_snapshot_sepolia_block_7239119.json");
1123 let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1124 let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1125
1126 let state: ComponentWithState = serde_json::from_value(data)
1127 .expect("Expected json to match ComponentWithState structure");
1128
1129 let block = BlockHeader {
1130 number: 7239119,
1131 hash: Bytes::from_str(
1132 "0x28d41d40f2ac275a4f5f621a636b9016b527d11d37d610a45ac3a821346ebf8c",
1133 )
1134 .expect("Invalid block hash"),
1135 parent_hash: Bytes::from(vec![0; 32]),
1136 ..Default::default()
1137 };
1138
1139 let t0 = Token::new(
1140 &Bytes::from_str("0x647e32181a64f4ffd4f0b0b4b052ec05b277729c").unwrap(),
1141 "T0",
1142 18,
1143 0,
1144 &[Some(10_000)],
1145 Chain::Ethereum,
1146 100,
1147 );
1148 let t1 = Token::new(
1149 &Bytes::from_str("0xe390a1c311b26f14ed0d55d3b0261c2320d15ca5").unwrap(),
1150 "T0",
1151 18,
1152 0,
1153 &[Some(10_000)],
1154 Chain::Ethereum,
1155 100,
1156 );
1157
1158 let all_tokens = [t0.clone(), t1.clone()]
1159 .iter()
1160 .map(|t| (t.address.clone(), t.clone()))
1161 .collect();
1162
1163 let usv4_state = UniswapV4State::try_from_with_header(
1164 state,
1165 block,
1166 &Default::default(),
1167 &all_tokens,
1168 &DecoderContext::new(),
1169 )
1170 .await
1171 .unwrap();
1172
1173 let res = usv4_state
1174 .get_amount_out(BigUint::from_u64(1000000000000000000).unwrap(), &t0, &t1)
1175 .unwrap();
1176
1177 let expected_amount = BigUint::from(9999909699895_u64);
1201 assert_eq!(res.amount, expected_amount);
1202 }
1203
1204 #[tokio::test]
1205 async fn test_get_limits() {
1206 let block = BlockHeader {
1207 number: 22689129,
1208 hash: Bytes::from_str(
1209 "0x7763ea30d11aef68da729b65250c09a88ad00458c041064aad8c9a9dbf17adde",
1210 )
1211 .expect("Invalid block hash"),
1212 parent_hash: Bytes::from(vec![0; 32]),
1213 ..Default::default()
1214 };
1215
1216 let project_root = env!("CARGO_MANIFEST_DIR");
1217 let asset_path =
1218 Path::new(project_root).join("tests/assets/decoder/uniswap_v4_snapshot.json");
1219 let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1220 let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1221
1222 let state: ComponentWithState = serde_json::from_value(data)
1223 .expect("Expected json to match ComponentWithState structure");
1224
1225 let t0 = Token::new(
1226 &Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
1227 "WBTC",
1228 8,
1229 0,
1230 &[Some(10_000)],
1231 Chain::Ethereum,
1232 100,
1233 );
1234 let t1 = Token::new(
1235 &Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
1236 "USDT",
1237 6,
1238 0,
1239 &[Some(10_000)],
1240 Chain::Ethereum,
1241 100,
1242 );
1243
1244 let all_tokens = [t0.clone(), t1.clone()]
1245 .iter()
1246 .map(|t| (t.address.clone(), t.clone()))
1247 .collect();
1248
1249 let usv4_state = UniswapV4State::try_from_with_header(
1250 state,
1251 block,
1252 &Default::default(),
1253 &all_tokens,
1254 &DecoderContext::new(),
1255 )
1256 .await
1257 .unwrap();
1258
1259 let res = usv4_state
1260 .get_limits(t0.address.clone(), t1.address.clone())
1261 .unwrap();
1262
1263 assert_eq!(&res.0, &BigUint::from_u128(71698353688830259750744466706).unwrap()); let out = usv4_state
1266 .get_amount_out(res.0, &t0, &t1)
1267 .expect("swap for limit in didn't work");
1268
1269 assert_eq!(&res.1, &out.amount);
1270 }
1271 #[test]
1272 fn test_get_amount_out_no_hook() {
1273 let usv4_state = UniswapV4State::new(
1278 541501951282951892,
1279 U256::from_str("5362798333066270795901222").unwrap(), UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 100 },
1281 -192022,
1282 1,
1283 vec![
1285 TickInfo {
1286 index: -887272,
1287 net_liquidity: 460382969070005,
1288 sqrt_price: U256::from(4295128739_u64),
1289 },
1290 TickInfo {
1291 index: -207244,
1292 net_liquidity: 561268407024557,
1293 sqrt_price: U256::from_str("2505291706254206075074035").unwrap(),
1294 },
1295 TickInfo {
1296 index: -196411,
1297 net_liquidity: 825711941800452,
1298 sqrt_price: U256::from_str("4306080513146952705853399").unwrap(),
1299 },
1300 TickInfo {
1301 index: -196257,
1302 net_liquidity: 64844666874010,
1303 sqrt_price: U256::from_str("4339363644587371378270009").unwrap(),
1304 },
1305 TickInfo {
1306 index: -195611,
1307 net_liquidity: 2344045150766798,
1308 sqrt_price: U256::from_str("4481806029599743916020126").unwrap(),
1309 },
1310 TickInfo {
1311 index: -194715,
1312 net_liquidity: 391037380558274654,
1313 sqrt_price: U256::from_str("4687145946111116896040494").unwrap(),
1314 },
1315 TickInfo {
1316 index: -194599,
1317 net_liquidity: 89032603464508,
1318 sqrt_price: U256::from_str("4714409015946702405379370").unwrap(),
1319 },
1320 TickInfo {
1321 index: -194389,
1322 net_liquidity: 66635600426483168,
1323 sqrt_price: U256::from_str("4764168603367683402636621").unwrap(),
1324 },
1325 TickInfo {
1326 index: -194160,
1327 net_liquidity: 6123093436523361,
1328 sqrt_price: U256::from_str("4819029067726467394386780").unwrap(),
1329 },
1330 TickInfo {
1331 index: -194025,
1332 net_liquidity: 79940813798964,
1333 sqrt_price: U256::from_str("4851665907541490407930032").unwrap(),
1334 },
1335 TickInfo {
1336 index: -193922,
1337 net_liquidity: 415630967437234,
1338 sqrt_price: U256::from_str("4876715181040466809166531").unwrap(),
1339 },
1340 TickInfo {
1341 index: -193876,
1342 net_liquidity: 9664144015186047,
1343 sqrt_price: U256::from_str("4887943972687250473582419").unwrap(),
1344 },
1345 TickInfo {
1346 index: -193818,
1347 net_liquidity: 435344726052344,
1348 sqrt_price: U256::from_str("4902138873132735049121973").unwrap(),
1349 },
1350 TickInfo {
1351 index: -193804,
1352 net_liquidity: 221726179374067,
1353 sqrt_price: U256::from_str("4905571399964683340605904").unwrap(),
1354 },
1355 TickInfo {
1356 index: -193719,
1357 net_liquidity: 101340835774487,
1358 sqrt_price: U256::from_str("4926463397882393957462188").unwrap(),
1359 },
1360 TickInfo {
1361 index: -193690,
1362 net_liquidity: 193367475630077,
1363 sqrt_price: U256::from_str("4933611593595025190448924").unwrap(),
1364 },
1365 TickInfo {
1366 index: -193643,
1367 net_liquidity: 357016631583746,
1368 sqrt_price: U256::from_str("4945218633428068823432932").unwrap(),
1369 },
1370 TickInfo {
1371 index: -193520,
1372 net_liquidity: 917243184365178,
1373 sqrt_price: U256::from_str("4975723910367862081017120").unwrap(),
1374 },
1375 TickInfo {
1376 index: -193440,
1377 net_liquidity: 114125890211958292,
1378 sqrt_price: U256::from_str("4995665665861492533686137").unwrap(),
1379 },
1380 TickInfo {
1381 index: -193380,
1382 net_liquidity: -65980729148766579,
1383 sqrt_price: U256::from_str("5010674414300823856025303").unwrap(),
1384 },
1385 TickInfo {
1386 index: -192891,
1387 net_liquidity: 1687883551433195,
1388 sqrt_price: U256::from_str("5134689105039642314202223").unwrap(),
1389 },
1390 TickInfo {
1391 index: -192573,
1392 net_liquidity: 11108903221360975,
1393 sqrt_price: U256::from_str("5216979018647067786855495").unwrap(),
1394 },
1395 TickInfo {
1396 index: -192448,
1397 net_liquidity: 32888457482352,
1398 sqrt_price: U256::from_str("5249685603828944002327927").unwrap(),
1399 },
1400 TickInfo {
1401 index: -191525,
1402 net_liquidity: -221726179374067,
1403 sqrt_price: U256::from_str("5497623359964843320146512").unwrap(),
1404 },
1405 TickInfo {
1406 index: -191447,
1407 net_liquidity: -32888457482352,
1408 sqrt_price: U256::from_str("5519104878745833608097296").unwrap(),
1409 },
1410 TickInfo {
1411 index: -191444,
1412 net_liquidity: -114125890211958292,
1413 sqrt_price: U256::from_str("5519932765173943847315221").unwrap(),
1414 },
1415 TickInfo {
1416 index: -191417,
1417 net_liquidity: -101340835774487,
1418 sqrt_price: U256::from_str("5527389333636021285046380").unwrap(),
1419 },
1420 TickInfo {
1421 index: -191384,
1422 net_liquidity: -9664144015186047,
1423 sqrt_price: U256::from_str("5536516597603056457376182").unwrap(),
1424 },
1425 TickInfo {
1426 index: -191148,
1427 net_liquidity: -561268407024557,
1428 sqrt_price: U256::from_str("5602231161238705865493165").unwrap(),
1429 },
1430 TickInfo {
1431 index: -191147,
1432 net_liquidity: -1687883551433195,
1433 sqrt_price: U256::from_str("5602511265794328966803451").unwrap(),
1434 },
1435 TickInfo {
1436 index: -191091,
1437 net_liquidity: -89032603464508,
1438 sqrt_price: U256::from_str("5618219493196441347292357").unwrap(),
1439 },
1440 TickInfo {
1441 index: -190950,
1442 net_liquidity: -189177935487638,
1443 sqrt_price: U256::from_str("5657965894785859782969011").unwrap(),
1444 },
1445 TickInfo {
1446 index: -190756,
1447 net_liquidity: -6123093436523361,
1448 sqrt_price: U256::from_str("5713112435031881967192022").unwrap(),
1449 },
1450 TickInfo {
1451 index: -190548,
1452 net_liquidity: -193367475630077,
1453 sqrt_price: U256::from_str("5772835841671084402427710").unwrap(),
1454 },
1455 TickInfo {
1456 index: -190430,
1457 net_liquidity: -11108903221360975,
1458 sqrt_price: U256::from_str("5806994534290341208820930").unwrap(),
1459 },
1460 TickInfo {
1461 index: -190195,
1462 net_liquidity: -391583014714302569,
1463 sqrt_price: U256::from_str("5875625707132601785181387").unwrap(),
1464 },
1465 TickInfo {
1466 index: -190043,
1467 net_liquidity: -357016631583746,
1468 sqrt_price: U256::from_str("5920448331650864936739481").unwrap(),
1469 },
1470 TickInfo {
1471 index: -189779,
1472 net_liquidity: -917243184365178,
1473 sqrt_price: U256::from_str("5999112356918485175181346").unwrap(),
1474 },
1475 TickInfo {
1476 index: -189663,
1477 net_liquidity: -2344045150766798,
1478 sqrt_price: U256::from_str("6034006559279282606084981").unwrap(),
1479 },
1480 TickInfo {
1481 index: -189620,
1482 net_liquidity: -435344726052344,
1483 sqrt_price: U256::from_str("6046992979471024289177519").unwrap(),
1484 },
1485 TickInfo {
1486 index: -189409,
1487 net_liquidity: -825711941800452,
1488 sqrt_price: U256::from_str("6111123241285165242130911").unwrap(),
1489 },
1490 TickInfo {
1491 index: -189325,
1492 net_liquidity: -3947182209207,
1493 sqrt_price: U256::from_str("6136842645893819031257990").unwrap(),
1494 },
1495 TickInfo {
1496 index: -189324,
1497 net_liquidity: -415630967437234,
1498 sqrt_price: U256::from_str("6137149480355443943537284").unwrap(),
1499 },
1500 TickInfo {
1501 index: -115136,
1502 net_liquidity: 462452451821,
1503 sqrt_price: U256::from_str("250529060232794967902094762").unwrap(),
1504 },
1505 TickInfo {
1506 index: -92109,
1507 net_liquidity: -462452451821,
1508 sqrt_price: U256::from_str("792242363124136400178523925").unwrap(),
1509 },
1510 TickInfo {
1511 index: 887272,
1512 net_liquidity: -521280453734808,
1513 sqrt_price: U256::from_str("1461446703485210103287273052203988822378723970342")
1514 .unwrap(),
1515 },
1516 ],
1517 )
1518 .unwrap();
1519
1520 let t0 = usdc();
1521 let t1 = eth();
1522
1523 let out = usv4_state
1524 .get_amount_out(BigUint::from_u64(2000000).unwrap(), &t0, &t1)
1525 .unwrap();
1526
1527 assert_eq!(out.amount, BigUint::from_str("436478419853848").unwrap())
1528 }
1529
1530 #[test]
1531 fn test_get_amount_out_euler_hook() {
1532 let block = BlockHeader {
1548 number: 22689128,
1549 hash: Bytes::from_str(
1550 "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
1551 )
1552 .expect("Invalid block hash"),
1553 timestamp: 1749739055,
1554 ..Default::default()
1555 };
1556
1557 let mut usv4_state = UniswapV4State::new(
1560 0,
1561 U256::from_str("4295128740").unwrap(),
1562 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
1563 0,
1564 1,
1565 vec![],
1566 )
1567 .unwrap();
1568
1569 let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
1570 .expect("Invalid hook address");
1571
1572 let db = SimulationDB::new(
1573 get_client(None).expect("Failed to create client"),
1574 get_runtime().expect("Failed to get runtime"),
1575 Some(block.clone()),
1576 );
1577 let engine = create_engine(db, true).expect("Failed to create simulation engine");
1578 let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
1579 .expect("Invalid pool manager address");
1580
1581 let hook_handler = GenericVMHookHandler::new(
1582 hook_address,
1583 engine,
1584 pool_manager,
1585 HashMap::new(),
1586 HashMap::new(),
1587 None,
1588 true, )
1590 .unwrap();
1591
1592 let t0 = usdc();
1593 let t1 = weth();
1594
1595 usv4_state.set_hook_handler(Box::new(hook_handler));
1596 let out = usv4_state
1597 .get_amount_out(BigUint::from_u64(7407000000).unwrap(), &t0, &t1)
1598 .unwrap();
1599
1600 assert_eq!(out.amount, BigUint::from_str("2681115183499232721").unwrap())
1601 }
1602
1603 #[test]
1604 fn test_get_amount_out_angstrom_hook() {
1605 let mut usv4_state = UniswapV4State::new(
1607 66319800403673162,
1609 U256::from_str("1314588940601923011323000261788004").unwrap(),
1610 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 8388608 },
1612 194343,
1613 10,
1614 vec![
1615 TickInfo::new(-887270, 198117767801).unwrap(),
1616 TickInfo::new(191990, 24561988698695).unwrap(),
1617 TickInfo::new(192280, 2839631428751224).unwrap(),
1618 TickInfo::new(193130, 318786492813931).unwrap(),
1619 TickInfo::new(194010, 26209207141081).unwrap(),
1620 TickInfo::new(194210, -26209207141081).unwrap(),
1621 TickInfo::new(194220, 63136622375641511).unwrap(),
1622 TickInfo::new(194420, -63136622375641511).unwrap(),
1623 TickInfo::new(195130, -318786492813931).unwrap(),
1624 TickInfo::new(196330, -2839631428751224).unwrap(),
1625 TickInfo::new(197100, -24561988698695).unwrap(),
1626 TickInfo::new(887270, -198117767801).unwrap(),
1627 ],
1628 )
1629 .unwrap();
1630
1631 let fees = AngstromFees {
1632 unlock: U24::from(338),
1639 protocol_unlock: U24::from(112),
1640 };
1641 let hook_handler = AngstromHookHandler::new(
1642 Address::from_str("0x0000000aa232009084bd71a5797d089aa4edfad4").unwrap(),
1643 Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90").unwrap(),
1644 fees,
1645 false,
1646 );
1647
1648 let t0 = usdc();
1649 let t1 = weth();
1650
1651 usv4_state.set_hook_handler(Box::new(hook_handler));
1652 let out = usv4_state
1653 .get_amount_out(
1654 BigUint::from_u64(
1655 6645198144, )
1657 .unwrap(),
1658 &t0, &t1, )
1661 .unwrap();
1662
1663 assert_eq!(out.amount, BigUint::from_str("1825627051870330472").unwrap())
1664 }
1665
1666 #[test]
1667 fn test_spot_price_with_recoverable_error() {
1668 let usv4_state = UniswapV4State::new(
1672 1000000000000000000u128, U256::from_str("79228162514264337593543950336").unwrap(), UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 },
1675 0,
1676 60,
1677 vec![
1678 TickInfo::new(-600, 500000000000000000i128).unwrap(),
1679 TickInfo::new(600, -500000000000000000i128).unwrap(),
1680 ],
1681 )
1682 .unwrap();
1683
1684 let spot_price_result = usv4_state.spot_price(&usdc(), &weth());
1686 assert!(spot_price_result.is_ok());
1687
1688 let price = spot_price_result.unwrap();
1691 assert!(price > 0.0);
1692 }
1693
1694 #[test]
1695 fn test_get_limits_with_hook_managed_liquidity_no_ranges_entrypoint() {
1696 let block = BlockHeader {
1701 number: 22689128,
1702 hash: Bytes::from_str(
1703 "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
1704 )
1705 .expect("Invalid block hash"),
1706 timestamp: 1749739055,
1707 ..Default::default()
1708 };
1709
1710 let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
1711 .expect("Invalid hook address");
1712
1713 let db = SimulationDB::new(
1714 get_client(None).expect("Failed to create client"),
1715 get_runtime().expect("Failed to get runtime"),
1716 Some(block.clone()),
1717 );
1718 let engine = create_engine(db, true).expect("Failed to create simulation engine");
1719 let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
1720 .expect("Invalid pool manager address");
1721
1722 let hook_handler = GenericVMHookHandler::new(
1725 hook_address,
1726 engine,
1727 pool_manager,
1728 HashMap::new(),
1729 HashMap::new(),
1730 None,
1731 true, )
1733 .unwrap();
1734
1735 let mut usv4_state = UniswapV4State::new(
1737 0, U256::from_str("4295128740").unwrap(),
1739 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
1740 0, 1, vec![], )
1744 .unwrap();
1745
1746 usv4_state.set_hook_handler(Box::new(hook_handler));
1747
1748 let token_in = usdc().address;
1749 let token_out = weth().address;
1750
1751 let (amount_in_limit, amount_out_limit) = usv4_state
1752 .get_limits(token_in, token_out)
1753 .expect("Should find limits through experimental swapping");
1754
1755 assert!(amount_in_limit > BigUint::from(10u64).pow(12));
1758 assert!(amount_in_limit < BigUint::from(10u64).pow(14));
1759
1760 assert!(amount_out_limit > BigUint::from(10u64).pow(20));
1762 assert!(amount_out_limit < BigUint::from(10u64).pow(22));
1763 }
1764
1765 #[rstest]
1766 #[case::high_liquidity(u128::MAX / 2)] #[case::medium_liquidity(10000000000000000000u128)] #[case::minimal_liquidity(1000u128)] fn test_find_max_amount(#[case] liquidity: u128) {
1770 let fees = UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 };
1772 let tick_spacing = 60;
1773 let ticks = vec![
1774 TickInfo::new(-600, (liquidity / 4) as i128).unwrap(),
1775 TickInfo::new(600, -((liquidity / 4) as i128)).unwrap(),
1776 ];
1777
1778 let usv4_state = UniswapV4State::new(
1779 liquidity,
1780 U256::from_str("79228162514264337593543950336").unwrap(),
1781 fees,
1782 0,
1783 tick_spacing,
1784 ticks,
1785 )
1786 .unwrap();
1787
1788 let token_in = usdc();
1789 let token_out = weth();
1790
1791 let (max_amount_in, _max_amount_out) = usv4_state
1792 .find_max_amount(&token_in, &token_out)
1793 .unwrap();
1794
1795 let success = usv4_state
1796 .get_amount_out(max_amount_in.clone(), &token_in, &token_out)
1797 .is_ok();
1798 assert!(success, "Should be able to swap the exact max amount.");
1799
1800 let one_more = &max_amount_in + BigUint::from(1u64);
1801 let should_fail = usv4_state
1802 .get_amount_out(one_more, &token_in, &token_out)
1803 .is_err();
1804 assert!(should_fail, "Swapping max_amount + 1 should fail.");
1805 }
1806
1807 #[test]
1808 fn test_calculate_swap_fees_with_override() {
1809 let fees = UniswapV4Fees::new(100, 90, 500);
1811
1812 let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
1815 assert_eq!(total_zero_for_one, 600);
1817
1818 let total_with_override = fees.calculate_swap_fees_pips(true, Some(1000));
1820 assert_eq!(total_with_override, 1100);
1822 }
1823
1824 #[test]
1825 fn test_max_combined_fees_stays_valid() {
1826 let fees = UniswapV4Fees::new(1000, 1000, 1000);
1828 let total = fees.calculate_swap_fees_pips(true, Some(lp_fee::MAX_LP_FEE));
1829
1830 assert_eq!(total, 1_000_000);
1833 }
1834
1835 #[test]
1836 fn test_get_limits_graceful_underflow() {
1837 let usv4_state = UniswapV4State::new(
1839 1000000,
1840 U256::from_str("79228162514264337593543950336").unwrap(), UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
1842 0,
1843 60,
1844 vec![
1845 TickInfo {
1848 index: -60,
1849 net_liquidity: 2000000, sqrt_price: U256::from_str("79051508376726796163471739988").unwrap(),
1851 },
1852 ],
1853 )
1854 .unwrap();
1855
1856 let usdc = usdc();
1857 let weth = weth();
1858
1859 let (limit_in, limit_out) = usv4_state
1860 .get_limits(usdc.address.clone(), weth.address.clone())
1861 .unwrap();
1862
1863 assert!(limit_in > BigUint::zero());
1865 assert!(limit_out > BigUint::zero());
1866 }
1867
1868 const MAX_PROTOCOL_FEE: u32 = 1000;
1873
1874 #[rstest]
1875 #[case::max_protocol_and_max_lp(MAX_PROTOCOL_FEE, lp_fee::MAX_LP_FEE, lp_fee::MAX_LP_FEE)]
1876 #[case::max_protocol_with_3000_lp(MAX_PROTOCOL_FEE, 3000, 3997)]
1877 #[case::max_protocol_with_zero_lp(MAX_PROTOCOL_FEE, 0, MAX_PROTOCOL_FEE)]
1878 #[case::zero_protocol_zero_lp(0, 0, 0)]
1879 #[case::zero_protocol_with_1000_lp(0, 1000, 1000)]
1880 fn test_calculate_swap_fees_uniswap_test_cases(
1881 #[case] protocol_fee: u32,
1882 #[case] lp_fee: u32,
1883 #[case] expected: u32,
1884 ) {
1885 let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
1886 let result = fees.calculate_swap_fees_pips(true, None);
1887 assert_eq!(result, expected);
1888 }
1889
1890 #[test]
1891 fn test_calculate_swap_fees_with_dynamic_fee() {
1892 let fees = UniswapV4Fees::new(100, 90, lp_fee::DYNAMIC_FEE_FLAG);
1894
1895 let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
1897 assert_eq!(total_zero_for_one, 100);
1899
1900 let total_with_override = fees.calculate_swap_fees_pips(true, Some(500));
1902 assert_eq!(total_with_override, 600);
1904 }
1905
1906 #[test]
1907 fn test_calculate_swap_fees_direction_matters() {
1908 let fees = UniswapV4Fees::new(100, 200, 500);
1910
1911 let zero_for_one_fee = fees.calculate_swap_fees_pips(true, None);
1912 assert_eq!(zero_for_one_fee, 600);
1914
1915 let one_for_zero_fee = fees.calculate_swap_fees_pips(false, None);
1916 assert_eq!(one_for_zero_fee, 700);
1918 }
1919
1920 #[rstest]
1921 #[case::high_lp_fee(1000, 500_000, 500_500)] #[case::mid_fees(500, 500_000, 500_250)] #[case::low_fees(100, 100_000, 100_090)] fn test_calculate_swap_fees_formula_precision(
1925 #[case] protocol_fee: u32,
1926 #[case] lp_fee: u32,
1927 #[case] expected: u32,
1928 ) {
1929 let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
1932 let result = fees.calculate_swap_fees_pips(true, None);
1933 assert_eq!(result, expected, "Failed for protocol={}, lp={}", protocol_fee, lp_fee);
1934 }
1935
1936 #[test]
1937 fn test_calculate_swap_fees_override_takes_precedence() {
1938 let fees = UniswapV4Fees::new(100, 100, 3000);
1940
1941 let result = fees.calculate_swap_fees_pips(true, Some(5000));
1943 assert_eq!(result, 5100);
1945
1946 let result_no_override = fees.calculate_swap_fees_pips(true, None);
1948 assert_eq!(result_no_override, 3100);
1950 }
1951
1952 #[test]
1953 fn test_calculate_swap_fees_zero_protocol_fee() {
1954 let fees = UniswapV4Fees::new(0, 0, 3000);
1956 let result = fees.calculate_swap_fees_pips(true, None);
1957 assert_eq!(result, 3000);
1959 }
1960
1961 #[test]
1962 fn test_calculate_swap_fees_zero_lp_fee() {
1963 let fees = UniswapV4Fees::new(500, 500, 0);
1965 let result = fees.calculate_swap_fees_pips(true, None);
1966 assert_eq!(result, 500);
1968 }
1969
1970 fn create_basic_v4_test_pool() -> UniswapV4State {
1972 let liquidity = 100_000_000_000_000_000_000u128; let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
1974 .expect("Failed to calculate sqrt price");
1975 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
1976
1977 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
1978
1979 UniswapV4State::new(
1980 liquidity,
1981 sqrt_price,
1982 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 }, tick,
1984 60, ticks,
1986 )
1987 .expect("Failed to create pool")
1988 }
1989
1990 #[test]
1991 fn test_swap_to_price_price_too_high() {
1992 let pool = create_basic_v4_test_pool();
1993
1994 let token_x = token_x();
1995 let token_y = token_y();
1996
1997 let target_price = Price::new(BigUint::from(10_000_000u64), BigUint::from(1_000_000u64));
1999
2000 let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
2001 token_x,
2002 token_y,
2003 SwapConstraint::PoolTargetPrice {
2004 target: target_price,
2005 tolerance: 0f64,
2006 min_amount_in: None,
2007 max_amount_in: None,
2008 },
2009 ));
2010 assert!(result.is_err(), "Should return error when target price is unreachable");
2011 }
2012
2013 #[test]
2014 fn test_swap_to_price_no_liquidity() {
2015 let pool = UniswapV4State::new(
2017 0, U256::from_str("79228162514264337593543950336").unwrap(),
2019 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2020 0,
2021 60,
2022 vec![],
2023 )
2024 .unwrap();
2025
2026 let token_x = token_x();
2027 let token_y = token_y();
2028
2029 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_000_000u64));
2030
2031 let pool_swap = pool.query_pool_swap(&QueryPoolSwapParams::new(
2032 token_x,
2033 token_y,
2034 SwapConstraint::PoolTargetPrice {
2035 target: target_price,
2036 tolerance: 0f64,
2037 min_amount_in: None,
2038 max_amount_in: None,
2039 },
2040 ));
2041
2042 assert!(pool_swap.is_err());
2043 }
2044
2045 #[test]
2046 fn test_swap_to_price_with_protocol_fees() {
2047 let liquidity = 100_000_000_000_000_000_000u128;
2048 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2049 .expect("Failed to calculate sqrt price");
2050 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2051
2052 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2053
2054 let pool = UniswapV4State::new(
2056 liquidity,
2057 sqrt_price,
2058 UniswapV4Fees {
2059 zero_for_one: 1000, one_for_zero: 200, lp_fee: 3000, },
2063 tick,
2064 60,
2065 ticks,
2066 )
2067 .expect("Failed to create pool");
2068
2069 let token_x = token_x();
2070 let token_y = token_y();
2071
2072 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2076 let pool_swap_forward = pool
2077 .query_pool_swap(&QueryPoolSwapParams::new(
2078 token_x.clone(),
2079 token_y.clone(),
2080 SwapConstraint::PoolTargetPrice {
2081 target: target_price,
2082 tolerance: 0f64,
2083 min_amount_in: None,
2084 max_amount_in: None,
2085 },
2086 ))
2087 .expect("swap_to_price failed");
2088
2089 let target_price_reverse =
2091 Price::new(BigUint::from(1_010_000u64), BigUint::from(2_040_000u64));
2092 let pool_swap_backward = pool
2093 .query_pool_swap(&QueryPoolSwapParams::new(
2094 token_y,
2095 token_x,
2096 SwapConstraint::PoolTargetPrice {
2097 target: target_price_reverse,
2098 tolerance: 0f64,
2099 min_amount_in: None,
2100 max_amount_in: None,
2101 },
2102 ))
2103 .expect("swap_to_price failed");
2104
2105 assert!(
2106 pool_swap_backward.amount_out().clone() > BigUint::ZERO,
2107 "One for zero swap should return non-zero output"
2108 );
2109
2110 assert!(
2113 pool_swap_forward.amount_out() < pool_swap_backward.amount_in(),
2114 "Backward fees should be lower therefore backward swap should be bigger"
2115 );
2116 assert!(
2117 pool_swap_forward.amount_in() < pool_swap_backward.amount_out(),
2118 "Backward fees should be lower therefore backward swap should be bigger"
2119 );
2120 }
2121
2122 #[test]
2123 fn test_swap_to_price_different_targets() {
2124 let pool = create_basic_v4_test_pool();
2126
2127 let token_x = token_x();
2128 let token_y = token_y();
2129
2130 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2133 let pool_swap_close = pool
2134 .query_pool_swap(&QueryPoolSwapParams::new(
2135 token_x.clone(),
2136 token_y.clone(),
2137 SwapConstraint::PoolTargetPrice {
2138 target: target_price,
2139 tolerance: 0f64,
2140 min_amount_in: None,
2141 max_amount_in: None,
2142 },
2143 ))
2144 .expect("swap_to_price failed");
2145 assert!(
2146 *pool_swap_close.amount_out() > BigUint::ZERO,
2147 "Expected non-zero for 1.98 Y/X target"
2148 );
2149
2150 let target_price = Price::new(BigUint::from(1_900_000u64), BigUint::from(1_000_000u64));
2152 let pool_swap_below = pool
2153 .query_pool_swap(&QueryPoolSwapParams::new(
2154 token_x.clone(),
2155 token_y.clone(),
2156 SwapConstraint::PoolTargetPrice {
2157 target: target_price,
2158 tolerance: 0f64,
2159 min_amount_in: None,
2160 max_amount_in: None,
2161 },
2162 ))
2163 .expect("swap_to_price failed");
2164 assert!(
2165 pool_swap_below.amount_out().clone() > BigUint::ZERO,
2166 "Expected non-zero for 1.90 Y/X target"
2167 );
2168
2169 let target_price = Price::new(BigUint::from(1_500_000u64), BigUint::from(1_000_000u64));
2171 let pool_swap_far = pool
2172 .query_pool_swap(&QueryPoolSwapParams::new(
2173 token_x,
2174 token_y,
2175 SwapConstraint::PoolTargetPrice {
2176 target: target_price,
2177 tolerance: 0f64,
2178 min_amount_in: None,
2179 max_amount_in: None,
2180 },
2181 ))
2182 .expect("swap_to_price failed");
2183 assert!(
2184 pool_swap_far.amount_out().clone() > BigUint::ZERO,
2185 "Expected non-zero for 1.5 Y/X target"
2186 );
2187
2188 assert!(
2190 pool_swap_close.amount_out().clone() < pool_swap_below.amount_out().clone(),
2191 "Closer target (1.98 Y/X) should require less volume than medium target (1.90 Y/X). \
2192 Got close: {}, medium: {}",
2193 pool_swap_close.amount_out().clone(),
2194 pool_swap_below.amount_out().clone()
2195 );
2196 assert!(
2197 pool_swap_below.amount_out().clone() < pool_swap_far.amount_out().clone(),
2198 "Medium target (1.90 Y/X) should require less volume than far target (1.5 Y/X). \
2199 Got medium: {}, far: {}",
2200 pool_swap_below.amount_out().clone(),
2201 pool_swap_far.amount_out().clone()
2202 );
2203 }
2204
2205 #[test]
2206 fn test_swap_to_price_around_spot_price() {
2207 let liquidity = 10_000_000_000_000_000u128;
2208 let sqrt_price =
2209 get_sqrt_price_q96(U256::from(2_000_000_000u64), U256::from(1_000_000_000u64))
2210 .expect("Failed to calculate sqrt price");
2211 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2212
2213 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2214
2215 let pool = UniswapV4State::new(
2217 liquidity,
2218 sqrt_price,
2219 UniswapV4Fees {
2220 zero_for_one: 0,
2221 one_for_zero: 0,
2222 lp_fee: 500, },
2224 tick,
2225 60,
2226 ticks,
2227 )
2228 .expect("Failed to create pool");
2229
2230 let token_x = token_x();
2231 let token_y = token_y();
2232
2233 let target_price = Price::new(BigUint::from(1_999_750u64), BigUint::from(1_000_250u64));
2235
2236 let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
2237 token_x.clone(),
2238 token_y.clone(),
2239 SwapConstraint::PoolTargetPrice {
2240 target: target_price,
2241 tolerance: 0f64,
2242 min_amount_in: None,
2243 max_amount_in: None,
2244 },
2245 ));
2246 assert!(result.is_err(), "Should return error when target price is unreachable");
2247
2248 let target_price = Price::new(BigUint::from(1_999_000u64), BigUint::from(1_001_000u64));
2250
2251 let pool_swap = pool
2252 .query_pool_swap(&QueryPoolSwapParams::new(
2253 token_x,
2254 token_y,
2255 SwapConstraint::PoolTargetPrice {
2256 target: target_price,
2257 tolerance: 0f64,
2258 min_amount_in: None,
2259 max_amount_in: None,
2260 },
2261 ))
2262 .expect("swap_to_price failed");
2263
2264 let expected_amount_out =
2266 BigUint::from_str("7062236922008").expect("Failed to parse expected value");
2267 assert_eq!(
2268 pool_swap.amount_out().clone(),
2269 expected_amount_out,
2270 "V4 should match V3 output with same fees (0.05%)"
2271 );
2272 }
2273
2274 #[test]
2275 fn test_swap_to_price_matches_get_amount_out() {
2276 let pool = create_basic_v4_test_pool();
2277
2278 let token_x = token_x();
2279 let token_y = token_y();
2280
2281 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2283 let pool_swap = pool
2284 .query_pool_swap(&QueryPoolSwapParams::new(
2285 token_x.clone(),
2286 token_y.clone(),
2287 SwapConstraint::PoolTargetPrice {
2288 target: target_price,
2289 tolerance: 0f64,
2290 min_amount_in: None,
2291 max_amount_in: None,
2292 },
2293 ))
2294 .expect("swap_to_price failed");
2295 assert!(*pool_swap.amount_in() > BigUint::ZERO, "Amount in should be positive");
2296
2297 let result = pool
2299 .get_amount_out(pool_swap.amount_in().clone(), &token_x, &token_y)
2300 .expect("get_amount_out failed");
2301
2302 assert!(result.amount > BigUint::ZERO);
2305 assert!(result.amount >= *pool_swap.amount_out());
2306 }
2307
2308 #[test]
2309 fn test_swap_to_price_basic() {
2310 let liquidity = 100_000_000_000_000_000_000u128;
2311 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2312 .expect("Failed to calculate sqrt price");
2313 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2314
2315 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2316
2317 let pool = UniswapV4State::new(
2318 liquidity,
2319 sqrt_price,
2320 UniswapV4Fees {
2321 zero_for_one: 0,
2322 one_for_zero: 0,
2323 lp_fee: 3000, },
2325 tick,
2326 60,
2327 ticks,
2328 )
2329 .expect("Failed to create pool");
2330
2331 let token_x = token_x();
2332 let token_y = token_y();
2333
2334 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2336
2337 let pool_swap = pool
2338 .query_pool_swap(&QueryPoolSwapParams::new(
2339 token_x,
2340 token_y,
2341 SwapConstraint::PoolTargetPrice {
2342 target: target_price,
2343 tolerance: 0f64,
2344 min_amount_in: None,
2345 max_amount_in: None,
2346 },
2347 ))
2348 .expect("swap_to_price failed");
2349
2350 let expected_amount_in = BigUint::from_str("246739021727519745").unwrap();
2352 let expected_amount_out = BigUint::from_str("490291909043340795").unwrap();
2353
2354 assert_eq!(
2355 *pool_swap.amount_in(),
2356 expected_amount_in,
2357 "amount_in should match expected value"
2358 );
2359 assert_eq!(
2360 *pool_swap.amount_out(),
2361 expected_amount_out,
2362 "amount_out should match expected value"
2363 );
2364 }
2365
2366 #[test]
2367 fn test_swap_price_limit_out_of_range_returns_error() {
2368 let pool = create_basic_v4_test_pool();
2369 let amount = -I256::from_raw(U256::from(1000u64)); let result = pool.swap(true, amount, Some(pool.sqrt_price), None);
2373 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2374
2375 let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO), None);
2377 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2378
2379 let result = pool.swap(false, amount, Some(pool.sqrt_price), None);
2381 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2382
2383 let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO), None);
2385 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2386 }
2387
2388 #[test]
2389 fn test_swap_at_extreme_price_returns_error() {
2390 let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
2393 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2394 let aligned_tick = (MIN_TICK / 60) * 60 + 60; let ticks = vec![
2397 TickInfo::new(aligned_tick, 0).unwrap(),
2398 TickInfo::new(aligned_tick + 60, 0).unwrap(),
2399 ];
2400 let pool = UniswapV4State::new(
2401 100_000_000_000_000_000_000u128,
2402 sqrt_price,
2403 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2404 tick,
2405 60,
2406 ticks,
2407 )
2408 .unwrap();
2409
2410 let amount = -I256::from_raw(U256::from(1000u64));
2411 let result = pool.swap(true, amount, None, None);
2413 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2414 }
2415}