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