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, u256_to_f64},
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;
73const HOOK_FEE_PROBE_EXP: u64 = 30;
78
79#[derive(Clone)]
80pub struct UniswapV4State {
81 liquidity: u128,
82 sqrt_price: U256,
83 fees: UniswapV4Fees,
84 tick: i32,
85 ticks: TickList,
86 tick_spacing: i32,
87 pub hook: Option<Box<dyn HookHandler>>,
88}
89
90impl_non_serializable_protocol!(UniswapV4State, "not supported due vm state deps");
91
92impl fmt::Debug for UniswapV4State {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.debug_struct("UniswapV4State")
95 .field("liquidity", &self.liquidity)
96 .field("sqrt_price", &self.sqrt_price)
97 .field("fees", &self.fees)
98 .field("tick", &self.tick)
99 .field("tick_spacing", &self.tick_spacing)
100 .finish_non_exhaustive()
101 }
102}
103
104impl PartialEq for UniswapV4State {
105 fn eq(&self, other: &Self) -> bool {
106 match (&self.hook, &other.hook) {
107 (Some(a), Some(b)) => a.is_equal(&**b),
108 (None, None) => true,
109 _ => false,
110 }
111 }
112}
113
114impl Eq for UniswapV4State {}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct UniswapV4Fees {
118 pub zero_for_one: u32,
120 pub one_for_zero: u32,
122 pub lp_fee: u32,
124}
125
126impl UniswapV4Fees {
127 pub fn new(zero_for_one: u32, one_for_zero: u32, lp_fee: u32) -> Self {
128 Self { zero_for_one, one_for_zero, lp_fee }
129 }
130
131 fn calculate_swap_fees_pips(&self, zero_for_one: bool, lp_fee_override: Option<u32>) -> u32 {
132 let protocol_fee = if zero_for_one { self.zero_for_one } else { self.one_for_zero };
133 let lp_fee = lp_fee_override.unwrap_or_else(|| {
134 if is_dynamic(self.lp_fee) {
136 0
137 } else {
138 self.lp_fee
139 }
140 });
141
142 protocol_fee + lp_fee - ((protocol_fee as u64 * lp_fee as u64 / 1_000_000) as u32)
147 }
148}
149
150impl UniswapV4State {
151 pub fn new(
153 liquidity: u128,
154 sqrt_price: U256,
155 fees: UniswapV4Fees,
156 tick: i32,
157 tick_spacing: i32,
158 ticks: Vec<TickInfo>,
159 ) -> Result<Self, SimulationError> {
160 let tick_spacing_u16 = tick_spacing.try_into().map_err(|_| {
161 SimulationError::FatalError(format!(
164 "tick_spacing {} must be positive (int24 -> u16 conversion failed)",
165 tick_spacing
166 ))
167 })?;
168 let tick_list = TickList::from(tick_spacing_u16, ticks)?;
169 Ok(UniswapV4State {
170 liquidity,
171 sqrt_price,
172 fees,
173 tick,
174 ticks: tick_list,
175 tick_spacing,
176 hook: None,
177 })
178 }
179
180 fn swap(
181 &self,
182 zero_for_one: bool,
183 amount_specified: I256,
184 sqrt_price_limit: Option<U256>,
185 lp_fee_override: Option<u32>,
186 ) -> Result<SwapResults, SimulationError> {
187 if amount_specified == I256::ZERO {
188 return Ok(SwapResults {
189 amount_calculated: I256::ZERO,
190 amount_specified: I256::ZERO,
191 amount_remaining: I256::ZERO,
192 sqrt_price: self.sqrt_price,
193 liquidity: self.liquidity,
194 tick: self.tick,
195 gas_used: U256::from(3_000), });
197 }
198
199 if self.liquidity == 0 {
200 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
201 }
202 let price_limit = if let Some(limit) = sqrt_price_limit {
203 limit
204 } else if zero_for_one {
205 safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
206 } else {
207 safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
208 };
209
210 let price_limit_valid = if zero_for_one {
211 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
212 } else {
213 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
214 };
215 if !price_limit_valid {
216 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
217 }
218
219 let exact_input = amount_specified < I256::ZERO;
220
221 let mut state = SwapState {
222 amount_remaining: amount_specified,
223 amount_calculated: I256::ZERO,
224 sqrt_price: self.sqrt_price,
225 tick: self.tick,
226 liquidity: self.liquidity,
227 };
228 let mut gas_used = U256::from(SWAP_BASE_GAS);
229
230 while state.amount_remaining != I256::ZERO && state.sqrt_price != price_limit {
231 let (mut next_tick, initialized) = match self
232 .ticks
233 .next_initialized_tick_within_one_word(state.tick, zero_for_one)
234 {
235 Ok((tick, init)) => {
236 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_BITMAP_LOOKUP))?;
237 (tick, init)
238 }
239 Err(tick_err) => match tick_err.kind {
240 TickListErrorKind::TicksExeeded => {
241 let mut new_state = self.clone();
242 new_state.liquidity = state.liquidity;
243 new_state.tick = state.tick;
244 new_state.sqrt_price = state.sqrt_price;
245 return Err(SimulationError::InvalidInput(
246 "Ticks exceeded".into(),
247 Some(GetAmountOutResult::new(
248 u256_to_biguint(state.amount_calculated.abs().into_raw()),
249 u256_to_biguint(gas_used),
250 Box::new(new_state),
251 )),
252 ));
253 }
254 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
255 },
256 };
257
258 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
259
260 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
261 let fee_pips = self
262 .fees
263 .calculate_swap_fees_pips(zero_for_one, lp_fee_override);
264
265 let sqrt_price_start = state.sqrt_price;
266 let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
267 state.sqrt_price,
268 UniswapV4State::get_sqrt_ratio_target(sqrt_price_next, price_limit, zero_for_one),
269 state.liquidity,
270 -state.amount_remaining,
274 fee_pips,
275 )?;
276 state.sqrt_price = sqrt_price;
277
278 let step = StepComputation {
279 sqrt_price_start,
280 tick_next: next_tick,
281 initialized,
282 sqrt_price_next,
283 amount_in,
284 amount_out,
285 fee_amount,
286 };
287 if exact_input {
288 state.amount_remaining += I256::checked_from_sign_and_abs(
289 Sign::Positive,
290 safe_add_u256(step.amount_in, step.fee_amount)?,
291 )
292 .unwrap();
293 state.amount_calculated -=
294 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
295 } else {
296 state.amount_remaining -=
297 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
298 state.amount_calculated += I256::checked_from_sign_and_abs(
299 Sign::Positive,
300 safe_add_u256(step.amount_in, step.fee_amount)?,
301 )
302 .unwrap();
303 }
304 if state.sqrt_price == step.sqrt_price_next {
305 if step.initialized {
306 let liquidity_raw = self
307 .ticks
308 .get_tick(step.tick_next)
309 .unwrap()
310 .net_liquidity;
311 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
312 state.liquidity =
313 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
314 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_TICK))?;
315 }
316 state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
317 } else if state.sqrt_price != step.sqrt_price_start {
318 state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
319 }
320 }
321
322 Ok(SwapResults {
323 amount_calculated: state.amount_calculated,
324 amount_specified,
325 amount_remaining: state.amount_remaining,
326 sqrt_price: state.sqrt_price,
327 liquidity: state.liquidity,
328 tick: state.tick,
329 gas_used: safe_add_u256(gas_used, U256::from(V4_CALLBACK_SETTLEMENT_GAS))?,
330 })
331 }
332
333 pub fn set_hook_handler(&mut self, handler: Box<dyn HookHandler>) {
334 self.hook = Some(handler);
335 }
336
337 fn get_sqrt_ratio_target(
338 sqrt_price_next: U256,
339 sqrt_price_limit: U256,
340 zero_for_one: bool,
341 ) -> U256 {
342 let cond1 = if zero_for_one {
343 sqrt_price_next < sqrt_price_limit
344 } else {
345 sqrt_price_next > sqrt_price_limit
346 };
347
348 if cond1 {
349 sqrt_price_limit
350 } else {
351 sqrt_price_next
352 }
353 }
354
355 fn find_limits_experimentally(
356 &self,
357 token_in: Bytes,
358 token_out: Bytes,
359 ) -> Result<(BigUint, BigUint), SimulationError> {
360 let token_in_obj =
363 Token::new(&token_in, "TOKEN_IN", 18, 0, &[Some(10_000)], Default::default(), 100);
364 let token_out_obj =
365 Token::new(&token_out, "TOKEN_OUT", 18, 0, &[Some(10_000)], Default::default(), 100);
366
367 self.find_max_amount(&token_in_obj, &token_out_obj)
368 }
369
370 fn find_max_amount(
383 &self,
384 token_in: &Token,
385 token_out: &Token,
386 ) -> Result<(BigUint, BigUint), SimulationError> {
387 let mut low = BigUint::from(1u64);
388
389 let mut high = BigUint::from(10u64).pow(18); let mut last_successful_amount_in = BigUint::from(1u64);
393 let mut last_successful_amount_out = BigUint::from(0u64);
394
395 while let Ok(result) = self.get_amount_out(high.clone(), token_in, token_out) {
398 low = last_successful_amount_in.clone();
401 last_successful_amount_in = high.clone();
402 last_successful_amount_out = result.amount;
403 high *= BigUint::from(10u64);
404
405 if high > BigUint::from(10u64).pow(75) {
407 return Ok((last_successful_amount_in, last_successful_amount_out));
408 }
409 }
410
411 while &high - &low > BigUint::from(1u64) {
413 let mid = (&low + &high) / BigUint::from(2u64);
414
415 match self.get_amount_out(mid.clone(), token_in, token_out) {
416 Ok(result) => {
417 last_successful_amount_in = mid.clone();
418 last_successful_amount_out = result.amount;
419 low = mid;
420 }
421 Err(_) => {
422 high = mid;
423 }
424 }
425 }
426
427 Ok((last_successful_amount_in, last_successful_amount_out))
428 }
429
430 fn has_no_initialized_ticks(&self) -> bool {
432 !self.ticks.has_initialized_ticks()
433 }
434
435 fn core_spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
438 let base_is_currency0 = base < quote;
439 let fee_pips = self
440 .fees
441 .calculate_swap_fees_pips(base_is_currency0, None);
442 let fee = fee_pips as f64 / 1_000_000.0;
443
444 let price = if base_is_currency0 {
445 sqrt_price_q96_to_f64(self.sqrt_price, base.decimals, quote.decimals)?
446 } else {
447 1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, quote.decimals, base.decimals)?
448 };
449
450 Ok(add_fee_markup(price, fee))
451 }
452
453 fn hook_fee_rate(&self, zero_for_one: bool) -> Result<Option<f64>, SimulationError> {
460 let Some(hook) = &self.hook else { return Ok(None) };
461
462 let probe = U256::from(10u64).pow(U256::from(HOOK_FEE_PROBE_EXP));
463 let Some(fee) = hook.unspecified_fee_amount(probe, zero_for_one)? else {
464 return Ok(None);
465 };
466
467 let rate = u256_to_f64(fee)? / u256_to_f64(probe)?;
468 if rate >= 1.0 {
469 return Err(SimulationError::FatalError(format!(
470 "Hook {} keeps {rate} of the output, leaving no price to quote",
471 hook.address()
472 )));
473 }
474
475 Ok(Some(rate))
476 }
477}
478
479#[typetag::serde]
480impl ProtocolSim for UniswapV4State {
481 fn fee(&self) -> f64 {
484 todo!()
485 }
486
487 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
488 if let Some(hook) = &self.hook {
489 if let Some(rate) = self.hook_fee_rate(quote < base)? {
494 return Ok(add_fee_markup(self.core_spot_price(base, quote)?, rate));
495 }
496
497 match hook.spot_price(base, quote) {
498 Ok(price) => return Ok(price),
499 Err(SimulationError::RecoverableError(_)) => {
500 let x1 = BigUint::from(10u64).pow(base.decimals) / BigUint::from(100u64); let x2 = &x1 + (&x1 / BigUint::from(100u64));
508
509 let y1 = self.get_amount_out(x1.clone(), base, quote)?;
511 let y2 = self.get_amount_out(x2.clone(), base, quote)?;
512
513 let num = y2
515 .amount
516 .checked_sub(&y1.amount)
517 .ok_or_else(|| {
518 SimulationError::FatalError(
519 "Cannot calculate spot price: y2 < y1".to_string(),
520 )
521 })?;
522 let den = x2.checked_sub(&x1).ok_or_else(|| {
523 SimulationError::FatalError(
524 "Cannot calculate spot price: x2 < x1".to_string(),
525 )
526 })?;
527
528 if den == BigUint::from(0u64) {
529 return Err(SimulationError::FatalError(
530 "Cannot calculate spot price: denominator is zero".to_string(),
531 ));
532 }
533
534 let num_f64 = num.to_f64().ok_or_else(|| {
536 SimulationError::FatalError(
537 "Failed to convert numerator to f64".to_string(),
538 )
539 })?;
540 let den_f64 = den.to_f64().ok_or_else(|| {
541 SimulationError::FatalError(
542 "Failed to convert denominator to f64".to_string(),
543 )
544 })?;
545
546 let token_correction = 10f64.powi(base.decimals as i32 - quote.decimals as i32);
547
548 return Ok(num_f64 / den_f64 * token_correction);
549 }
550 Err(e) => return Err(e),
551 }
552 }
553
554 self.core_spot_price(base, quote)
555 }
556
557 fn get_amount_out(
558 &self,
559 amount_in: BigUint,
560 token_in: &Token,
561 token_out: &Token,
562 ) -> Result<GetAmountOutResult, SimulationError> {
563 let zero_for_one = token_in < token_out;
564 let amount_specified = I256::checked_from_sign_and_abs(
565 Sign::Negative,
566 U256::from_be_slice(&amount_in.to_bytes_be()),
567 )
568 .ok_or_else(|| {
569 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
570 })?;
571
572 let mut amount_to_swap = amount_specified;
573 let mut lp_fee_override: Option<u32> = None;
574 let mut before_swap_gas = 0u64;
575 let mut after_swap_gas = 0u64;
576 let mut before_swap_delta = BeforeSwapDelta(I256::ZERO);
577 let mut storage_overwrites = None;
578
579 let token_in_address = Address::from_slice(&token_in.address);
580 let token_out_address = Address::from_slice(&token_out.address);
581
582 let state_context = StateContext {
583 currency_0: if zero_for_one { token_in_address } else { token_out_address },
584 currency_1: if zero_for_one { token_out_address } else { token_in_address },
585 fees: self.fees.clone(),
586 tick_spacing: self.tick_spacing,
587 };
588
589 let swap_params = SwapParams {
590 zero_for_one,
591 amount_specified: amount_to_swap,
592 sqrt_price_limit: self.sqrt_price,
593 };
594
595 if let Some(ref hook) = self.hook {
597 if has_permission(hook.address(), HookOptions::BeforeSwap) {
598 let before_swap_params = BeforeSwapParameters {
599 context: state_context.clone(),
600 sender: *EXTERNAL_ACCOUNT,
601 swap_params: swap_params.clone(),
602 hook_data: Bytes::new(),
603 };
604
605 let before_swap_result = hook
606 .before_swap(before_swap_params, None, None)
607 .map_err(|e| {
608 SimulationError::FatalError(format!(
609 "BeforeSwap hook simulation failed: {e:?}"
610 ))
611 })?;
612
613 before_swap_gas = before_swap_result.gas_estimate;
614 before_swap_delta = before_swap_result.result.amount_delta;
615 storage_overwrites = Some(before_swap_result.result.overwrites);
616
617 if before_swap_delta.as_i256() != I256::ZERO {
620 amount_to_swap += I256::from(before_swap_delta.get_specified_delta());
621 if amount_to_swap > I256::ZERO {
622 return Err(SimulationError::FatalError(
623 "Hook delta exceeds swap amount".into(),
624 ));
625 }
626 }
627
628 let hook_fee = before_swap_result
633 .result
634 .fee
635 .to::<u32>();
636 if hook_fee != 0 {
637 let cleaned_fee = lp_fee::remove_override_flag(hook_fee);
639
640 if !lp_fee::is_valid(cleaned_fee) {
642 return Err(SimulationError::FatalError(format!(
643 "LP fee override {} exceeds maximum {} pips",
644 cleaned_fee,
645 lp_fee::MAX_LP_FEE
646 )));
647 }
648
649 lp_fee_override = Some(cleaned_fee);
650 }
651 }
652 }
653
654 let result = self.swap(zero_for_one, amount_to_swap, None, lp_fee_override)?;
656
657 let mut swap_delta = BalanceDelta::from_swap_result(result.amount_calculated, zero_for_one);
659
660 let hook_delta_specified = before_swap_delta.get_specified_delta();
663 let mut hook_delta_unspecified = before_swap_delta.get_unspecified_delta();
664
665 if let Some(ref hook) = self.hook {
666 if has_permission(hook.address(), HookOptions::AfterSwap) {
667 let after_swap_params = AfterSwapParameters {
668 context: state_context,
669 sender: *EXTERNAL_ACCOUNT,
670 swap_params,
671 delta: swap_delta,
672 hook_data: Bytes::new(),
673 };
674
675 let after_swap_result = hook
676 .after_swap(after_swap_params, storage_overwrites, None)
677 .map_err(|e| {
678 SimulationError::FatalError(format!(
679 "AfterSwap hook simulation failed: {e:?}"
680 ))
681 })?;
682 after_swap_gas = after_swap_result.gas_estimate;
683 if has_permission(hook.address(), HookOptions::AfterSwapReturnsDelta) {
688 hook_delta_unspecified += after_swap_result.result;
689 }
690 }
691 }
692
693 if (hook_delta_specified != I128::ZERO) || (hook_delta_unspecified != I128::ZERO) {
696 let hook_delta = if (amount_specified < I256::ZERO) == zero_for_one {
697 BalanceDelta::new(hook_delta_specified, hook_delta_unspecified)
698 } else {
699 BalanceDelta::new(hook_delta_unspecified, hook_delta_specified)
700 };
701 swap_delta = swap_delta - hook_delta
703 }
704
705 let amount_out = if (amount_specified < I256::ZERO) == zero_for_one {
706 swap_delta.amount1()
707 } else {
708 swap_delta.amount0()
709 };
710
711 trace!(?amount_in, ?token_in, ?token_out, ?zero_for_one, ?result, "V4 SWAP");
712 let mut new_state = self.clone();
713 new_state.liquidity = result.liquidity;
714 new_state.tick = result.tick;
715 new_state.sqrt_price = result.sqrt_price;
716
717 let mut hook_overhead = before_swap_gas + after_swap_gas;
723 if before_swap_gas > 0 {
724 hook_overhead += PM_PER_HOOK_CALL_OVERHEAD;
725 }
726 if after_swap_gas > 0 {
727 hook_overhead += PM_PER_HOOK_CALL_OVERHEAD;
728 }
729 let total_gas_used = result.gas_used + U256::from(hook_overhead);
730 Ok(GetAmountOutResult::new(
731 u256_to_biguint(U256::from(amount_out.abs())),
732 u256_to_biguint(total_gas_used),
733 Box::new(new_state),
734 ))
735 }
736
737 fn get_limits(
738 &self,
739 token_in: Bytes,
740 token_out: Bytes,
741 ) -> Result<(BigUint, BigUint), SimulationError> {
742 if let Some(hook) = &self.hook {
743 if self.liquidity == 0 && self.has_no_initialized_ticks() {
745 match hook.get_amount_ranges(token_in.clone(), token_out.clone()) {
747 Ok(amount_ranges) => {
748 return Ok((
749 u256_to_biguint(amount_ranges.amount_in_range.1),
750 u256_to_biguint(amount_ranges.amount_out_range.1),
751 ))
752 }
753 Err(SimulationError::RecoverableError(msg))
756 if msg.contains("not implemented") || msg.contains("not set") =>
757 {
758 return self.find_limits_experimentally(token_in, token_out);
761 }
763 Err(e) => return Err(e),
764 }
765 }
766 }
767
768 if self.liquidity == 0 {
770 return Ok((BigUint::zero(), BigUint::zero()));
771 }
772
773 let zero_for_one = token_in < token_out;
774 let mut current_tick = self.tick;
775 let mut current_sqrt_price = self.sqrt_price;
776 let mut current_liquidity = self.liquidity;
777 let mut total_amount_in = U256::ZERO;
778 let mut total_amount_out = U256::ZERO;
779 let mut ticks_crossed: u64 = 0;
780
781 while let Ok((tick, initialized)) = self
784 .ticks
785 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
786 {
787 if ticks_crossed >= MAX_TICKS_CROSSED {
789 break;
790 }
791 ticks_crossed += 1;
792
793 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
795
796 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
798
799 let (amount_in, amount_out) = if zero_for_one {
802 let amount0 = get_amount0_delta(
803 sqrt_price_next,
804 current_sqrt_price,
805 current_liquidity,
806 true,
807 )?;
808 let amount1 = get_amount1_delta(
809 sqrt_price_next,
810 current_sqrt_price,
811 current_liquidity,
812 false,
813 )?;
814 (amount0, amount1)
815 } else {
816 let amount0 = get_amount0_delta(
817 sqrt_price_next,
818 current_sqrt_price,
819 current_liquidity,
820 false,
821 )?;
822 let amount1 = get_amount1_delta(
823 sqrt_price_next,
824 current_sqrt_price,
825 current_liquidity,
826 true,
827 )?;
828 (amount1, amount0)
829 };
830
831 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
833 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
834
835 if initialized {
840 let liquidity_raw = self
841 .ticks
842 .get_tick(next_tick)
843 .unwrap()
844 .net_liquidity;
845 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
846
847 match liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta) {
850 Ok(new_liquidity) => {
851 current_liquidity = new_liquidity;
852 }
853 Err(_) => {
854 break;
857 }
858 }
859 }
860
861 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
863 current_sqrt_price = sqrt_price_next;
864
865 if current_liquidity == 0 {
867 break;
868 }
869 }
870
871 if let Some(hook) = &self.hook {
874 if let Some(fee) = hook.unspecified_fee_amount(total_amount_out, zero_for_one)? {
875 total_amount_out = safe_sub_u256(total_amount_out, fee)?;
876 }
877 }
878
879 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
880 }
881
882 fn delta_transition(
883 &mut self,
884 delta: ProtocolStateDelta,
885 tokens: &HashMap<Bytes, Token>,
886 balances: &Balances,
887 ) -> Result<(), TransitionError> {
888 if let Some(mut hook) = self.hook.clone() {
889 match hook.delta_transition(delta.clone(), tokens, balances) {
890 Ok(()) => self.set_hook_handler(hook),
891 Err(TransitionError::SimulationError(SimulationError::RecoverableError(msg)))
892 if msg.contains("not implemented") =>
893 {
894 }
896 Err(e) => return Err(e),
897 }
898 }
899
900 if let Some(liquidity) = delta
902 .updated_attributes
903 .get("liquidity")
904 {
905 self.liquidity = u128::from(liquidity.clone());
906 }
907 if let Some(sqrt_price) = delta
908 .updated_attributes
909 .get("sqrt_price_x96")
910 {
911 self.sqrt_price = U256::from_be_slice(sqrt_price);
912 }
913 if let Some(tick) = delta.updated_attributes.get("tick") {
914 self.tick = i24_be_bytes_to_i32(tick);
915 }
916 if let Some(lp_fee) = delta.updated_attributes.get("fee") {
917 self.fees.lp_fee = u32::from(lp_fee.clone());
918 }
919 if let Some(zero2one_protocol_fee) = delta
920 .updated_attributes
921 .get("protocol_fees/zero2one")
922 {
923 self.fees.zero_for_one = u32::from(zero2one_protocol_fee.clone());
924 }
925 if let Some(one2zero_protocol_fee) = delta
926 .updated_attributes
927 .get("protocol_fees/one2zero")
928 {
929 self.fees.one_for_zero = u32::from(one2zero_protocol_fee.clone());
930 }
931
932 for (key, value) in delta.updated_attributes.iter() {
934 if key.starts_with("ticks/") {
936 let parts: Vec<&str> = key.split('/').collect();
937 self.ticks
938 .set_tick_liquidity(
939 parts[1]
940 .parse::<i32>()
941 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
942 i128::from(value.clone()),
943 )
944 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
945 }
946 }
947 for key in delta.deleted_attributes.iter() {
949 if key.starts_with("ticks/") {
951 let parts: Vec<&str> = key.split('/').collect();
952 self.ticks
953 .set_tick_liquidity(
954 parts[1]
955 .parse::<i32>()
956 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
957 0,
958 )
959 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
960 }
961 }
962
963 Ok(())
964 }
965
966 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
975 if self.liquidity == 0 {
976 return Err(SimulationError::FatalError("No liquidity".to_string()));
977 }
978
979 let zero_for_one = params.token_in().address < params.token_out().address;
981 let fee_pips = self
982 .fees
983 .calculate_swap_fees_pips(zero_for_one, None);
984
985 match params.swap_constraint() {
986 SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
987 "Uniswap V4 does not support TradeLimitPrice constraint in query_pool_swap"
988 .to_string(),
989 None,
990 )),
991 SwapConstraint::PoolTargetPrice {
992 target,
993 tolerance: _,
994 min_amount_in: _,
995 max_amount_in: _,
996 } => {
997 if self.liquidity == 0 {
998 return Err(SimulationError::FatalError("No liquidity".to_string()));
999 }
1000
1001 let (amount_in, amount_out, swap_result) = clmm_swap_to_price(
1002 self.sqrt_price,
1003 ¶ms.token_in().address,
1004 ¶ms.token_out().address,
1005 target,
1006 fee_pips,
1007 Sign::Negative, |zero_for_one, amount_specified, sqrt_price_limit| {
1009 self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit), None)
1010 },
1011 )?;
1012
1013 let mut new_state = self.clone();
1014 new_state.liquidity = swap_result.liquidity;
1015 new_state.tick = swap_result.tick;
1016 new_state.sqrt_price = swap_result.sqrt_price;
1017
1018 Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
1019 }
1020 }
1021 }
1022
1023 fn clone_box(&self) -> Box<dyn ProtocolSim> {
1024 Box::new(self.clone())
1025 }
1026
1027 fn as_any(&self) -> &dyn Any {
1028 self
1029 }
1030
1031 fn as_any_mut(&mut self) -> &mut dyn Any {
1032 self
1033 }
1034
1035 fn eq(&self, other: &dyn ProtocolSim) -> bool {
1036 if let Some(other_state) = other
1037 .as_any()
1038 .downcast_ref::<UniswapV4State>()
1039 {
1040 self.liquidity == other_state.liquidity &&
1041 self.sqrt_price == other_state.sqrt_price &&
1042 self.fees == other_state.fees &&
1043 self.tick == other_state.tick &&
1044 self.ticks == other_state.ticks
1045 } else {
1046 false
1047 }
1048 }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053 use std::{collections::HashSet, fs, path::Path, str::FromStr};
1054
1055 use alloy::primitives::{aliases::U24, U160};
1056 use num_traits::FromPrimitive;
1057 use rstest::rstest;
1058 use serde_json::Value;
1059 use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
1060 use tycho_common::{models::Chain, simulation::protocol_sim::Price};
1061
1062 use super::*;
1063 use crate::{
1064 evm::{
1065 engine_db::{
1066 create_engine,
1067 simulation_db::SimulationDB,
1068 utils::{get_client, get_runtime},
1069 },
1070 protocol::{
1071 u256_num::biguint_to_u256,
1072 uniswap_v4::hooks::{
1073 angstrom::hook_handler::{AngstromFees, AngstromHookHandler},
1074 generic_vm_hook_handler::GenericVMHookHandler,
1075 models::{AfterSwapDelta, AmountRanges, BeforeSwapOutput, WithGasEstimate},
1076 pons_v2::hook_handler::{PonsV2HookHandler, PONS_V2_HOOK_ROBINHOOD},
1077 },
1078 utils::uniswap::{lp_fee, sqrt_price_math::get_sqrt_price_q96},
1079 },
1080 },
1081 protocol::models::{DecoderContext, TryFromWithBlock},
1082 };
1083
1084 fn usdc() -> Token {
1086 Token::new(
1087 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1088 "USDC",
1089 6,
1090 0,
1091 &[Some(10_000)],
1092 Default::default(),
1093 100,
1094 )
1095 }
1096
1097 fn weth() -> Token {
1098 Token::new(
1099 &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1100 "WETH",
1101 18,
1102 0,
1103 &[Some(10_000)],
1104 Default::default(),
1105 100,
1106 )
1107 }
1108
1109 fn eth() -> Token {
1110 Token::new(
1111 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
1112 "ETH",
1113 18,
1114 0,
1115 &[Some(10_000)],
1116 Default::default(),
1117 100,
1118 )
1119 }
1120
1121 fn token_x() -> Token {
1122 Token::new(
1123 &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
1124 "X",
1125 18,
1126 0,
1127 &[Some(10_000)],
1128 Default::default(),
1129 100,
1130 )
1131 }
1132
1133 fn token_y() -> Token {
1134 Token::new(
1135 &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
1136 "Y",
1137 18,
1138 0,
1139 &[Some(10_000)],
1140 Default::default(),
1141 100,
1142 )
1143 }
1144
1145 #[test]
1146 fn test_delta_transition() {
1147 let mut pool = UniswapV4State::new(
1148 1000,
1149 U256::from_str("1000").unwrap(),
1150 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 700 },
1151 100,
1152 60,
1153 vec![TickInfo::new(120, 10000).unwrap(), TickInfo::new(180, -10000).unwrap()],
1154 )
1155 .unwrap();
1156
1157 let attributes: HashMap<String, Bytes> = [
1158 ("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1159 ("sqrt_price_x96".to_string(), Bytes::from(1001_u64.to_be_bytes().to_vec())),
1160 ("tick".to_string(), Bytes::from(120_i32.to_be_bytes().to_vec())),
1161 ("protocol_fees/zero2one".to_string(), Bytes::from(50_u32.to_be_bytes().to_vec())),
1162 ("protocol_fees/one2zero".to_string(), Bytes::from(75_u32.to_be_bytes().to_vec())),
1163 ("fee".to_string(), Bytes::from(100_u32.to_be_bytes().to_vec())),
1164 ("ticks/-120/net_liquidity".to_string(), Bytes::from(10200_u64.to_be_bytes().to_vec())),
1165 ("ticks/120/net_liquidity".to_string(), Bytes::from(9800_u64.to_be_bytes().to_vec())),
1166 ("block_number".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1167 ("block_timestamp".to_string(), Bytes::from(1758201935_u64.to_be_bytes().to_vec())),
1168 ]
1169 .into_iter()
1170 .collect();
1171
1172 let delta = ProtocolStateDelta {
1173 component_id: "State1".to_owned(),
1174 updated_attributes: attributes,
1175 deleted_attributes: HashSet::new(),
1176 };
1177
1178 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1179 .unwrap();
1180
1181 assert_eq!(pool.liquidity, 2000);
1182 assert_eq!(pool.sqrt_price, U256::from(1001));
1183 assert_eq!(pool.tick, 120);
1184 assert_eq!(pool.fees.zero_for_one, 50);
1185 assert_eq!(pool.fees.one_for_zero, 75);
1186 assert_eq!(pool.fees.lp_fee, 100);
1187 assert_eq!(
1188 pool.ticks
1189 .get_tick(-120)
1190 .unwrap()
1191 .net_liquidity,
1192 10200
1193 );
1194 assert_eq!(
1195 pool.ticks
1196 .get_tick(120)
1197 .unwrap()
1198 .net_liquidity,
1199 9800
1200 );
1201 }
1202
1203 #[tokio::test]
1204 async fn test_swap_sim() {
1206 use tycho_client::feed::dto;
1207 let project_root = env!("CARGO_MANIFEST_DIR");
1208 let asset_path = Path::new(project_root)
1209 .join("tests/assets/decoder/uniswap_v4_snapshot_sepolia_block_7239119.json");
1210 let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1211 let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1212 let state: ComponentWithState = serde_json::from_value::<dto::ComponentWithState>(data)
1213 .expect("Expected json to match ComponentWithState structure")
1214 .into();
1215
1216 let block = BlockHeader {
1217 number: 7239119,
1218 hash: Bytes::from_str(
1219 "0x28d41d40f2ac275a4f5f621a636b9016b527d11d37d610a45ac3a821346ebf8c",
1220 )
1221 .expect("Invalid block hash"),
1222 parent_hash: Bytes::from(vec![0; 32]),
1223 ..Default::default()
1224 };
1225
1226 let t0 = Token::new(
1227 &Bytes::from_str("0x647e32181a64f4ffd4f0b0b4b052ec05b277729c").unwrap(),
1228 "T0",
1229 18,
1230 0,
1231 &[Some(10_000)],
1232 Chain::Ethereum,
1233 100,
1234 );
1235 let t1 = Token::new(
1236 &Bytes::from_str("0xe390a1c311b26f14ed0d55d3b0261c2320d15ca5").unwrap(),
1237 "T1",
1238 18,
1239 0,
1240 &[Some(10_000)],
1241 Chain::Ethereum,
1242 100,
1243 );
1244
1245 let all_tokens = [t0.clone(), t1.clone()]
1246 .iter()
1247 .map(|t| (t.address.clone(), t.clone()))
1248 .collect();
1249
1250 let usv4_state = UniswapV4State::try_from_with_header(
1251 state,
1252 block,
1253 &Default::default(),
1254 &all_tokens,
1255 &DecoderContext::new(),
1256 )
1257 .await
1258 .unwrap();
1259
1260 assert!(usv4_state.hook.is_none());
1263
1264 let res = usv4_state
1265 .get_amount_out(BigUint::from_u64(1000000000000000000).unwrap(), &t0, &t1)
1266 .unwrap();
1267
1268 let expected_amount = BigUint::from(9999909699895_u64);
1269 assert_eq!(res.amount, expected_amount);
1270 }
1271
1272 #[tokio::test]
1273 async fn test_get_limits() {
1274 use tycho_client::feed::dto;
1275 let block = BlockHeader {
1276 number: 22689129,
1277 hash: Bytes::from_str(
1278 "0x7763ea30d11aef68da729b65250c09a88ad00458c041064aad8c9a9dbf17adde",
1279 )
1280 .expect("Invalid block hash"),
1281 parent_hash: Bytes::from(vec![0; 32]),
1282 ..Default::default()
1283 };
1284
1285 let project_root = env!("CARGO_MANIFEST_DIR");
1286 let asset_path =
1287 Path::new(project_root).join("tests/assets/decoder/uniswap_v4_snapshot.json");
1288 let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1289 let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1290 let state: ComponentWithState = serde_json::from_value::<dto::ComponentWithState>(data)
1291 .expect("Expected json to match ComponentWithState structure")
1292 .into();
1293
1294 let t0 = Token::new(
1295 &Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
1296 "WBTC",
1297 8,
1298 0,
1299 &[Some(10_000)],
1300 Chain::Ethereum,
1301 100,
1302 );
1303 let t1 = Token::new(
1304 &Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
1305 "USDT",
1306 6,
1307 0,
1308 &[Some(10_000)],
1309 Chain::Ethereum,
1310 100,
1311 );
1312
1313 let all_tokens = [t0.clone(), t1.clone()]
1314 .iter()
1315 .map(|t| (t.address.clone(), t.clone()))
1316 .collect();
1317
1318 let usv4_state = UniswapV4State::try_from_with_header(
1319 state,
1320 block,
1321 &Default::default(),
1322 &all_tokens,
1323 &DecoderContext::new(),
1324 )
1325 .await
1326 .unwrap();
1327
1328 assert!(usv4_state.hook.is_none());
1331
1332 let res = usv4_state
1333 .get_limits(t0.address.clone(), t1.address.clone())
1334 .unwrap();
1335
1336 assert_eq!(&res.0, &BigUint::from_u128(71698353688830259750744466706).unwrap());
1337
1338 let out = usv4_state
1339 .get_amount_out(res.0, &t0, &t1)
1340 .expect("swap for limit in didn't work");
1341
1342 assert_eq!(&res.1, &out.amount);
1343 }
1344 #[test]
1345 fn test_get_amount_out_no_hook() {
1346 let usv4_state = UniswapV4State::new(
1351 541501951282951892,
1352 U256::from_str("5362798333066270795901222").unwrap(), UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 100 },
1354 -192022,
1355 1,
1356 vec![
1358 TickInfo {
1359 index: -887272,
1360 net_liquidity: 460382969070005,
1361 sqrt_price: U256::from(4295128739_u64),
1362 },
1363 TickInfo {
1364 index: -207244,
1365 net_liquidity: 561268407024557,
1366 sqrt_price: U256::from_str("2505291706254206075074035").unwrap(),
1367 },
1368 TickInfo {
1369 index: -196411,
1370 net_liquidity: 825711941800452,
1371 sqrt_price: U256::from_str("4306080513146952705853399").unwrap(),
1372 },
1373 TickInfo {
1374 index: -196257,
1375 net_liquidity: 64844666874010,
1376 sqrt_price: U256::from_str("4339363644587371378270009").unwrap(),
1377 },
1378 TickInfo {
1379 index: -195611,
1380 net_liquidity: 2344045150766798,
1381 sqrt_price: U256::from_str("4481806029599743916020126").unwrap(),
1382 },
1383 TickInfo {
1384 index: -194715,
1385 net_liquidity: 391037380558274654,
1386 sqrt_price: U256::from_str("4687145946111116896040494").unwrap(),
1387 },
1388 TickInfo {
1389 index: -194599,
1390 net_liquidity: 89032603464508,
1391 sqrt_price: U256::from_str("4714409015946702405379370").unwrap(),
1392 },
1393 TickInfo {
1394 index: -194389,
1395 net_liquidity: 66635600426483168,
1396 sqrt_price: U256::from_str("4764168603367683402636621").unwrap(),
1397 },
1398 TickInfo {
1399 index: -194160,
1400 net_liquidity: 6123093436523361,
1401 sqrt_price: U256::from_str("4819029067726467394386780").unwrap(),
1402 },
1403 TickInfo {
1404 index: -194025,
1405 net_liquidity: 79940813798964,
1406 sqrt_price: U256::from_str("4851665907541490407930032").unwrap(),
1407 },
1408 TickInfo {
1409 index: -193922,
1410 net_liquidity: 415630967437234,
1411 sqrt_price: U256::from_str("4876715181040466809166531").unwrap(),
1412 },
1413 TickInfo {
1414 index: -193876,
1415 net_liquidity: 9664144015186047,
1416 sqrt_price: U256::from_str("4887943972687250473582419").unwrap(),
1417 },
1418 TickInfo {
1419 index: -193818,
1420 net_liquidity: 435344726052344,
1421 sqrt_price: U256::from_str("4902138873132735049121973").unwrap(),
1422 },
1423 TickInfo {
1424 index: -193804,
1425 net_liquidity: 221726179374067,
1426 sqrt_price: U256::from_str("4905571399964683340605904").unwrap(),
1427 },
1428 TickInfo {
1429 index: -193719,
1430 net_liquidity: 101340835774487,
1431 sqrt_price: U256::from_str("4926463397882393957462188").unwrap(),
1432 },
1433 TickInfo {
1434 index: -193690,
1435 net_liquidity: 193367475630077,
1436 sqrt_price: U256::from_str("4933611593595025190448924").unwrap(),
1437 },
1438 TickInfo {
1439 index: -193643,
1440 net_liquidity: 357016631583746,
1441 sqrt_price: U256::from_str("4945218633428068823432932").unwrap(),
1442 },
1443 TickInfo {
1444 index: -193520,
1445 net_liquidity: 917243184365178,
1446 sqrt_price: U256::from_str("4975723910367862081017120").unwrap(),
1447 },
1448 TickInfo {
1449 index: -193440,
1450 net_liquidity: 114125890211958292,
1451 sqrt_price: U256::from_str("4995665665861492533686137").unwrap(),
1452 },
1453 TickInfo {
1454 index: -193380,
1455 net_liquidity: -65980729148766579,
1456 sqrt_price: U256::from_str("5010674414300823856025303").unwrap(),
1457 },
1458 TickInfo {
1459 index: -192891,
1460 net_liquidity: 1687883551433195,
1461 sqrt_price: U256::from_str("5134689105039642314202223").unwrap(),
1462 },
1463 TickInfo {
1464 index: -192573,
1465 net_liquidity: 11108903221360975,
1466 sqrt_price: U256::from_str("5216979018647067786855495").unwrap(),
1467 },
1468 TickInfo {
1469 index: -192448,
1470 net_liquidity: 32888457482352,
1471 sqrt_price: U256::from_str("5249685603828944002327927").unwrap(),
1472 },
1473 TickInfo {
1474 index: -191525,
1475 net_liquidity: -221726179374067,
1476 sqrt_price: U256::from_str("5497623359964843320146512").unwrap(),
1477 },
1478 TickInfo {
1479 index: -191447,
1480 net_liquidity: -32888457482352,
1481 sqrt_price: U256::from_str("5519104878745833608097296").unwrap(),
1482 },
1483 TickInfo {
1484 index: -191444,
1485 net_liquidity: -114125890211958292,
1486 sqrt_price: U256::from_str("5519932765173943847315221").unwrap(),
1487 },
1488 TickInfo {
1489 index: -191417,
1490 net_liquidity: -101340835774487,
1491 sqrt_price: U256::from_str("5527389333636021285046380").unwrap(),
1492 },
1493 TickInfo {
1494 index: -191384,
1495 net_liquidity: -9664144015186047,
1496 sqrt_price: U256::from_str("5536516597603056457376182").unwrap(),
1497 },
1498 TickInfo {
1499 index: -191148,
1500 net_liquidity: -561268407024557,
1501 sqrt_price: U256::from_str("5602231161238705865493165").unwrap(),
1502 },
1503 TickInfo {
1504 index: -191147,
1505 net_liquidity: -1687883551433195,
1506 sqrt_price: U256::from_str("5602511265794328966803451").unwrap(),
1507 },
1508 TickInfo {
1509 index: -191091,
1510 net_liquidity: -89032603464508,
1511 sqrt_price: U256::from_str("5618219493196441347292357").unwrap(),
1512 },
1513 TickInfo {
1514 index: -190950,
1515 net_liquidity: -189177935487638,
1516 sqrt_price: U256::from_str("5657965894785859782969011").unwrap(),
1517 },
1518 TickInfo {
1519 index: -190756,
1520 net_liquidity: -6123093436523361,
1521 sqrt_price: U256::from_str("5713112435031881967192022").unwrap(),
1522 },
1523 TickInfo {
1524 index: -190548,
1525 net_liquidity: -193367475630077,
1526 sqrt_price: U256::from_str("5772835841671084402427710").unwrap(),
1527 },
1528 TickInfo {
1529 index: -190430,
1530 net_liquidity: -11108903221360975,
1531 sqrt_price: U256::from_str("5806994534290341208820930").unwrap(),
1532 },
1533 TickInfo {
1534 index: -190195,
1535 net_liquidity: -391583014714302569,
1536 sqrt_price: U256::from_str("5875625707132601785181387").unwrap(),
1537 },
1538 TickInfo {
1539 index: -190043,
1540 net_liquidity: -357016631583746,
1541 sqrt_price: U256::from_str("5920448331650864936739481").unwrap(),
1542 },
1543 TickInfo {
1544 index: -189779,
1545 net_liquidity: -917243184365178,
1546 sqrt_price: U256::from_str("5999112356918485175181346").unwrap(),
1547 },
1548 TickInfo {
1549 index: -189663,
1550 net_liquidity: -2344045150766798,
1551 sqrt_price: U256::from_str("6034006559279282606084981").unwrap(),
1552 },
1553 TickInfo {
1554 index: -189620,
1555 net_liquidity: -435344726052344,
1556 sqrt_price: U256::from_str("6046992979471024289177519").unwrap(),
1557 },
1558 TickInfo {
1559 index: -189409,
1560 net_liquidity: -825711941800452,
1561 sqrt_price: U256::from_str("6111123241285165242130911").unwrap(),
1562 },
1563 TickInfo {
1564 index: -189325,
1565 net_liquidity: -3947182209207,
1566 sqrt_price: U256::from_str("6136842645893819031257990").unwrap(),
1567 },
1568 TickInfo {
1569 index: -189324,
1570 net_liquidity: -415630967437234,
1571 sqrt_price: U256::from_str("6137149480355443943537284").unwrap(),
1572 },
1573 TickInfo {
1574 index: -115136,
1575 net_liquidity: 462452451821,
1576 sqrt_price: U256::from_str("250529060232794967902094762").unwrap(),
1577 },
1578 TickInfo {
1579 index: -92109,
1580 net_liquidity: -462452451821,
1581 sqrt_price: U256::from_str("792242363124136400178523925").unwrap(),
1582 },
1583 TickInfo {
1584 index: 887272,
1585 net_liquidity: -521280453734808,
1586 sqrt_price: U256::from_str("1461446703485210103287273052203988822378723970342")
1587 .unwrap(),
1588 },
1589 ],
1590 )
1591 .unwrap();
1592
1593 let t0 = usdc();
1594 let t1 = eth();
1595
1596 let out = usv4_state
1597 .get_amount_out(BigUint::from_u64(2000000).unwrap(), &t0, &t1)
1598 .unwrap();
1599
1600 assert_eq!(out.amount, BigUint::from_str("436478419853848").unwrap())
1601 }
1602
1603 #[test]
1604 fn test_get_amount_out_euler_hook() {
1605 let block = BlockHeader {
1621 number: 22689128,
1622 hash: Bytes::from_str(
1623 "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
1624 )
1625 .expect("Invalid block hash"),
1626 timestamp: 1749739055,
1627 ..Default::default()
1628 };
1629
1630 let mut usv4_state = UniswapV4State::new(
1633 0,
1634 U256::from_str("4295128740").unwrap(),
1635 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
1636 0,
1637 1,
1638 vec![],
1639 )
1640 .unwrap();
1641
1642 let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
1643 .expect("Invalid hook address");
1644
1645 let db = SimulationDB::new(
1646 get_client(None).expect("Failed to create client"),
1647 get_runtime().expect("Failed to get runtime"),
1648 Some(block.clone()),
1649 );
1650 let engine = create_engine(db, true).expect("Failed to create simulation engine");
1651 let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
1652 .expect("Invalid pool manager address");
1653
1654 let hook_handler = GenericVMHookHandler::new(
1655 hook_address,
1656 engine,
1657 pool_manager,
1658 HashMap::new(),
1659 HashMap::new(),
1660 None,
1661 true, )
1663 .unwrap();
1664
1665 let t0 = usdc();
1666 let t1 = weth();
1667
1668 usv4_state.set_hook_handler(Box::new(hook_handler));
1669 let out = usv4_state
1670 .get_amount_out(BigUint::from_u64(7407000000).unwrap(), &t0, &t1)
1671 .unwrap();
1672
1673 assert_eq!(out.amount, BigUint::from_str("2681115183499232721").unwrap())
1674 }
1675
1676 #[test]
1677 fn test_get_amount_out_angstrom_hook() {
1678 let mut usv4_state = UniswapV4State::new(
1680 66319800403673162,
1682 U256::from_str("1314588940601923011323000261788004").unwrap(),
1683 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 8388608 },
1685 194343,
1686 10,
1687 vec![
1688 TickInfo::new(-887270, 198117767801).unwrap(),
1689 TickInfo::new(191990, 24561988698695).unwrap(),
1690 TickInfo::new(192280, 2839631428751224).unwrap(),
1691 TickInfo::new(193130, 318786492813931).unwrap(),
1692 TickInfo::new(194010, 26209207141081).unwrap(),
1693 TickInfo::new(194210, -26209207141081).unwrap(),
1694 TickInfo::new(194220, 63136622375641511).unwrap(),
1695 TickInfo::new(194420, -63136622375641511).unwrap(),
1696 TickInfo::new(195130, -318786492813931).unwrap(),
1697 TickInfo::new(196330, -2839631428751224).unwrap(),
1698 TickInfo::new(197100, -24561988698695).unwrap(),
1699 TickInfo::new(887270, -198117767801).unwrap(),
1700 ],
1701 )
1702 .unwrap();
1703
1704 let fees = AngstromFees {
1705 unlock: U24::from(338),
1712 protocol_unlock: U24::from(112),
1713 };
1714 let hook_handler = AngstromHookHandler::new(
1715 Address::from_str("0x0000000aa232009084bd71a5797d089aa4edfad4").unwrap(),
1716 Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90").unwrap(),
1717 fees,
1718 false,
1719 );
1720
1721 let t0 = usdc();
1722 let t1 = weth();
1723
1724 usv4_state.set_hook_handler(Box::new(hook_handler));
1725 let out = usv4_state
1726 .get_amount_out(
1727 BigUint::from_u64(
1728 6645198144, )
1730 .unwrap(),
1731 &t0, &t1, )
1734 .unwrap();
1735
1736 assert_eq!(out.amount, BigUint::from_str("1825627051870330472").unwrap())
1737 }
1738
1739 #[derive(Debug, Clone, PartialEq, Eq)]
1741 struct AfterSwapTestHook {
1742 address: Address,
1743 delta: I128,
1744 analytic_fee_bps: Option<u32>,
1747 spot_price_override: Option<u64>,
1750 }
1751
1752 impl HookHandler for AfterSwapTestHook {
1753 fn address(&self) -> Address {
1754 self.address
1755 }
1756
1757 fn before_swap(
1758 &self,
1759 _: BeforeSwapParameters,
1760 _: Option<HashMap<Address, HashMap<U256, U256>>>,
1761 _: Option<HashMap<Address, HashMap<U256, U256>>>,
1762 ) -> Result<WithGasEstimate<BeforeSwapOutput>, SimulationError> {
1763 Err(SimulationError::RecoverableError("not implemented".into()))
1764 }
1765
1766 fn after_swap(
1767 &self,
1768 _: AfterSwapParameters,
1769 _: Option<HashMap<Address, HashMap<U256, U256>>>,
1770 _: Option<HashMap<Address, HashMap<U256, U256>>>,
1771 ) -> Result<WithGasEstimate<AfterSwapDelta>, SimulationError> {
1772 Ok(WithGasEstimate { gas_estimate: AFTER_SWAP_TEST_HOOK_GAS, result: self.delta })
1773 }
1774
1775 fn fee(&self, _: &UniswapV4State, _: SwapParams) -> Result<f64, SimulationError> {
1776 Err(SimulationError::RecoverableError("not implemented".into()))
1777 }
1778
1779 fn spot_price(&self, _: &Token, _: &Token) -> Result<f64, SimulationError> {
1780 match self.spot_price_override {
1781 Some(price) => Ok(price as f64),
1782 None => Err(SimulationError::RecoverableError("not implemented".into())),
1783 }
1784 }
1785
1786 fn unspecified_fee_amount(
1787 &self,
1788 unspecified: U256,
1789 _: bool,
1790 ) -> Result<Option<U256>, SimulationError> {
1791 let Some(bps) = self.analytic_fee_bps else { return Ok(None) };
1792 Ok(Some(unspecified * U256::from(bps) / U256::from(10_000u64)))
1793 }
1794
1795 fn get_amount_ranges(&self, _: Bytes, _: Bytes) -> Result<AmountRanges, SimulationError> {
1796 Err(SimulationError::RecoverableError("not implemented".into()))
1797 }
1798
1799 fn delta_transition(
1800 &mut self,
1801 _: ProtocolStateDelta,
1802 _: &HashMap<Bytes, Token>,
1803 _: &Balances,
1804 ) -> Result<(), TransitionError> {
1805 Ok(())
1806 }
1807
1808 fn clone_box(&self) -> Box<dyn HookHandler> {
1809 Box::new(self.clone())
1810 }
1811
1812 fn as_any(&self) -> &dyn Any {
1813 self
1814 }
1815
1816 fn is_equal(&self, other: &dyn HookHandler) -> bool {
1817 other.as_any().downcast_ref::<Self>() == Some(self)
1818 }
1819 }
1820
1821 const AFTER_SWAP_TEST_HOOK_GAS: u64 = 1_000;
1824 const AFTER_SWAP_TEST_HOOK_DELTA: u64 = 777;
1825
1826 fn construct_hook_address(hook_options: &[HookOptions]) -> Address {
1827 let mut hook_flags = U160::ZERO;
1828 let one = U160::from_limbs([1, 0, 0]);
1829 for hook_option in hook_options {
1830 hook_flags |= one << (*hook_option as u8);
1831 }
1832 Address::from(hook_flags)
1833 }
1834
1835 fn after_swap_test_hook(hook_options: &[HookOptions]) -> Box<dyn HookHandler> {
1836 Box::new(AfterSwapTestHook {
1837 address: construct_hook_address(hook_options),
1838 delta: I128::unchecked_from(AFTER_SWAP_TEST_HOOK_DELTA),
1839 analytic_fee_bps: None,
1840 spot_price_override: None,
1841 })
1842 }
1843
1844 fn analytic_fee_test_hook(analytic_fee_bps: u32) -> Box<dyn HookHandler> {
1846 Box::new(AfterSwapTestHook {
1847 address: construct_hook_address(&[
1848 HookOptions::AfterSwap,
1849 HookOptions::AfterSwapReturnsDelta,
1850 ]),
1851 delta: I128::ZERO,
1852 analytic_fee_bps: Some(analytic_fee_bps),
1853 spot_price_override: None,
1854 })
1855 }
1856
1857 const TEST_HOOK_SPOT_PRICE: u64 = 1_000_000;
1860
1861 fn pricing_test_hook(analytic_fee_bps: Option<u32>) -> Box<dyn HookHandler> {
1864 Box::new(AfterSwapTestHook {
1865 address: construct_hook_address(&[
1866 HookOptions::AfterSwap,
1867 HookOptions::AfterSwapReturnsDelta,
1868 ]),
1869 delta: I128::ZERO,
1870 analytic_fee_bps,
1871 spot_price_override: Some(TEST_HOOK_SPOT_PRICE),
1872 })
1873 }
1874
1875 fn basic_v4_test_pool_tokens(zero_for_one: bool) -> (Token, Token) {
1876 if zero_for_one {
1877 (token_x(), token_y())
1878 } else {
1879 (token_y(), token_x())
1880 }
1881 }
1882
1883 #[rstest]
1886 #[case::zero_for_one(true)]
1887 #[case::one_for_zero(false)]
1888 fn test_after_swap_delta_ignored_without_returns_delta_permission(#[case] zero_for_one: bool) {
1889 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
1890 let amount_in = BigUint::from(1_000_000_000_000_000u64);
1891
1892 let hookless = create_basic_v4_test_pool()
1893 .get_amount_out(amount_in.clone(), &token_in, &token_out)
1894 .expect("hookless swap should succeed");
1895
1896 let mut pool = create_basic_v4_test_pool();
1897 pool.set_hook_handler(after_swap_test_hook(&[HookOptions::AfterSwap]));
1898 let gated = pool
1899 .get_amount_out(amount_in, &token_in, &token_out)
1900 .expect("gated swap should succeed");
1901
1902 assert_eq!(gated.amount, hookless.amount);
1903 assert_eq!(
1904 gated.gas,
1905 hookless.gas + BigUint::from(AFTER_SWAP_TEST_HOOK_GAS + PM_PER_HOOK_CALL_OVERHEAD)
1906 );
1907 }
1908
1909 #[rstest]
1912 #[case::zero_for_one(true)]
1913 #[case::one_for_zero(false)]
1914 fn test_after_swap_delta_applied_with_returns_delta_permission(#[case] zero_for_one: bool) {
1915 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
1916 let amount_in = BigUint::from(1_000_000_000_000_000u64);
1917
1918 let hookless = create_basic_v4_test_pool()
1919 .get_amount_out(amount_in.clone(), &token_in, &token_out)
1920 .expect("hookless swap should succeed");
1921
1922 let mut pool = create_basic_v4_test_pool();
1923 pool.set_hook_handler(after_swap_test_hook(&[
1924 HookOptions::AfterSwap,
1925 HookOptions::AfterSwapReturnsDelta,
1926 ]));
1927 let with_delta = pool
1928 .get_amount_out(amount_in, &token_in, &token_out)
1929 .expect("swap with hook delta should succeed");
1930
1931 assert_eq!(with_delta.amount, &hookless.amount - BigUint::from(AFTER_SWAP_TEST_HOOK_DELTA));
1932 assert_eq!(
1933 with_delta.gas,
1934 hookless.gas + BigUint::from(AFTER_SWAP_TEST_HOOK_GAS + PM_PER_HOOK_CALL_OVERHEAD)
1935 );
1936 }
1937
1938 const FEELESS_POOL_LIQUIDITY: u128 = 100_000_000_000_000_000_000; fn create_feeless_v4_test_pool(liquidity: u128) -> UniswapV4State {
1944 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
1945 .expect("a price of two has a square root");
1946 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("the sqrt price maps to a tick");
1947 let position = FEELESS_POOL_LIQUIDITY as i128;
1948
1949 UniswapV4State::new(
1950 liquidity,
1951 sqrt_price,
1952 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 0 },
1953 tick,
1954 60,
1955 vec![
1956 TickInfo::new(-46_080, position).unwrap(),
1957 TickInfo::new(46_080, -position).unwrap(),
1958 ],
1959 )
1960 .expect("the pool builds")
1961 }
1962
1963 fn pons_test_handler() -> PonsV2HookHandler {
1964 PonsV2HookHandler::new(PONS_V2_HOOK_ROBINHOOD, 100, 100)
1965 }
1966
1967 #[test]
1972 fn hookless_spot_price_is_the_documented_buy_price_with_lp_fee_markup() {
1973 const LP_FEE_PIPS: u32 = 3_000;
1974 const LP_FEE: f64 = LP_FEE_PIPS as f64 / 1_000_000.0;
1975 const LIQUIDITY: u128 = 100_000_000_000_000_000_000; const TOLERANCE: f64 = 1e-12;
1977
1978 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
1979 .expect("a price of two has a square root");
1980 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("the sqrt price maps to a tick");
1981 let pool = UniswapV4State::new(
1982 LIQUIDITY,
1983 sqrt_price,
1984 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: LP_FEE_PIPS },
1985 tick,
1986 60,
1987 vec![
1988 TickInfo::new(-46_080, LIQUIDITY as i128).unwrap(),
1989 TickInfo::new(46_080, -(LIQUIDITY as i128)).unwrap(),
1990 ],
1991 )
1992 .expect("the pool builds");
1993
1994 assert!(pool.hook.is_none(), "the pinned values only hold with no hook handler");
1995
1996 let (t0, t1) = (token_x(), token_y());
1997 let pre_fee = sqrt_price_q96_to_f64(sqrt_price, t0.decimals, t1.decimals)
1998 .expect("the sqrt price converts to a price");
1999
2000 let expected_buy_t0 = pre_fee / (1.0 - LP_FEE);
2001 let expected_buy_t1 = (1.0 / pre_fee) / (1.0 - LP_FEE);
2002
2003 let buy_t0 = pool
2004 .spot_price(&t0, &t1)
2005 .expect("a hookless pool always prices");
2006 let buy_t1 = pool
2007 .spot_price(&t1, &t0)
2008 .expect("a hookless pool always prices");
2009
2010 assert!(
2011 (buy_t0 / expected_buy_t0 - 1.0).abs() < TOLERANCE,
2012 "buying t0 quoted {buy_t0}, expected {expected_buy_t0}"
2013 );
2014 assert!(
2015 (buy_t1 / expected_buy_t1 - 1.0).abs() < TOLERANCE,
2016 "buying t1 quoted {buy_t1}, expected {expected_buy_t1}"
2017 );
2018 }
2019
2020 #[rstest]
2024 #[case::base_is_currency0(true)]
2025 #[case::base_is_currency1(false)]
2026 fn test_spot_price_marks_up_an_analytic_hook_fee(#[case] base_is_currency0: bool) {
2027 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2028 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2029 hooked.set_hook_handler(Box::new(pons_test_handler()));
2030
2031 let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2032 .spot_price(&base, "e)
2033 .expect("a hookless pool always prices");
2034 let price = hooked
2035 .spot_price(&base, "e)
2036 .expect("the hook prices its own fee");
2037
2038 assert!(price > core, "hooked {price} is not above hookless {core}");
2039 let ratio = price / core;
2040 assert!((ratio * 0.98 - 1.0).abs() < 1e-9, "hooked/hookless is {ratio}, not 1/0.98");
2041 }
2042
2043 #[rstest]
2046 #[case::base_is_currency0(true)]
2047 #[case::base_is_currency1(false)]
2048 fn test_hooked_spot_price_matches_a_small_hooked_buy(#[case] base_is_currency0: bool) {
2049 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2050 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2051 hooked.set_hook_handler(Box::new(pons_test_handler()));
2052
2053 let quote_in = BigUint::from(100_000_000_000_000u64);
2054 let base_out = hooked
2055 .get_amount_out(quote_in.clone(), "e, &base)
2056 .expect("a tiny buy always fits the pool")
2057 .amount;
2058
2059 let executed = quote_in.to_f64().unwrap() / base_out.to_f64().unwrap();
2060 let spot = hooked
2061 .spot_price(&base, "e)
2062 .expect("the hook prices its own fee");
2063
2064 assert!((executed / spot - 1.0).abs() < 1e-4, "executed {executed}, quoted {spot}");
2065 }
2066
2067 #[rstest]
2071 #[case::base_is_currency0(true)]
2072 #[case::base_is_currency1(false)]
2073 fn test_spot_price_prefers_an_analytic_fee_over_the_hooks_own_price(
2074 #[case] base_is_currency0: bool,
2075 ) {
2076 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2077 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2078 hooked.set_hook_handler(pricing_test_hook(Some(200)));
2079
2080 let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2081 .spot_price(&base, "e)
2082 .expect("a hookless pool always prices");
2083 let price = hooked
2084 .spot_price(&base, "e)
2085 .expect("the hook prices its own fee");
2086
2087 assert_ne!(
2088 price, TEST_HOOK_SPOT_PRICE as f64,
2089 "the pool asked the hook for a price instead of marking up its own"
2090 );
2091 let ratio = price / core;
2092 assert!((ratio * 0.98 - 1.0).abs() < 1e-9, "hooked/hookless is {ratio}, not 1/0.98");
2093 }
2094
2095 #[rstest]
2099 #[case::base_is_currency0(true)]
2100 #[case::base_is_currency1(false)]
2101 fn test_spot_price_uses_the_hooks_own_price_without_an_analytic_fee(
2102 #[case] base_is_currency0: bool,
2103 ) {
2104 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2105 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2106 hooked.set_hook_handler(pricing_test_hook(None));
2107
2108 let price = hooked
2109 .spot_price(&base, "e)
2110 .expect("the hook answers with a price");
2111
2112 assert_eq!(price, TEST_HOOK_SPOT_PRICE as f64);
2113 }
2114
2115 #[rstest]
2118 #[case::base_is_currency0(true)]
2119 #[case::base_is_currency1(false)]
2120 fn test_spot_price_falls_back_to_finite_difference_without_an_analytic_fee(
2121 #[case] base_is_currency0: bool,
2122 ) {
2123 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2124 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2125 hooked.set_hook_handler(after_swap_test_hook(&[
2126 HookOptions::AfterSwap,
2127 HookOptions::AfterSwapReturnsDelta,
2128 ]));
2129
2130 let x1 = BigUint::from(10u64).pow(base.decimals) / BigUint::from(100u64);
2131 let x2 = &x1 + (&x1 / BigUint::from(100u64));
2132 let y1 = hooked
2133 .get_amount_out(x1.clone(), &base, "e)
2134 .expect("the smaller probe swap fits")
2135 .amount;
2136 let y2 = hooked
2137 .get_amount_out(x2.clone(), &base, "e)
2138 .expect("the larger probe swap fits")
2139 .amount;
2140 let slope = (&y2 - &y1).to_f64().unwrap() / (&x2 - &x1).to_f64().unwrap();
2141
2142 let price = hooked
2143 .spot_price(&base, "e)
2144 .expect("the fallback always prices");
2145
2146 assert_eq!(price, slope);
2147 let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2148 .spot_price(&base, "e)
2149 .expect("a hookless pool always prices");
2150 assert!((price / core - 1.0).abs() < 1e-3, "fallback {price} strayed from core {core}");
2151 }
2152
2153 #[rstest]
2156 #[case::zero_for_one(true)]
2157 #[case::one_for_zero(false)]
2158 fn test_get_limits_reports_the_output_net_of_an_analytic_hook_fee(#[case] zero_for_one: bool) {
2159 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2160 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2161 hooked.set_hook_handler(Box::new(pons_test_handler()));
2162
2163 let (hookless_in, hookless_out) = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2164 .get_limits(token_in.address.clone(), token_out.address.clone())
2165 .expect("a pool with liquidity has limits");
2166 let (limit_in, limit_out) = hooked
2167 .get_limits(token_in.address.clone(), token_out.address.clone())
2168 .expect("a pool with liquidity has limits");
2169
2170 assert!(hookless_out > BigUint::zero(), "the reference pool must move some output");
2171 assert_eq!(limit_in, hookless_in);
2172 let taken = pons_test_handler()
2173 .fee_and_tax(biguint_to_u256(&hookless_out))
2174 .expect("a pool sized output never overflows");
2175 assert_eq!(limit_out, &hookless_out - u256_to_biguint(taken));
2176 assert!(limit_out < hookless_out);
2177 }
2178
2179 #[rstest]
2182 #[case::zero_for_one(true)]
2183 #[case::one_for_zero(false)]
2184 fn test_get_limits_unchanged_without_an_analytic_hook_fee(#[case] zero_for_one: bool) {
2185 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2186 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2187 hooked.set_hook_handler(after_swap_test_hook(&[
2188 HookOptions::AfterSwap,
2189 HookOptions::AfterSwapReturnsDelta,
2190 ]));
2191
2192 let hookless = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2193 .get_limits(token_in.address.clone(), token_out.address.clone())
2194 .expect("a pool with liquidity has limits");
2195 let limits = hooked
2196 .get_limits(token_in.address.clone(), token_out.address.clone())
2197 .expect("a pool with liquidity has limits");
2198
2199 assert_eq!(limits, hookless);
2200 }
2201
2202 #[test]
2205 fn test_get_limits_on_a_drained_pool_is_zero_with_an_analytic_hook() {
2206 let (token_in, token_out) = basic_v4_test_pool_tokens(true);
2207 let mut drained = create_feeless_v4_test_pool(0);
2208 drained.set_hook_handler(Box::new(pons_test_handler()));
2209
2210 let limits = drained
2211 .get_limits(token_in.address, token_out.address)
2212 .expect("a drained pool reports zero rather than failing");
2213
2214 assert_eq!(limits, (BigUint::zero(), BigUint::zero()));
2215 }
2216
2217 #[rstest]
2220 #[case::the_whole_output(10_000)]
2221 #[case::more_than_the_whole_output(20_000)]
2222 fn test_spot_price_rejects_a_hook_that_takes_the_whole_output(#[case] analytic_fee_bps: u32) {
2223 let mut pool = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2224 pool.set_hook_handler(analytic_fee_test_hook(analytic_fee_bps));
2225
2226 let error = pool
2227 .spot_price(&token_x(), &token_y())
2228 .expect_err("a rate of one or more is not a price");
2229
2230 assert!(matches!(error, SimulationError::FatalError(_)), "{error:?}");
2231 }
2232
2233 #[test]
2234 fn test_spot_price_with_recoverable_error() {
2235 let usv4_state = UniswapV4State::new(
2239 1000000000000000000u128, U256::from_str("79228162514264337593543950336").unwrap(), UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 },
2242 0,
2243 60,
2244 vec![
2245 TickInfo::new(-600, 500000000000000000i128).unwrap(),
2246 TickInfo::new(600, -500000000000000000i128).unwrap(),
2247 ],
2248 )
2249 .unwrap();
2250
2251 let spot_price_result = usv4_state.spot_price(&usdc(), &weth());
2253 assert!(spot_price_result.is_ok());
2254
2255 let price = spot_price_result.unwrap();
2258 assert!(price > 0.0);
2259 }
2260
2261 #[test]
2262 fn test_get_limits_with_hook_managed_liquidity_no_ranges_entrypoint() {
2263 let block = BlockHeader {
2268 number: 22689128,
2269 hash: Bytes::from_str(
2270 "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
2271 )
2272 .expect("Invalid block hash"),
2273 timestamp: 1749739055,
2274 ..Default::default()
2275 };
2276
2277 let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
2278 .expect("Invalid hook address");
2279
2280 let db = SimulationDB::new(
2281 get_client(None).expect("Failed to create client"),
2282 get_runtime().expect("Failed to get runtime"),
2283 Some(block.clone()),
2284 );
2285 let engine = create_engine(db, true).expect("Failed to create simulation engine");
2286 let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
2287 .expect("Invalid pool manager address");
2288
2289 let hook_handler = GenericVMHookHandler::new(
2292 hook_address,
2293 engine,
2294 pool_manager,
2295 HashMap::new(),
2296 HashMap::new(),
2297 None,
2298 true, )
2300 .unwrap();
2301
2302 let mut usv4_state = UniswapV4State::new(
2304 0, U256::from_str("4295128740").unwrap(),
2306 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
2307 0, 1, vec![], )
2311 .unwrap();
2312
2313 usv4_state.set_hook_handler(Box::new(hook_handler));
2314
2315 let token_in = usdc().address;
2316 let token_out = weth().address;
2317
2318 let (amount_in_limit, amount_out_limit) = usv4_state
2319 .get_limits(token_in, token_out)
2320 .expect("Should find limits through experimental swapping");
2321
2322 assert!(amount_in_limit > BigUint::from(10u64).pow(12));
2325 assert!(amount_in_limit < BigUint::from(10u64).pow(14));
2326
2327 assert!(amount_out_limit > BigUint::from(10u64).pow(20));
2329 assert!(amount_out_limit < BigUint::from(10u64).pow(22));
2330 }
2331
2332 #[rstest]
2333 #[case::high_liquidity(u128::MAX / 2)] #[case::medium_liquidity(10000000000000000000u128)] #[case::minimal_liquidity(1000u128)] fn test_find_max_amount(#[case] liquidity: u128) {
2337 let fees = UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 };
2339 let tick_spacing = 60;
2340 let ticks = vec![
2341 TickInfo::new(-600, (liquidity / 4) as i128).unwrap(),
2342 TickInfo::new(600, -((liquidity / 4) as i128)).unwrap(),
2343 ];
2344
2345 let usv4_state = UniswapV4State::new(
2346 liquidity,
2347 U256::from_str("79228162514264337593543950336").unwrap(),
2348 fees,
2349 0,
2350 tick_spacing,
2351 ticks,
2352 )
2353 .unwrap();
2354
2355 let token_in = usdc();
2356 let token_out = weth();
2357
2358 let (max_amount_in, _max_amount_out) = usv4_state
2359 .find_max_amount(&token_in, &token_out)
2360 .unwrap();
2361
2362 let success = usv4_state
2363 .get_amount_out(max_amount_in.clone(), &token_in, &token_out)
2364 .is_ok();
2365 assert!(success, "Should be able to swap the exact max amount.");
2366
2367 let one_more = &max_amount_in + BigUint::from(1u64);
2368 let should_fail = usv4_state
2369 .get_amount_out(one_more, &token_in, &token_out)
2370 .is_err();
2371 assert!(should_fail, "Swapping max_amount + 1 should fail.");
2372 }
2373
2374 #[test]
2375 fn test_calculate_swap_fees_with_override() {
2376 let fees = UniswapV4Fees::new(100, 90, 500);
2378
2379 let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
2382 assert_eq!(total_zero_for_one, 600);
2384
2385 let total_with_override = fees.calculate_swap_fees_pips(true, Some(1000));
2387 assert_eq!(total_with_override, 1100);
2389 }
2390
2391 #[test]
2392 fn test_max_combined_fees_stays_valid() {
2393 let fees = UniswapV4Fees::new(1000, 1000, 1000);
2395 let total = fees.calculate_swap_fees_pips(true, Some(lp_fee::MAX_LP_FEE));
2396
2397 assert_eq!(total, 1_000_000);
2400 }
2401
2402 #[test]
2403 fn test_get_limits_graceful_underflow() {
2404 let usv4_state = UniswapV4State::new(
2406 1000000,
2407 U256::from_str("79228162514264337593543950336").unwrap(), UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2409 0,
2410 60,
2411 vec![
2412 TickInfo {
2415 index: -60,
2416 net_liquidity: 2000000, sqrt_price: U256::from_str("79051508376726796163471739988").unwrap(),
2418 },
2419 ],
2420 )
2421 .unwrap();
2422
2423 let usdc = usdc();
2424 let weth = weth();
2425
2426 let (limit_in, limit_out) = usv4_state
2427 .get_limits(usdc.address.clone(), weth.address.clone())
2428 .unwrap();
2429
2430 assert!(limit_in > BigUint::zero());
2432 assert!(limit_out > BigUint::zero());
2433 }
2434
2435 const MAX_PROTOCOL_FEE: u32 = 1000;
2440
2441 #[rstest]
2442 #[case::max_protocol_and_max_lp(MAX_PROTOCOL_FEE, lp_fee::MAX_LP_FEE, lp_fee::MAX_LP_FEE)]
2443 #[case::max_protocol_with_3000_lp(MAX_PROTOCOL_FEE, 3000, 3997)]
2444 #[case::max_protocol_with_zero_lp(MAX_PROTOCOL_FEE, 0, MAX_PROTOCOL_FEE)]
2445 #[case::zero_protocol_zero_lp(0, 0, 0)]
2446 #[case::zero_protocol_with_1000_lp(0, 1000, 1000)]
2447 fn test_calculate_swap_fees_uniswap_test_cases(
2448 #[case] protocol_fee: u32,
2449 #[case] lp_fee: u32,
2450 #[case] expected: u32,
2451 ) {
2452 let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
2453 let result = fees.calculate_swap_fees_pips(true, None);
2454 assert_eq!(result, expected);
2455 }
2456
2457 #[test]
2458 fn test_calculate_swap_fees_with_dynamic_fee() {
2459 let fees = UniswapV4Fees::new(100, 90, lp_fee::DYNAMIC_FEE_FLAG);
2461
2462 let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
2464 assert_eq!(total_zero_for_one, 100);
2466
2467 let total_with_override = fees.calculate_swap_fees_pips(true, Some(500));
2469 assert_eq!(total_with_override, 600);
2471 }
2472
2473 #[test]
2474 fn test_calculate_swap_fees_direction_matters() {
2475 let fees = UniswapV4Fees::new(100, 200, 500);
2477
2478 let zero_for_one_fee = fees.calculate_swap_fees_pips(true, None);
2479 assert_eq!(zero_for_one_fee, 600);
2481
2482 let one_for_zero_fee = fees.calculate_swap_fees_pips(false, None);
2483 assert_eq!(one_for_zero_fee, 700);
2485 }
2486
2487 #[rstest]
2488 #[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(
2492 #[case] protocol_fee: u32,
2493 #[case] lp_fee: u32,
2494 #[case] expected: u32,
2495 ) {
2496 let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
2499 let result = fees.calculate_swap_fees_pips(true, None);
2500 assert_eq!(result, expected, "Failed for protocol={}, lp={}", protocol_fee, lp_fee);
2501 }
2502
2503 #[test]
2504 fn test_calculate_swap_fees_override_takes_precedence() {
2505 let fees = UniswapV4Fees::new(100, 100, 3000);
2507
2508 let result = fees.calculate_swap_fees_pips(true, Some(5000));
2510 assert_eq!(result, 5100);
2512
2513 let result_no_override = fees.calculate_swap_fees_pips(true, None);
2515 assert_eq!(result_no_override, 3100);
2517 }
2518
2519 #[test]
2520 fn test_calculate_swap_fees_zero_protocol_fee() {
2521 let fees = UniswapV4Fees::new(0, 0, 3000);
2523 let result = fees.calculate_swap_fees_pips(true, None);
2524 assert_eq!(result, 3000);
2526 }
2527
2528 #[test]
2529 fn test_calculate_swap_fees_zero_lp_fee() {
2530 let fees = UniswapV4Fees::new(500, 500, 0);
2532 let result = fees.calculate_swap_fees_pips(true, None);
2533 assert_eq!(result, 500);
2535 }
2536
2537 fn create_basic_v4_test_pool() -> UniswapV4State {
2539 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))
2541 .expect("Failed to calculate sqrt price");
2542 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2543
2544 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2545
2546 UniswapV4State::new(
2547 liquidity,
2548 sqrt_price,
2549 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 }, tick,
2551 60, ticks,
2553 )
2554 .expect("Failed to create pool")
2555 }
2556
2557 fn create_tick_boundary_v4_test_pool() -> UniswapV4State {
2558 let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
2559 let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
2560
2561 UniswapV4State::new(
2562 100_000_000_000_000_000_000u128,
2563 sqrt_price,
2564 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2565 0,
2566 60,
2567 ticks,
2568 )
2569 .expect("Failed to create pool")
2570 }
2571
2572 #[test]
2573 fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
2574 let pool = create_tick_boundary_v4_test_pool();
2575 let amount = -I256::from_raw(U256::from(100_000_000_000_000_000u64));
2576
2577 let result = pool
2578 .swap(true, amount, None, None)
2579 .expect("swap should stay within the current liquidity range");
2580 let expected_tick =
2581 get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
2582
2583 assert_ne!(result.sqrt_price, pool.sqrt_price);
2584 assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
2585 assert_ne!(expected_tick, pool.tick);
2586 assert_eq!(result.tick, expected_tick);
2587 }
2588
2589 #[test]
2590 fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
2591 let mut pool = create_tick_boundary_v4_test_pool();
2592 pool.tick = -1;
2593 let amount = -I256::from_raw(U256::from(1u64));
2594
2595 let result = pool
2596 .swap(true, amount, None, None)
2597 .expect("swap should consume the input as fee without moving price");
2598
2599 assert_eq!(result.sqrt_price, pool.sqrt_price);
2600 assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
2601 assert_eq!(result.tick, pool.tick);
2602 }
2603
2604 #[test]
2605 fn test_swap_to_price_price_too_high() {
2606 let pool = create_basic_v4_test_pool();
2607
2608 let token_x = token_x();
2609 let token_y = token_y();
2610
2611 let target_price = Price::new(BigUint::from(10_000_000u64), BigUint::from(1_000_000u64));
2613
2614 let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
2615 token_x,
2616 token_y,
2617 SwapConstraint::PoolTargetPrice {
2618 target: target_price,
2619 tolerance: 0f64,
2620 min_amount_in: None,
2621 max_amount_in: None,
2622 },
2623 ));
2624 assert!(result.is_err(), "Should return error when target price is unreachable");
2625 }
2626
2627 #[test]
2628 fn test_swap_to_price_no_liquidity() {
2629 let pool = UniswapV4State::new(
2631 0, U256::from_str("79228162514264337593543950336").unwrap(),
2633 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2634 0,
2635 60,
2636 vec![],
2637 )
2638 .unwrap();
2639
2640 let token_x = token_x();
2641 let token_y = token_y();
2642
2643 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_000_000u64));
2644
2645 let pool_swap = pool.query_pool_swap(&QueryPoolSwapParams::new(
2646 token_x,
2647 token_y,
2648 SwapConstraint::PoolTargetPrice {
2649 target: target_price,
2650 tolerance: 0f64,
2651 min_amount_in: None,
2652 max_amount_in: None,
2653 },
2654 ));
2655
2656 assert!(pool_swap.is_err());
2657 }
2658
2659 #[test]
2660 fn test_swap_to_price_with_protocol_fees() {
2661 let liquidity = 100_000_000_000_000_000_000u128;
2662 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2663 .expect("Failed to calculate sqrt price");
2664 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2665
2666 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2667
2668 let pool = UniswapV4State::new(
2670 liquidity,
2671 sqrt_price,
2672 UniswapV4Fees {
2673 zero_for_one: 1000, one_for_zero: 200, lp_fee: 3000, },
2677 tick,
2678 60,
2679 ticks,
2680 )
2681 .expect("Failed to create pool");
2682
2683 let token_x = token_x();
2684 let token_y = token_y();
2685
2686 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2690 let pool_swap_forward = pool
2691 .query_pool_swap(&QueryPoolSwapParams::new(
2692 token_x.clone(),
2693 token_y.clone(),
2694 SwapConstraint::PoolTargetPrice {
2695 target: target_price,
2696 tolerance: 0f64,
2697 min_amount_in: None,
2698 max_amount_in: None,
2699 },
2700 ))
2701 .expect("swap_to_price failed");
2702
2703 let target_price_reverse =
2705 Price::new(BigUint::from(1_010_000u64), BigUint::from(2_040_000u64));
2706 let pool_swap_backward = pool
2707 .query_pool_swap(&QueryPoolSwapParams::new(
2708 token_y,
2709 token_x,
2710 SwapConstraint::PoolTargetPrice {
2711 target: target_price_reverse,
2712 tolerance: 0f64,
2713 min_amount_in: None,
2714 max_amount_in: None,
2715 },
2716 ))
2717 .expect("swap_to_price failed");
2718
2719 assert!(
2720 pool_swap_backward.amount_out().clone() > BigUint::ZERO,
2721 "One for zero swap should return non-zero output"
2722 );
2723
2724 assert!(
2727 pool_swap_forward.amount_out() < pool_swap_backward.amount_in(),
2728 "Backward fees should be lower therefore backward swap should be bigger"
2729 );
2730 assert!(
2731 pool_swap_forward.amount_in() < pool_swap_backward.amount_out(),
2732 "Backward fees should be lower therefore backward swap should be bigger"
2733 );
2734 }
2735
2736 #[test]
2737 fn test_swap_to_price_different_targets() {
2738 let pool = create_basic_v4_test_pool();
2740
2741 let token_x = token_x();
2742 let token_y = token_y();
2743
2744 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2747 let pool_swap_close = pool
2748 .query_pool_swap(&QueryPoolSwapParams::new(
2749 token_x.clone(),
2750 token_y.clone(),
2751 SwapConstraint::PoolTargetPrice {
2752 target: target_price,
2753 tolerance: 0f64,
2754 min_amount_in: None,
2755 max_amount_in: None,
2756 },
2757 ))
2758 .expect("swap_to_price failed");
2759 assert!(
2760 *pool_swap_close.amount_out() > BigUint::ZERO,
2761 "Expected non-zero for 1.98 Y/X target"
2762 );
2763
2764 let target_price = Price::new(BigUint::from(1_900_000u64), BigUint::from(1_000_000u64));
2766 let pool_swap_below = pool
2767 .query_pool_swap(&QueryPoolSwapParams::new(
2768 token_x.clone(),
2769 token_y.clone(),
2770 SwapConstraint::PoolTargetPrice {
2771 target: target_price,
2772 tolerance: 0f64,
2773 min_amount_in: None,
2774 max_amount_in: None,
2775 },
2776 ))
2777 .expect("swap_to_price failed");
2778 assert!(
2779 pool_swap_below.amount_out().clone() > BigUint::ZERO,
2780 "Expected non-zero for 1.90 Y/X target"
2781 );
2782
2783 let target_price = Price::new(BigUint::from(1_500_000u64), BigUint::from(1_000_000u64));
2785 let pool_swap_far = pool
2786 .query_pool_swap(&QueryPoolSwapParams::new(
2787 token_x,
2788 token_y,
2789 SwapConstraint::PoolTargetPrice {
2790 target: target_price,
2791 tolerance: 0f64,
2792 min_amount_in: None,
2793 max_amount_in: None,
2794 },
2795 ))
2796 .expect("swap_to_price failed");
2797 assert!(
2798 pool_swap_far.amount_out().clone() > BigUint::ZERO,
2799 "Expected non-zero for 1.5 Y/X target"
2800 );
2801
2802 assert!(
2804 pool_swap_close.amount_out().clone() < pool_swap_below.amount_out().clone(),
2805 "Closer target (1.98 Y/X) should require less volume than medium target (1.90 Y/X). \
2806 Got close: {}, medium: {}",
2807 pool_swap_close.amount_out().clone(),
2808 pool_swap_below.amount_out().clone()
2809 );
2810 assert!(
2811 pool_swap_below.amount_out().clone() < pool_swap_far.amount_out().clone(),
2812 "Medium target (1.90 Y/X) should require less volume than far target (1.5 Y/X). \
2813 Got medium: {}, far: {}",
2814 pool_swap_below.amount_out().clone(),
2815 pool_swap_far.amount_out().clone()
2816 );
2817 }
2818
2819 #[test]
2820 fn test_swap_to_price_around_spot_price() {
2821 let liquidity = 10_000_000_000_000_000u128;
2822 let sqrt_price =
2823 get_sqrt_price_q96(U256::from(2_000_000_000u64), U256::from(1_000_000_000u64))
2824 .expect("Failed to calculate sqrt price");
2825 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2826
2827 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2828
2829 let pool = UniswapV4State::new(
2831 liquidity,
2832 sqrt_price,
2833 UniswapV4Fees {
2834 zero_for_one: 0,
2835 one_for_zero: 0,
2836 lp_fee: 500, },
2838 tick,
2839 60,
2840 ticks,
2841 )
2842 .expect("Failed to create pool");
2843
2844 let token_x = token_x();
2845 let token_y = token_y();
2846
2847 let target_price = Price::new(BigUint::from(1_999_750u64), BigUint::from(1_000_250u64));
2849
2850 let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
2851 token_x.clone(),
2852 token_y.clone(),
2853 SwapConstraint::PoolTargetPrice {
2854 target: target_price,
2855 tolerance: 0f64,
2856 min_amount_in: None,
2857 max_amount_in: None,
2858 },
2859 ));
2860 assert!(result.is_err(), "Should return error when target price is unreachable");
2861
2862 let target_price = Price::new(BigUint::from(1_999_000u64), BigUint::from(1_001_000u64));
2864
2865 let pool_swap = pool
2866 .query_pool_swap(&QueryPoolSwapParams::new(
2867 token_x,
2868 token_y,
2869 SwapConstraint::PoolTargetPrice {
2870 target: target_price,
2871 tolerance: 0f64,
2872 min_amount_in: None,
2873 max_amount_in: None,
2874 },
2875 ))
2876 .expect("swap_to_price failed");
2877
2878 let expected_amount_out =
2880 BigUint::from_str("7062236922008").expect("Failed to parse expected value");
2881 assert_eq!(
2882 pool_swap.amount_out().clone(),
2883 expected_amount_out,
2884 "V4 should match V3 output with same fees (0.05%)"
2885 );
2886 }
2887
2888 #[test]
2889 fn test_swap_to_price_matches_get_amount_out() {
2890 let pool = create_basic_v4_test_pool();
2891
2892 let token_x = token_x();
2893 let token_y = token_y();
2894
2895 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2897 let pool_swap = pool
2898 .query_pool_swap(&QueryPoolSwapParams::new(
2899 token_x.clone(),
2900 token_y.clone(),
2901 SwapConstraint::PoolTargetPrice {
2902 target: target_price,
2903 tolerance: 0f64,
2904 min_amount_in: None,
2905 max_amount_in: None,
2906 },
2907 ))
2908 .expect("swap_to_price failed");
2909 assert!(*pool_swap.amount_in() > BigUint::ZERO, "Amount in should be positive");
2910
2911 let result = pool
2913 .get_amount_out(pool_swap.amount_in().clone(), &token_x, &token_y)
2914 .expect("get_amount_out failed");
2915
2916 assert!(result.amount > BigUint::ZERO);
2919 assert!(result.amount >= *pool_swap.amount_out());
2920 }
2921
2922 #[test]
2923 fn test_swap_to_price_basic() {
2924 let liquidity = 100_000_000_000_000_000_000u128;
2925 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2926 .expect("Failed to calculate sqrt price");
2927 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2928
2929 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2930
2931 let pool = UniswapV4State::new(
2932 liquidity,
2933 sqrt_price,
2934 UniswapV4Fees {
2935 zero_for_one: 0,
2936 one_for_zero: 0,
2937 lp_fee: 3000, },
2939 tick,
2940 60,
2941 ticks,
2942 )
2943 .expect("Failed to create pool");
2944
2945 let token_x = token_x();
2946 let token_y = token_y();
2947
2948 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2950
2951 let pool_swap = pool
2952 .query_pool_swap(&QueryPoolSwapParams::new(
2953 token_x,
2954 token_y,
2955 SwapConstraint::PoolTargetPrice {
2956 target: target_price,
2957 tolerance: 0f64,
2958 min_amount_in: None,
2959 max_amount_in: None,
2960 },
2961 ))
2962 .expect("swap_to_price failed");
2963
2964 let expected_amount_in = BigUint::from_str("246739021727519745").unwrap();
2966 let expected_amount_out = BigUint::from_str("490291909043340795").unwrap();
2967
2968 assert_eq!(
2969 *pool_swap.amount_in(),
2970 expected_amount_in,
2971 "amount_in should match expected value"
2972 );
2973 assert_eq!(
2974 *pool_swap.amount_out(),
2975 expected_amount_out,
2976 "amount_out should match expected value"
2977 );
2978 }
2979
2980 #[test]
2981 fn test_swap_price_limit_out_of_range_returns_error() {
2982 let pool = create_basic_v4_test_pool();
2983 let amount = -I256::from_raw(U256::from(1000u64)); let result = pool.swap(true, amount, Some(pool.sqrt_price), None);
2987 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2988
2989 let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO), None);
2991 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2992
2993 let result = pool.swap(false, amount, Some(pool.sqrt_price), None);
2995 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
2996
2997 let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO), None);
2999 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3000 }
3001
3002 #[test]
3003 fn test_swap_at_extreme_price_returns_error() {
3004 let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
3007 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
3008 let aligned_tick = (MIN_TICK / 60) * 60 + 60; let ticks = vec![
3011 TickInfo::new(aligned_tick, 0).unwrap(),
3012 TickInfo::new(aligned_tick + 60, 0).unwrap(),
3013 ];
3014 let pool = UniswapV4State::new(
3015 100_000_000_000_000_000_000u128,
3016 sqrt_price,
3017 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
3018 tick,
3019 60,
3020 ticks,
3021 )
3022 .unwrap();
3023
3024 let amount = -I256::from_raw(U256::from(1000u64));
3025 let result = pool.swap(true, amount, None, None);
3027 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3028 }
3029}