1use std::{any::Any, collections::HashMap, fmt, sync::Arc};
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::{
24 protocol::{
25 clmm::clmm_swap_to_price,
26 safe_math::{safe_add_u256, safe_sub_u256},
27 u256_num::{u256_to_biguint, u256_to_f64},
28 uniswap_v4::hooks::{
29 hook_handler::HookHandler,
30 models::{
31 AfterSwapParameters, BalanceDelta, BeforeSwapDelta, BeforeSwapParameters,
32 StateContext, SwapParams,
33 },
34 },
35 utils::{
36 add_fee_markup,
37 uniswap::{
38 i24_be_bytes_to_i32, liquidity_math,
39 lp_fee::{self, is_dynamic},
40 sqrt_price_math::{
41 get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64,
42 },
43 swap_math,
44 tick_list::{TickInfo, TickList, TickListErrorKind},
45 tick_math::{
46 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK,
47 MIN_SQRT_RATIO, MIN_TICK,
48 },
49 StepComputation, SwapResults, SwapState,
50 },
51 },
52 vm::constants::EXTERNAL_ACCOUNT,
53 },
54 simulation::PendingOverrides,
55 },
56 impl_non_serializable_protocol,
57};
58
59const SWAP_BASE_GAS: u64 = 185_000;
62const GAS_PER_BITMAP_LOOKUP: u64 = 3_500;
65const GAS_PER_TICK: u64 = 29_000;
69const V4_CALLBACK_SETTLEMENT_GAS: u64 = 30_000;
71const PM_PER_HOOK_CALL_OVERHEAD: u64 = 25_000;
75const MAX_SWAP_GAS: u64 = 16_700_000;
77const MAX_TICKS_CROSSED: u64 = (MAX_SWAP_GAS - SWAP_BASE_GAS) / GAS_PER_TICK;
78const HOOK_FEE_PROBE_EXP: u64 = 30;
83
84#[derive(Clone)]
85pub struct UniswapV4State {
86 liquidity: u128,
87 sqrt_price: U256,
88 fees: UniswapV4Fees,
89 tick: i32,
90 ticks: TickList,
91 tick_spacing: i32,
92 pub hook: Option<Box<dyn HookHandler>>,
93 pending_overrides: Option<Arc<PendingOverrides>>,
96}
97
98impl_non_serializable_protocol!(UniswapV4State, "not supported due vm state deps");
99
100impl fmt::Debug for UniswapV4State {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.debug_struct("UniswapV4State")
103 .field("liquidity", &self.liquidity)
104 .field("sqrt_price", &self.sqrt_price)
105 .field("fees", &self.fees)
106 .field("tick", &self.tick)
107 .field("tick_spacing", &self.tick_spacing)
108 .field("pending_overrides", &self.pending_overrides.is_some())
109 .finish_non_exhaustive()
110 }
111}
112
113impl PartialEq for UniswapV4State {
114 fn eq(&self, other: &Self) -> bool {
115 match (&self.hook, &other.hook) {
116 (Some(a), Some(b)) => a.is_equal(&**b),
117 (None, None) => true,
118 _ => false,
119 }
120 }
121}
122
123impl Eq for UniswapV4State {}
124
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub struct UniswapV4Fees {
127 pub zero_for_one: u32,
129 pub one_for_zero: u32,
131 pub lp_fee: u32,
133}
134
135impl UniswapV4Fees {
136 pub fn new(zero_for_one: u32, one_for_zero: u32, lp_fee: u32) -> Self {
137 Self { zero_for_one, one_for_zero, lp_fee }
138 }
139
140 fn calculate_swap_fees_pips(&self, zero_for_one: bool, lp_fee_override: Option<u32>) -> u32 {
141 let protocol_fee = if zero_for_one { self.zero_for_one } else { self.one_for_zero };
142 let lp_fee = lp_fee_override.unwrap_or_else(|| {
143 if is_dynamic(self.lp_fee) {
145 0
146 } else {
147 self.lp_fee
148 }
149 });
150
151 protocol_fee + lp_fee - ((protocol_fee as u64 * lp_fee as u64 / 1_000_000) as u32)
156 }
157}
158
159impl UniswapV4State {
160 pub fn new(
162 liquidity: u128,
163 sqrt_price: U256,
164 fees: UniswapV4Fees,
165 tick: i32,
166 tick_spacing: i32,
167 ticks: Vec<TickInfo>,
168 ) -> Result<Self, SimulationError> {
169 let tick_spacing_u16 = tick_spacing.try_into().map_err(|_| {
170 SimulationError::FatalError(format!(
173 "tick_spacing {} must be positive (int24 -> u16 conversion failed)",
174 tick_spacing
175 ))
176 })?;
177 let tick_list = TickList::from(tick_spacing_u16, ticks)?;
178 Ok(UniswapV4State {
179 liquidity,
180 sqrt_price,
181 fees,
182 tick,
183 ticks: tick_list,
184 tick_spacing,
185 hook: None,
186 pending_overrides: None,
187 })
188 }
189
190 pub fn pending_overrides(&self) -> Option<&PendingOverrides> {
191 self.pending_overrides.as_deref()
192 }
193
194 fn swap(
195 &self,
196 zero_for_one: bool,
197 amount_specified: I256,
198 sqrt_price_limit: Option<U256>,
199 lp_fee_override: Option<u32>,
200 ) -> Result<SwapResults, SimulationError> {
201 if amount_specified == I256::ZERO {
202 return Ok(SwapResults {
203 amount_calculated: I256::ZERO,
204 amount_specified: I256::ZERO,
205 amount_remaining: I256::ZERO,
206 sqrt_price: self.sqrt_price,
207 liquidity: self.liquidity,
208 tick: self.tick,
209 gas_used: U256::from(3_000), });
211 }
212
213 if self.liquidity == 0 {
214 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
215 }
216 let price_limit = if let Some(limit) = sqrt_price_limit {
217 limit
218 } else if zero_for_one {
219 safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
220 } else {
221 safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
222 };
223
224 let price_limit_valid = if zero_for_one {
225 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
226 } else {
227 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
228 };
229 if !price_limit_valid {
230 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
231 }
232
233 let exact_input = amount_specified < I256::ZERO;
234
235 let mut state = SwapState {
236 amount_remaining: amount_specified,
237 amount_calculated: I256::ZERO,
238 sqrt_price: self.sqrt_price,
239 tick: self.tick,
240 liquidity: self.liquidity,
241 };
242 let mut gas_used = U256::from(SWAP_BASE_GAS);
243
244 while state.amount_remaining != I256::ZERO && state.sqrt_price != price_limit {
245 let (mut next_tick, initialized) = match self
246 .ticks
247 .next_initialized_tick_within_one_word(state.tick, zero_for_one)
248 {
249 Ok((tick, init)) => {
250 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_BITMAP_LOOKUP))?;
251 (tick, init)
252 }
253 Err(tick_err) => match tick_err.kind {
254 TickListErrorKind::TicksExeeded => {
255 let mut new_state = self.clone();
256 new_state.liquidity = state.liquidity;
257 new_state.tick = state.tick;
258 new_state.sqrt_price = state.sqrt_price;
259 return Err(SimulationError::InvalidInput(
260 "Ticks exceeded".into(),
261 Some(GetAmountOutResult::new(
262 u256_to_biguint(state.amount_calculated.abs().into_raw()),
263 u256_to_biguint(gas_used),
264 Box::new(new_state),
265 )),
266 ));
267 }
268 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
269 },
270 };
271
272 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
273
274 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
275 let fee_pips = self
276 .fees
277 .calculate_swap_fees_pips(zero_for_one, lp_fee_override);
278
279 let sqrt_price_start = state.sqrt_price;
280 let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
281 state.sqrt_price,
282 UniswapV4State::get_sqrt_ratio_target(sqrt_price_next, price_limit, zero_for_one),
283 state.liquidity,
284 -state.amount_remaining,
288 fee_pips,
289 )?;
290 state.sqrt_price = sqrt_price;
291
292 let step = StepComputation {
293 sqrt_price_start,
294 tick_next: next_tick,
295 initialized,
296 sqrt_price_next,
297 amount_in,
298 amount_out,
299 fee_amount,
300 };
301 if exact_input {
302 state.amount_remaining += I256::checked_from_sign_and_abs(
303 Sign::Positive,
304 safe_add_u256(step.amount_in, step.fee_amount)?,
305 )
306 .unwrap();
307 state.amount_calculated -=
308 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
309 } else {
310 state.amount_remaining -=
311 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
312 state.amount_calculated += I256::checked_from_sign_and_abs(
313 Sign::Positive,
314 safe_add_u256(step.amount_in, step.fee_amount)?,
315 )
316 .unwrap();
317 }
318 if state.sqrt_price == step.sqrt_price_next {
319 if step.initialized {
320 let liquidity_raw = self
321 .ticks
322 .get_tick(step.tick_next)
323 .unwrap()
324 .net_liquidity;
325 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
326 state.liquidity =
327 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
328 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_TICK))?;
329 }
330 state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
331 } else if state.sqrt_price != step.sqrt_price_start {
332 state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
333 }
334 }
335
336 Ok(SwapResults {
337 amount_calculated: state.amount_calculated,
338 amount_specified,
339 amount_remaining: state.amount_remaining,
340 sqrt_price: state.sqrt_price,
341 liquidity: state.liquidity,
342 tick: state.tick,
343 gas_used: safe_add_u256(gas_used, U256::from(V4_CALLBACK_SETTLEMENT_GAS))?,
344 })
345 }
346
347 pub fn set_hook_handler(&mut self, handler: Box<dyn HookHandler>) {
348 self.hook = Some(handler);
349 }
350
351 fn get_sqrt_ratio_target(
352 sqrt_price_next: U256,
353 sqrt_price_limit: U256,
354 zero_for_one: bool,
355 ) -> U256 {
356 let cond1 = if zero_for_one {
357 sqrt_price_next < sqrt_price_limit
358 } else {
359 sqrt_price_next > sqrt_price_limit
360 };
361
362 if cond1 {
363 sqrt_price_limit
364 } else {
365 sqrt_price_next
366 }
367 }
368
369 fn find_limits_experimentally(
370 &self,
371 token_in: Bytes,
372 token_out: Bytes,
373 ) -> Result<(BigUint, BigUint), SimulationError> {
374 let token_in_obj =
377 Token::new(&token_in, "TOKEN_IN", 18, 0, &[Some(10_000)], Default::default(), 100);
378 let token_out_obj =
379 Token::new(&token_out, "TOKEN_OUT", 18, 0, &[Some(10_000)], Default::default(), 100);
380
381 self.find_max_amount(&token_in_obj, &token_out_obj)
382 }
383
384 fn find_max_amount(
397 &self,
398 token_in: &Token,
399 token_out: &Token,
400 ) -> Result<(BigUint, BigUint), SimulationError> {
401 let mut low = BigUint::from(1u64);
402
403 let mut high = BigUint::from(10u64).pow(18); let mut last_successful_amount_in = BigUint::from(1u64);
407 let mut last_successful_amount_out = BigUint::from(0u64);
408
409 while let Ok(result) = self.get_amount_out(high.clone(), token_in, token_out) {
412 low = last_successful_amount_in.clone();
415 last_successful_amount_in = high.clone();
416 last_successful_amount_out = result.amount;
417 high *= BigUint::from(10u64);
418
419 if high > BigUint::from(10u64).pow(75) {
421 return Ok((last_successful_amount_in, last_successful_amount_out));
422 }
423 }
424
425 while &high - &low > BigUint::from(1u64) {
427 let mid = (&low + &high) / BigUint::from(2u64);
428
429 match self.get_amount_out(mid.clone(), token_in, token_out) {
430 Ok(result) => {
431 last_successful_amount_in = mid.clone();
432 last_successful_amount_out = result.amount;
433 low = mid;
434 }
435 Err(_) => {
436 high = mid;
437 }
438 }
439 }
440
441 Ok((last_successful_amount_in, last_successful_amount_out))
442 }
443
444 fn has_no_initialized_ticks(&self) -> bool {
446 !self.ticks.has_initialized_ticks()
447 }
448
449 fn core_spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
452 let base_is_currency0 = base < quote;
453 let fee_pips = self
454 .fees
455 .calculate_swap_fees_pips(base_is_currency0, None);
456 let fee = fee_pips as f64 / 1_000_000.0;
457
458 let price = if base_is_currency0 {
459 sqrt_price_q96_to_f64(self.sqrt_price, base.decimals, quote.decimals)?
460 } else {
461 1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, quote.decimals, base.decimals)?
462 };
463
464 Ok(add_fee_markup(price, fee))
465 }
466
467 fn hook_fee_rate(&self, zero_for_one: bool) -> Result<Option<f64>, SimulationError> {
474 let Some(hook) = &self.hook else { return Ok(None) };
475
476 let probe = U256::from(10u64).pow(U256::from(HOOK_FEE_PROBE_EXP));
477 let Some(fee) = hook.unspecified_fee_amount(probe, zero_for_one)? else {
478 return Ok(None);
479 };
480
481 let rate = u256_to_f64(fee)? / u256_to_f64(probe)?;
482 if rate >= 1.0 {
483 return Err(SimulationError::FatalError(format!(
484 "Hook {} keeps {rate} of the output, leaving no price to quote",
485 hook.address()
486 )));
487 }
488
489 Ok(Some(rate))
490 }
491}
492
493#[typetag::serde]
494impl ProtocolSim for UniswapV4State {
495 fn set_pending_overrides(
498 &mut self,
499 overrides: Arc<dyn Any + Send + Sync>,
500 ) -> Result<(), SimulationError> {
501 let overrides = overrides
502 .downcast::<PendingOverrides>()
503 .map_err(|_| {
504 SimulationError::FatalError(
505 "Uniswap V4 pending overrides must be `PendingOverrides`".to_string(),
506 )
507 })?;
508 if let Some(hook) = &mut self.hook {
509 hook.set_pending_overrides(Arc::clone(&overrides));
510 }
511 self.pending_overrides = Some(overrides);
512 Ok(())
513 }
514
515 fn fee(&self) -> f64 {
518 todo!()
519 }
520
521 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
522 if let Some(hook) = &self.hook {
523 if let Some(rate) = self.hook_fee_rate(quote < base)? {
528 return Ok(add_fee_markup(self.core_spot_price(base, quote)?, rate));
529 }
530
531 match hook.spot_price(base, quote) {
532 Ok(price) => return Ok(price),
533 Err(SimulationError::RecoverableError(_)) => {
534 let x1 = BigUint::from(10u64).pow(base.decimals) / BigUint::from(100u64); let x2 = &x1 + (&x1 / BigUint::from(100u64));
542
543 let y1 = self.get_amount_out(x1.clone(), base, quote)?;
545 let y2 = self.get_amount_out(x2.clone(), base, quote)?;
546
547 let num = y2
549 .amount
550 .checked_sub(&y1.amount)
551 .ok_or_else(|| {
552 SimulationError::FatalError(
553 "Cannot calculate spot price: y2 < y1".to_string(),
554 )
555 })?;
556 let den = x2.checked_sub(&x1).ok_or_else(|| {
557 SimulationError::FatalError(
558 "Cannot calculate spot price: x2 < x1".to_string(),
559 )
560 })?;
561
562 if den == BigUint::from(0u64) {
563 return Err(SimulationError::FatalError(
564 "Cannot calculate spot price: denominator is zero".to_string(),
565 ));
566 }
567
568 let num_f64 = num.to_f64().ok_or_else(|| {
570 SimulationError::FatalError(
571 "Failed to convert numerator to f64".to_string(),
572 )
573 })?;
574 let den_f64 = den.to_f64().ok_or_else(|| {
575 SimulationError::FatalError(
576 "Failed to convert denominator to f64".to_string(),
577 )
578 })?;
579
580 let token_correction = 10f64.powi(base.decimals as i32 - quote.decimals as i32);
581
582 return Ok(num_f64 / den_f64 * token_correction);
583 }
584 Err(e) => return Err(e),
585 }
586 }
587
588 self.core_spot_price(base, quote)
589 }
590
591 fn get_amount_out(
592 &self,
593 amount_in: BigUint,
594 token_in: &Token,
595 token_out: &Token,
596 ) -> Result<GetAmountOutResult, SimulationError> {
597 let zero_for_one = token_in < token_out;
598 let amount_specified = I256::checked_from_sign_and_abs(
599 Sign::Negative,
600 U256::from_be_slice(&amount_in.to_bytes_be()),
601 )
602 .ok_or_else(|| {
603 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
604 })?;
605
606 let mut amount_to_swap = amount_specified;
607 let mut lp_fee_override: Option<u32> = None;
608 let mut before_swap_gas = 0u64;
609 let mut after_swap_gas = 0u64;
610 let mut before_swap_delta = BeforeSwapDelta(I256::ZERO);
611 let mut storage_overwrites = None;
612
613 let token_in_address = Address::from_slice(&token_in.address);
614 let token_out_address = Address::from_slice(&token_out.address);
615
616 let state_context = StateContext {
617 currency_0: if zero_for_one { token_in_address } else { token_out_address },
618 currency_1: if zero_for_one { token_out_address } else { token_in_address },
619 fees: self.fees.clone(),
620 tick_spacing: self.tick_spacing,
621 };
622
623 let swap_params = SwapParams {
624 zero_for_one,
625 amount_specified: amount_to_swap,
626 sqrt_price_limit: self.sqrt_price,
627 };
628
629 if let Some(ref hook) = self.hook {
631 if has_permission(hook.address(), HookOptions::BeforeSwap) {
632 let before_swap_params = BeforeSwapParameters {
633 context: state_context.clone(),
634 sender: *EXTERNAL_ACCOUNT,
635 swap_params: swap_params.clone(),
636 hook_data: Bytes::new(),
637 };
638
639 let before_swap_result = hook
640 .before_swap(before_swap_params, None, None)
641 .map_err(|e| {
642 SimulationError::FatalError(format!(
643 "BeforeSwap hook simulation failed: {e:?}"
644 ))
645 })?;
646
647 before_swap_gas = before_swap_result.gas_estimate;
648 before_swap_delta = before_swap_result.result.amount_delta;
649 storage_overwrites = Some(before_swap_result.result.overwrites);
650
651 if before_swap_delta.as_i256() != I256::ZERO {
654 amount_to_swap += I256::from(before_swap_delta.get_specified_delta());
655 if amount_to_swap > I256::ZERO {
656 return Err(SimulationError::FatalError(
657 "Hook delta exceeds swap amount".into(),
658 ));
659 }
660 }
661
662 let hook_fee = before_swap_result
667 .result
668 .fee
669 .to::<u32>();
670 if hook_fee != 0 {
671 let cleaned_fee = lp_fee::remove_override_flag(hook_fee);
673
674 if !lp_fee::is_valid(cleaned_fee) {
676 return Err(SimulationError::FatalError(format!(
677 "LP fee override {} exceeds maximum {} pips",
678 cleaned_fee,
679 lp_fee::MAX_LP_FEE
680 )));
681 }
682
683 lp_fee_override = Some(cleaned_fee);
684 }
685 }
686 }
687
688 let result = self.swap(zero_for_one, amount_to_swap, None, lp_fee_override)?;
690
691 let mut swap_delta = BalanceDelta::from_swap_result(result.amount_calculated, zero_for_one);
693
694 let hook_delta_specified = before_swap_delta.get_specified_delta();
697 let mut hook_delta_unspecified = before_swap_delta.get_unspecified_delta();
698
699 if let Some(ref hook) = self.hook {
700 if has_permission(hook.address(), HookOptions::AfterSwap) {
701 let after_swap_params = AfterSwapParameters {
702 context: state_context,
703 sender: *EXTERNAL_ACCOUNT,
704 swap_params,
705 delta: swap_delta,
706 hook_data: Bytes::new(),
707 };
708
709 let after_swap_result = hook
710 .after_swap(after_swap_params, storage_overwrites, None)
711 .map_err(|e| {
712 SimulationError::FatalError(format!(
713 "AfterSwap hook simulation failed: {e:?}"
714 ))
715 })?;
716 after_swap_gas = after_swap_result.gas_estimate;
717 if has_permission(hook.address(), HookOptions::AfterSwapReturnsDelta) {
722 hook_delta_unspecified += after_swap_result.result;
723 }
724 }
725 }
726
727 if (hook_delta_specified != I128::ZERO) || (hook_delta_unspecified != I128::ZERO) {
730 let hook_delta = if (amount_specified < I256::ZERO) == zero_for_one {
731 BalanceDelta::new(hook_delta_specified, hook_delta_unspecified)
732 } else {
733 BalanceDelta::new(hook_delta_unspecified, hook_delta_specified)
734 };
735 swap_delta = swap_delta - hook_delta
737 }
738
739 let amount_out = if (amount_specified < I256::ZERO) == zero_for_one {
740 swap_delta.amount1()
741 } else {
742 swap_delta.amount0()
743 };
744
745 trace!(?amount_in, ?token_in, ?token_out, ?zero_for_one, ?result, "V4 SWAP");
746 let mut new_state = self.clone();
747 new_state.liquidity = result.liquidity;
748 new_state.tick = result.tick;
749 new_state.sqrt_price = result.sqrt_price;
750
751 let mut hook_overhead = before_swap_gas + after_swap_gas;
757 if before_swap_gas > 0 {
758 hook_overhead += PM_PER_HOOK_CALL_OVERHEAD;
759 }
760 if after_swap_gas > 0 {
761 hook_overhead += PM_PER_HOOK_CALL_OVERHEAD;
762 }
763 let total_gas_used = result.gas_used + U256::from(hook_overhead);
764 Ok(GetAmountOutResult::new(
765 u256_to_biguint(U256::from(amount_out.abs())),
766 u256_to_biguint(total_gas_used),
767 Box::new(new_state),
768 ))
769 }
770
771 fn get_limits(
772 &self,
773 token_in: Bytes,
774 token_out: Bytes,
775 ) -> Result<(BigUint, BigUint), SimulationError> {
776 if let Some(hook) = &self.hook {
777 if self.liquidity == 0 && self.has_no_initialized_ticks() {
779 match hook.get_amount_ranges(token_in.clone(), token_out.clone()) {
781 Ok(amount_ranges) => {
782 return Ok((
783 u256_to_biguint(amount_ranges.amount_in_range.1),
784 u256_to_biguint(amount_ranges.amount_out_range.1),
785 ))
786 }
787 Err(SimulationError::RecoverableError(msg))
790 if msg.contains("not implemented") || msg.contains("not set") =>
791 {
792 return self.find_limits_experimentally(token_in, token_out);
795 }
797 Err(e) => return Err(e),
798 }
799 }
800 }
801
802 if self.liquidity == 0 {
804 return Ok((BigUint::zero(), BigUint::zero()));
805 }
806
807 let zero_for_one = token_in < token_out;
808 let mut current_tick = self.tick;
809 let mut current_sqrt_price = self.sqrt_price;
810 let mut current_liquidity = self.liquidity;
811 let mut total_amount_in = U256::ZERO;
812 let mut total_amount_out = U256::ZERO;
813 let mut ticks_crossed: u64 = 0;
814
815 while let Ok((tick, initialized)) = self
818 .ticks
819 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
820 {
821 if ticks_crossed >= MAX_TICKS_CROSSED {
823 break;
824 }
825 ticks_crossed += 1;
826
827 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
829
830 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
832
833 let (amount_in, amount_out) = if zero_for_one {
836 let amount0 = get_amount0_delta(
837 sqrt_price_next,
838 current_sqrt_price,
839 current_liquidity,
840 true,
841 )?;
842 let amount1 = get_amount1_delta(
843 sqrt_price_next,
844 current_sqrt_price,
845 current_liquidity,
846 false,
847 )?;
848 (amount0, amount1)
849 } else {
850 let amount0 = get_amount0_delta(
851 sqrt_price_next,
852 current_sqrt_price,
853 current_liquidity,
854 false,
855 )?;
856 let amount1 = get_amount1_delta(
857 sqrt_price_next,
858 current_sqrt_price,
859 current_liquidity,
860 true,
861 )?;
862 (amount1, amount0)
863 };
864
865 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
867 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
868
869 if initialized {
874 let liquidity_raw = self
875 .ticks
876 .get_tick(next_tick)
877 .unwrap()
878 .net_liquidity;
879 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
880
881 match liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta) {
884 Ok(new_liquidity) => {
885 current_liquidity = new_liquidity;
886 }
887 Err(_) => {
888 break;
891 }
892 }
893 }
894
895 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
897 current_sqrt_price = sqrt_price_next;
898
899 if current_liquidity == 0 {
901 break;
902 }
903 }
904
905 if let Some(hook) = &self.hook {
908 if let Some(fee) = hook.unspecified_fee_amount(total_amount_out, zero_for_one)? {
909 total_amount_out = safe_sub_u256(total_amount_out, fee)?;
910 }
911 }
912
913 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
914 }
915
916 fn delta_transition(
917 &mut self,
918 delta: ProtocolStateDelta,
919 tokens: &HashMap<Bytes, Token>,
920 balances: &Balances,
921 ) -> Result<(), TransitionError> {
922 if let Some(mut hook) = self.hook.clone() {
923 match hook.delta_transition(delta.clone(), tokens, balances) {
924 Ok(()) => self.set_hook_handler(hook),
925 Err(TransitionError::SimulationError(SimulationError::RecoverableError(msg)))
926 if msg.contains("not implemented") =>
927 {
928 }
930 Err(e) => return Err(e),
931 }
932 }
933
934 if let Some(liquidity) = delta
936 .updated_attributes
937 .get("liquidity")
938 {
939 self.liquidity = u128::from(liquidity.clone());
940 }
941 if let Some(sqrt_price) = delta
942 .updated_attributes
943 .get("sqrt_price_x96")
944 {
945 self.sqrt_price = U256::from_be_slice(sqrt_price);
946 }
947 if let Some(tick) = delta.updated_attributes.get("tick") {
948 self.tick = i24_be_bytes_to_i32(tick);
949 }
950 if let Some(lp_fee) = delta.updated_attributes.get("fee") {
951 self.fees.lp_fee = u32::from(lp_fee.clone());
952 }
953 if let Some(zero2one_protocol_fee) = delta
954 .updated_attributes
955 .get("protocol_fees/zero2one")
956 {
957 self.fees.zero_for_one = u32::from(zero2one_protocol_fee.clone());
958 }
959 if let Some(one2zero_protocol_fee) = delta
960 .updated_attributes
961 .get("protocol_fees/one2zero")
962 {
963 self.fees.one_for_zero = u32::from(one2zero_protocol_fee.clone());
964 }
965
966 for (key, value) in delta.updated_attributes.iter() {
968 if key.starts_with("ticks/") {
970 let parts: Vec<&str> = key.split('/').collect();
971 self.ticks
972 .set_tick_liquidity(
973 parts[1]
974 .parse::<i32>()
975 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
976 i128::from(value.clone()),
977 )
978 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
979 }
980 }
981 for key in delta.deleted_attributes.iter() {
983 if key.starts_with("ticks/") {
985 let parts: Vec<&str> = key.split('/').collect();
986 self.ticks
987 .set_tick_liquidity(
988 parts[1]
989 .parse::<i32>()
990 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
991 0,
992 )
993 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
994 }
995 }
996
997 Ok(())
998 }
999
1000 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
1009 if self.liquidity == 0 {
1010 return Err(SimulationError::FatalError("No liquidity".to_string()));
1011 }
1012
1013 let zero_for_one = params.token_in().address < params.token_out().address;
1015 let fee_pips = self
1016 .fees
1017 .calculate_swap_fees_pips(zero_for_one, None);
1018
1019 match params.swap_constraint() {
1020 SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
1021 "Uniswap V4 does not support TradeLimitPrice constraint in query_pool_swap"
1022 .to_string(),
1023 None,
1024 )),
1025 SwapConstraint::PoolTargetPrice {
1026 target,
1027 tolerance: _,
1028 min_amount_in: _,
1029 max_amount_in: _,
1030 } => {
1031 if self.liquidity == 0 {
1032 return Err(SimulationError::FatalError("No liquidity".to_string()));
1033 }
1034
1035 let (amount_in, amount_out, swap_result) = clmm_swap_to_price(
1036 self.sqrt_price,
1037 ¶ms.token_in().address,
1038 ¶ms.token_out().address,
1039 target,
1040 fee_pips,
1041 Sign::Negative, |zero_for_one, amount_specified, sqrt_price_limit| {
1043 self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit), None)
1044 },
1045 )?;
1046
1047 let mut new_state = self.clone();
1048 new_state.liquidity = swap_result.liquidity;
1049 new_state.tick = swap_result.tick;
1050 new_state.sqrt_price = swap_result.sqrt_price;
1051
1052 Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
1053 }
1054 }
1055 }
1056
1057 fn clone_box(&self) -> Box<dyn ProtocolSim> {
1058 Box::new(self.clone())
1059 }
1060
1061 fn as_any(&self) -> &dyn Any {
1062 self
1063 }
1064
1065 fn as_any_mut(&mut self) -> &mut dyn Any {
1066 self
1067 }
1068
1069 fn eq(&self, other: &dyn ProtocolSim) -> bool {
1070 if let Some(other_state) = other
1071 .as_any()
1072 .downcast_ref::<UniswapV4State>()
1073 {
1074 self.liquidity == other_state.liquidity &&
1075 self.sqrt_price == other_state.sqrt_price &&
1076 self.fees == other_state.fees &&
1077 self.tick == other_state.tick &&
1078 self.ticks == other_state.ticks
1079 } else {
1080 false
1081 }
1082 }
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087 use std::{collections::HashSet, fs, path::Path, str::FromStr};
1088
1089 use alloy::primitives::{aliases::U24, U160};
1090 use num_traits::FromPrimitive;
1091 use rstest::rstest;
1092 use serde_json::Value;
1093 use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
1094 use tycho_common::{models::Chain, simulation::protocol_sim::Price};
1095
1096 use super::*;
1097 use crate::{
1098 evm::{
1099 engine_db::{
1100 create_engine,
1101 simulation_db::SimulationDB,
1102 utils::{get_client, get_runtime},
1103 },
1104 protocol::{
1105 u256_num::biguint_to_u256,
1106 uniswap_v4::hooks::{
1107 angstrom::hook_handler::{AngstromFees, AngstromHookHandler},
1108 generic_vm_hook_handler::GenericVMHookHandler,
1109 models::{AfterSwapDelta, AmountRanges, BeforeSwapOutput, WithGasEstimate},
1110 pons_v2::hook_handler::{PonsV2HookHandler, PONS_V2_HOOK_ROBINHOOD},
1111 },
1112 utils::uniswap::{lp_fee, sqrt_price_math::get_sqrt_price_q96},
1113 },
1114 },
1115 protocol::models::{DecoderContext, TryFromWithBlock},
1116 };
1117
1118 fn usdc() -> Token {
1120 Token::new(
1121 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1122 "USDC",
1123 6,
1124 0,
1125 &[Some(10_000)],
1126 Default::default(),
1127 100,
1128 )
1129 }
1130
1131 fn weth() -> Token {
1132 Token::new(
1133 &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1134 "WETH",
1135 18,
1136 0,
1137 &[Some(10_000)],
1138 Default::default(),
1139 100,
1140 )
1141 }
1142
1143 fn eth() -> Token {
1144 Token::new(
1145 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
1146 "ETH",
1147 18,
1148 0,
1149 &[Some(10_000)],
1150 Default::default(),
1151 100,
1152 )
1153 }
1154
1155 fn token_x() -> Token {
1156 Token::new(
1157 &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
1158 "X",
1159 18,
1160 0,
1161 &[Some(10_000)],
1162 Default::default(),
1163 100,
1164 )
1165 }
1166
1167 fn token_y() -> Token {
1168 Token::new(
1169 &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
1170 "Y",
1171 18,
1172 0,
1173 &[Some(10_000)],
1174 Default::default(),
1175 100,
1176 )
1177 }
1178
1179 #[test]
1180 fn test_delta_transition() {
1181 let mut pool = UniswapV4State::new(
1182 1000,
1183 U256::from_str("1000").unwrap(),
1184 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 700 },
1185 100,
1186 60,
1187 vec![TickInfo::new(120, 10000).unwrap(), TickInfo::new(180, -10000).unwrap()],
1188 )
1189 .unwrap();
1190
1191 let attributes: HashMap<String, Bytes> = [
1192 ("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1193 ("sqrt_price_x96".to_string(), Bytes::from(1001_u64.to_be_bytes().to_vec())),
1194 ("tick".to_string(), Bytes::from(120_i32.to_be_bytes().to_vec())),
1195 ("protocol_fees/zero2one".to_string(), Bytes::from(50_u32.to_be_bytes().to_vec())),
1196 ("protocol_fees/one2zero".to_string(), Bytes::from(75_u32.to_be_bytes().to_vec())),
1197 ("fee".to_string(), Bytes::from(100_u32.to_be_bytes().to_vec())),
1198 ("ticks/-120/net_liquidity".to_string(), Bytes::from(10200_u64.to_be_bytes().to_vec())),
1199 ("ticks/120/net_liquidity".to_string(), Bytes::from(9800_u64.to_be_bytes().to_vec())),
1200 ("block_number".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1201 ("block_timestamp".to_string(), Bytes::from(1758201935_u64.to_be_bytes().to_vec())),
1202 ]
1203 .into_iter()
1204 .collect();
1205
1206 let delta = ProtocolStateDelta {
1207 component_id: "State1".to_owned(),
1208 updated_attributes: attributes,
1209 deleted_attributes: HashSet::new(),
1210 };
1211
1212 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1213 .unwrap();
1214
1215 assert_eq!(pool.liquidity, 2000);
1216 assert_eq!(pool.sqrt_price, U256::from(1001));
1217 assert_eq!(pool.tick, 120);
1218 assert_eq!(pool.fees.zero_for_one, 50);
1219 assert_eq!(pool.fees.one_for_zero, 75);
1220 assert_eq!(pool.fees.lp_fee, 100);
1221 assert_eq!(
1222 pool.ticks
1223 .get_tick(-120)
1224 .unwrap()
1225 .net_liquidity,
1226 10200
1227 );
1228 assert_eq!(
1229 pool.ticks
1230 .get_tick(120)
1231 .unwrap()
1232 .net_liquidity,
1233 9800
1234 );
1235 }
1236
1237 #[tokio::test]
1238 async fn test_swap_sim() {
1240 use tycho_client::feed::dto;
1241 let project_root = env!("CARGO_MANIFEST_DIR");
1242 let asset_path = Path::new(project_root)
1243 .join("tests/assets/decoder/uniswap_v4_snapshot_sepolia_block_7239119.json");
1244 let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1245 let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1246 let state: ComponentWithState = serde_json::from_value::<dto::ComponentWithState>(data)
1247 .expect("Expected json to match ComponentWithState structure")
1248 .into();
1249
1250 let block = BlockHeader {
1251 number: 7239119,
1252 hash: Bytes::from_str(
1253 "0x28d41d40f2ac275a4f5f621a636b9016b527d11d37d610a45ac3a821346ebf8c",
1254 )
1255 .expect("Invalid block hash"),
1256 parent_hash: Bytes::from(vec![0; 32]),
1257 ..Default::default()
1258 };
1259
1260 let t0 = Token::new(
1261 &Bytes::from_str("0x647e32181a64f4ffd4f0b0b4b052ec05b277729c").unwrap(),
1262 "T0",
1263 18,
1264 0,
1265 &[Some(10_000)],
1266 Chain::Ethereum,
1267 100,
1268 );
1269 let t1 = Token::new(
1270 &Bytes::from_str("0xe390a1c311b26f14ed0d55d3b0261c2320d15ca5").unwrap(),
1271 "T1",
1272 18,
1273 0,
1274 &[Some(10_000)],
1275 Chain::Ethereum,
1276 100,
1277 );
1278
1279 let all_tokens = [t0.clone(), t1.clone()]
1280 .iter()
1281 .map(|t| (t.address.clone(), t.clone()))
1282 .collect();
1283
1284 let usv4_state = UniswapV4State::try_from_with_header(
1285 state,
1286 block,
1287 &Default::default(),
1288 &all_tokens,
1289 &DecoderContext::new(),
1290 )
1291 .await
1292 .unwrap();
1293
1294 assert!(usv4_state.hook.is_none());
1297
1298 let res = usv4_state
1299 .get_amount_out(BigUint::from_u64(1000000000000000000).unwrap(), &t0, &t1)
1300 .unwrap();
1301
1302 let expected_amount = BigUint::from(9999909699895_u64);
1303 assert_eq!(res.amount, expected_amount);
1304 }
1305
1306 #[tokio::test]
1307 async fn test_get_limits() {
1308 use tycho_client::feed::dto;
1309 let block = BlockHeader {
1310 number: 22689129,
1311 hash: Bytes::from_str(
1312 "0x7763ea30d11aef68da729b65250c09a88ad00458c041064aad8c9a9dbf17adde",
1313 )
1314 .expect("Invalid block hash"),
1315 parent_hash: Bytes::from(vec![0; 32]),
1316 ..Default::default()
1317 };
1318
1319 let project_root = env!("CARGO_MANIFEST_DIR");
1320 let asset_path =
1321 Path::new(project_root).join("tests/assets/decoder/uniswap_v4_snapshot.json");
1322 let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1323 let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1324 let state: ComponentWithState = serde_json::from_value::<dto::ComponentWithState>(data)
1325 .expect("Expected json to match ComponentWithState structure")
1326 .into();
1327
1328 let t0 = Token::new(
1329 &Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
1330 "WBTC",
1331 8,
1332 0,
1333 &[Some(10_000)],
1334 Chain::Ethereum,
1335 100,
1336 );
1337 let t1 = Token::new(
1338 &Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
1339 "USDT",
1340 6,
1341 0,
1342 &[Some(10_000)],
1343 Chain::Ethereum,
1344 100,
1345 );
1346
1347 let all_tokens = [t0.clone(), t1.clone()]
1348 .iter()
1349 .map(|t| (t.address.clone(), t.clone()))
1350 .collect();
1351
1352 let usv4_state = UniswapV4State::try_from_with_header(
1353 state,
1354 block,
1355 &Default::default(),
1356 &all_tokens,
1357 &DecoderContext::new(),
1358 )
1359 .await
1360 .unwrap();
1361
1362 assert!(usv4_state.hook.is_none());
1365
1366 let res = usv4_state
1367 .get_limits(t0.address.clone(), t1.address.clone())
1368 .unwrap();
1369
1370 assert_eq!(&res.0, &BigUint::from_u128(71698353688830259750744466706).unwrap());
1371
1372 let out = usv4_state
1373 .get_amount_out(res.0, &t0, &t1)
1374 .expect("swap for limit in didn't work");
1375
1376 assert_eq!(&res.1, &out.amount);
1377 }
1378 type SeenCalls = Vec<(&'static str, Option<PendingOverrides>)>;
1380
1381 #[derive(Debug, Clone)]
1384 struct ConfigurableFeeHook {
1385 address: Address,
1386 pending_overrides: Option<Arc<PendingOverrides>>,
1387 seen: Arc<std::sync::Mutex<SeenCalls>>,
1388 }
1389
1390 impl ConfigurableFeeHook {
1391 fn new() -> Self {
1393 let mut address = [0u8; 20];
1394 address[19] = 0xC0;
1395 Self {
1396 address: Address::from_slice(&address),
1397 pending_overrides: None,
1398 seen: Arc::default(),
1399 }
1400 }
1401
1402 fn pending_fee(&self) -> U256 {
1403 self.pending_overrides
1404 .as_ref()
1405 .and_then(|p| p.storage.as_ref())
1406 .and_then(|storage| storage.get(&self.address))
1407 .and_then(|slots| slots.get(&U256::ZERO))
1408 .copied()
1409 .unwrap_or(U256::ZERO)
1410 }
1411 }
1412
1413 impl HookHandler for ConfigurableFeeHook {
1414 fn address(&self) -> Address {
1415 self.address
1416 }
1417
1418 fn before_swap(
1419 &self,
1420 _params: BeforeSwapParameters,
1421 _overwrites: Option<HashMap<Address, HashMap<U256, U256>>>,
1422 _transient_storage: Option<HashMap<Address, HashMap<U256, U256>>>,
1423 ) -> Result<WithGasEstimate<BeforeSwapOutput>, SimulationError> {
1424 self.seen.lock().unwrap().push((
1425 "before",
1426 self.pending_overrides
1427 .as_deref()
1428 .cloned(),
1429 ));
1430 Ok(WithGasEstimate {
1431 gas_estimate: 0,
1432 result: BeforeSwapOutput {
1433 amount_delta: BeforeSwapDelta(I256::ZERO),
1434 fee: U24::from(self.pending_fee()),
1435 overwrites: HashMap::new(),
1436 transient_storage: HashMap::new(),
1437 },
1438 })
1439 }
1440
1441 fn after_swap(
1442 &self,
1443 _params: AfterSwapParameters,
1444 _overwrites: Option<HashMap<Address, HashMap<U256, U256>>>,
1445 _transient_storage_params: Option<HashMap<Address, HashMap<U256, U256>>>,
1446 ) -> Result<WithGasEstimate<AfterSwapDelta>, SimulationError> {
1447 self.seen.lock().unwrap().push((
1448 "after",
1449 self.pending_overrides
1450 .as_deref()
1451 .cloned(),
1452 ));
1453 Ok(WithGasEstimate { gas_estimate: 0, result: I128::ZERO })
1454 }
1455
1456 fn fee(
1457 &self,
1458 _context: &UniswapV4State,
1459 _params: SwapParams,
1460 ) -> Result<f64, SimulationError> {
1461 Ok(0.0)
1462 }
1463
1464 fn spot_price(&self, _base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
1465 Err(SimulationError::RecoverableError("not implemented".into()))
1466 }
1467
1468 fn get_amount_ranges(
1469 &self,
1470 _token_in: Bytes,
1471 _token_out: Bytes,
1472 ) -> Result<AmountRanges, SimulationError> {
1473 Err(SimulationError::RecoverableError("not implemented".into()))
1474 }
1475
1476 fn set_pending_overrides(&mut self, overrides: Arc<PendingOverrides>) {
1477 self.pending_overrides = Some(overrides);
1478 }
1479
1480 fn delta_transition(
1481 &mut self,
1482 _delta: ProtocolStateDelta,
1483 _tokens: &HashMap<Bytes, Token>,
1484 _balances: &Balances,
1485 ) -> Result<(), TransitionError> {
1486 Ok(())
1487 }
1488
1489 fn clone_box(&self) -> Box<dyn HookHandler> {
1490 Box::new(self.clone())
1491 }
1492
1493 fn as_any(&self) -> &dyn Any {
1494 self
1495 }
1496
1497 fn is_equal(&self, other: &dyn HookHandler) -> bool {
1498 other
1499 .as_any()
1500 .downcast_ref::<Self>()
1501 .is_some_and(|o| o.address == self.address)
1502 }
1503 }
1504
1505 fn hooked_pool(hook: &ConfigurableFeeHook) -> UniswapV4State {
1507 let liquidity = 1_000_000_000_000_000_000u128;
1508 let mut pool = UniswapV4State::new(
1509 liquidity,
1510 U256::from(1u8) << 96,
1511 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: lp_fee::DYNAMIC_FEE_FLAG },
1512 0,
1513 60,
1514 vec![
1515 TickInfo::new(-60, liquidity as i128).unwrap(),
1516 TickInfo::new(60, -(liquidity as i128)).unwrap(),
1517 ],
1518 )
1519 .unwrap();
1520 pool.set_hook_handler(Box::new(hook.clone()));
1521 pool
1522 }
1523
1524 fn pending_fee_update(hook: &ConfigurableFeeHook, fee_pips: u64) -> PendingOverrides {
1525 PendingOverrides {
1526 storage: Some(HashMap::from([(
1527 hook.address,
1528 HashMap::from([(U256::ZERO, U256::from(fee_pips))]),
1529 )])),
1530 native_balances: None,
1531 block: None,
1532 }
1533 }
1534
1535 #[test]
1536 fn test_pending_overrides_reach_both_hook_calls_and_change_the_quote() {
1537 let hook = ConfigurableFeeHook::new();
1538 let amount_in = BigUint::from(1_000_000u64);
1539
1540 let confirmed = hooked_pool(&hook)
1541 .get_amount_out(amount_in.clone(), &token_x(), &token_y())
1542 .unwrap();
1543 let mut pool = hooked_pool(&hook);
1544 pool.set_pending_overrides(Arc::new(pending_fee_update(&hook, 100_000)))
1545 .unwrap();
1546 let pending = pool
1547 .get_amount_out(amount_in, &token_x(), &token_y())
1548 .unwrap();
1549
1550 assert!(
1551 pending.amount < confirmed.amount,
1552 "a 10% fee the pending block wrote must cut the output: {} vs {}",
1553 pending.amount,
1554 confirmed.amount
1555 );
1556 let seen = hook.seen.lock().unwrap();
1557 let calls: Vec<(&str, bool)> = seen
1558 .iter()
1559 .map(|(call, pending)| {
1560 (
1561 *call,
1562 pending
1563 .as_ref()
1564 .is_some_and(|p| p.storage.is_some()),
1565 )
1566 })
1567 .collect();
1568 assert_eq!(
1569 calls,
1570 vec![("before", false), ("after", false), ("before", true), ("after", true)],
1571 "the confirmed quote runs both calls without overrides, the pending one with them"
1572 );
1573 }
1574
1575 #[test]
1576 fn test_pending_overrides_survive_clone_box() {
1577 let hook = ConfigurableFeeHook::new();
1578 let mut pool = hooked_pool(&hook);
1579 pool.set_pending_overrides(Arc::new(pending_fee_update(&hook, 1)))
1580 .unwrap();
1581
1582 let cloned = pool.clone_box();
1583 let cloned = cloned
1584 .as_any()
1585 .downcast_ref::<UniswapV4State>()
1586 .unwrap();
1587
1588 assert!(cloned.pending_overrides().is_some(), "a clone quotes under the same overrides");
1589 }
1590
1591 #[test]
1592 fn test_set_pending_overrides_rejects_another_payload_type() {
1593 let hook = ConfigurableFeeHook::new();
1594 let mut pool = hooked_pool(&hook);
1595
1596 let result = pool.set_pending_overrides(Arc::new(0u8));
1597
1598 assert!(matches!(result, Err(SimulationError::FatalError(_))));
1599 assert!(pool.pending_overrides().is_none(), "a rejected payload sets nothing");
1600 assert!(hook.seen.lock().unwrap().is_empty());
1601 }
1602
1603 #[test]
1604 fn test_pending_overrides_are_not_part_of_equality() {
1605 let hook = ConfigurableFeeHook::new();
1606 let confirmed = hooked_pool(&hook);
1607 let mut pending = hooked_pool(&hook);
1608 pending
1609 .set_pending_overrides(Arc::new(pending_fee_update(&hook, 1)))
1610 .unwrap();
1611
1612 assert!(confirmed == pending, "PartialEq ignores the overrides");
1613 assert!(ProtocolSim::eq(&confirmed, &pending), "ProtocolSim::eq ignores the overrides");
1614 }
1615
1616 #[test]
1617 fn test_get_amount_out_no_hook() {
1618 let usv4_state = UniswapV4State::new(
1623 541501951282951892,
1624 U256::from_str("5362798333066270795901222").unwrap(), UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 100 },
1626 -192022,
1627 1,
1628 vec![
1630 TickInfo {
1631 index: -887272,
1632 net_liquidity: 460382969070005,
1633 sqrt_price: U256::from(4295128739_u64),
1634 },
1635 TickInfo {
1636 index: -207244,
1637 net_liquidity: 561268407024557,
1638 sqrt_price: U256::from_str("2505291706254206075074035").unwrap(),
1639 },
1640 TickInfo {
1641 index: -196411,
1642 net_liquidity: 825711941800452,
1643 sqrt_price: U256::from_str("4306080513146952705853399").unwrap(),
1644 },
1645 TickInfo {
1646 index: -196257,
1647 net_liquidity: 64844666874010,
1648 sqrt_price: U256::from_str("4339363644587371378270009").unwrap(),
1649 },
1650 TickInfo {
1651 index: -195611,
1652 net_liquidity: 2344045150766798,
1653 sqrt_price: U256::from_str("4481806029599743916020126").unwrap(),
1654 },
1655 TickInfo {
1656 index: -194715,
1657 net_liquidity: 391037380558274654,
1658 sqrt_price: U256::from_str("4687145946111116896040494").unwrap(),
1659 },
1660 TickInfo {
1661 index: -194599,
1662 net_liquidity: 89032603464508,
1663 sqrt_price: U256::from_str("4714409015946702405379370").unwrap(),
1664 },
1665 TickInfo {
1666 index: -194389,
1667 net_liquidity: 66635600426483168,
1668 sqrt_price: U256::from_str("4764168603367683402636621").unwrap(),
1669 },
1670 TickInfo {
1671 index: -194160,
1672 net_liquidity: 6123093436523361,
1673 sqrt_price: U256::from_str("4819029067726467394386780").unwrap(),
1674 },
1675 TickInfo {
1676 index: -194025,
1677 net_liquidity: 79940813798964,
1678 sqrt_price: U256::from_str("4851665907541490407930032").unwrap(),
1679 },
1680 TickInfo {
1681 index: -193922,
1682 net_liquidity: 415630967437234,
1683 sqrt_price: U256::from_str("4876715181040466809166531").unwrap(),
1684 },
1685 TickInfo {
1686 index: -193876,
1687 net_liquidity: 9664144015186047,
1688 sqrt_price: U256::from_str("4887943972687250473582419").unwrap(),
1689 },
1690 TickInfo {
1691 index: -193818,
1692 net_liquidity: 435344726052344,
1693 sqrt_price: U256::from_str("4902138873132735049121973").unwrap(),
1694 },
1695 TickInfo {
1696 index: -193804,
1697 net_liquidity: 221726179374067,
1698 sqrt_price: U256::from_str("4905571399964683340605904").unwrap(),
1699 },
1700 TickInfo {
1701 index: -193719,
1702 net_liquidity: 101340835774487,
1703 sqrt_price: U256::from_str("4926463397882393957462188").unwrap(),
1704 },
1705 TickInfo {
1706 index: -193690,
1707 net_liquidity: 193367475630077,
1708 sqrt_price: U256::from_str("4933611593595025190448924").unwrap(),
1709 },
1710 TickInfo {
1711 index: -193643,
1712 net_liquidity: 357016631583746,
1713 sqrt_price: U256::from_str("4945218633428068823432932").unwrap(),
1714 },
1715 TickInfo {
1716 index: -193520,
1717 net_liquidity: 917243184365178,
1718 sqrt_price: U256::from_str("4975723910367862081017120").unwrap(),
1719 },
1720 TickInfo {
1721 index: -193440,
1722 net_liquidity: 114125890211958292,
1723 sqrt_price: U256::from_str("4995665665861492533686137").unwrap(),
1724 },
1725 TickInfo {
1726 index: -193380,
1727 net_liquidity: -65980729148766579,
1728 sqrt_price: U256::from_str("5010674414300823856025303").unwrap(),
1729 },
1730 TickInfo {
1731 index: -192891,
1732 net_liquidity: 1687883551433195,
1733 sqrt_price: U256::from_str("5134689105039642314202223").unwrap(),
1734 },
1735 TickInfo {
1736 index: -192573,
1737 net_liquidity: 11108903221360975,
1738 sqrt_price: U256::from_str("5216979018647067786855495").unwrap(),
1739 },
1740 TickInfo {
1741 index: -192448,
1742 net_liquidity: 32888457482352,
1743 sqrt_price: U256::from_str("5249685603828944002327927").unwrap(),
1744 },
1745 TickInfo {
1746 index: -191525,
1747 net_liquidity: -221726179374067,
1748 sqrt_price: U256::from_str("5497623359964843320146512").unwrap(),
1749 },
1750 TickInfo {
1751 index: -191447,
1752 net_liquidity: -32888457482352,
1753 sqrt_price: U256::from_str("5519104878745833608097296").unwrap(),
1754 },
1755 TickInfo {
1756 index: -191444,
1757 net_liquidity: -114125890211958292,
1758 sqrt_price: U256::from_str("5519932765173943847315221").unwrap(),
1759 },
1760 TickInfo {
1761 index: -191417,
1762 net_liquidity: -101340835774487,
1763 sqrt_price: U256::from_str("5527389333636021285046380").unwrap(),
1764 },
1765 TickInfo {
1766 index: -191384,
1767 net_liquidity: -9664144015186047,
1768 sqrt_price: U256::from_str("5536516597603056457376182").unwrap(),
1769 },
1770 TickInfo {
1771 index: -191148,
1772 net_liquidity: -561268407024557,
1773 sqrt_price: U256::from_str("5602231161238705865493165").unwrap(),
1774 },
1775 TickInfo {
1776 index: -191147,
1777 net_liquidity: -1687883551433195,
1778 sqrt_price: U256::from_str("5602511265794328966803451").unwrap(),
1779 },
1780 TickInfo {
1781 index: -191091,
1782 net_liquidity: -89032603464508,
1783 sqrt_price: U256::from_str("5618219493196441347292357").unwrap(),
1784 },
1785 TickInfo {
1786 index: -190950,
1787 net_liquidity: -189177935487638,
1788 sqrt_price: U256::from_str("5657965894785859782969011").unwrap(),
1789 },
1790 TickInfo {
1791 index: -190756,
1792 net_liquidity: -6123093436523361,
1793 sqrt_price: U256::from_str("5713112435031881967192022").unwrap(),
1794 },
1795 TickInfo {
1796 index: -190548,
1797 net_liquidity: -193367475630077,
1798 sqrt_price: U256::from_str("5772835841671084402427710").unwrap(),
1799 },
1800 TickInfo {
1801 index: -190430,
1802 net_liquidity: -11108903221360975,
1803 sqrt_price: U256::from_str("5806994534290341208820930").unwrap(),
1804 },
1805 TickInfo {
1806 index: -190195,
1807 net_liquidity: -391583014714302569,
1808 sqrt_price: U256::from_str("5875625707132601785181387").unwrap(),
1809 },
1810 TickInfo {
1811 index: -190043,
1812 net_liquidity: -357016631583746,
1813 sqrt_price: U256::from_str("5920448331650864936739481").unwrap(),
1814 },
1815 TickInfo {
1816 index: -189779,
1817 net_liquidity: -917243184365178,
1818 sqrt_price: U256::from_str("5999112356918485175181346").unwrap(),
1819 },
1820 TickInfo {
1821 index: -189663,
1822 net_liquidity: -2344045150766798,
1823 sqrt_price: U256::from_str("6034006559279282606084981").unwrap(),
1824 },
1825 TickInfo {
1826 index: -189620,
1827 net_liquidity: -435344726052344,
1828 sqrt_price: U256::from_str("6046992979471024289177519").unwrap(),
1829 },
1830 TickInfo {
1831 index: -189409,
1832 net_liquidity: -825711941800452,
1833 sqrt_price: U256::from_str("6111123241285165242130911").unwrap(),
1834 },
1835 TickInfo {
1836 index: -189325,
1837 net_liquidity: -3947182209207,
1838 sqrt_price: U256::from_str("6136842645893819031257990").unwrap(),
1839 },
1840 TickInfo {
1841 index: -189324,
1842 net_liquidity: -415630967437234,
1843 sqrt_price: U256::from_str("6137149480355443943537284").unwrap(),
1844 },
1845 TickInfo {
1846 index: -115136,
1847 net_liquidity: 462452451821,
1848 sqrt_price: U256::from_str("250529060232794967902094762").unwrap(),
1849 },
1850 TickInfo {
1851 index: -92109,
1852 net_liquidity: -462452451821,
1853 sqrt_price: U256::from_str("792242363124136400178523925").unwrap(),
1854 },
1855 TickInfo {
1856 index: 887272,
1857 net_liquidity: -521280453734808,
1858 sqrt_price: U256::from_str("1461446703485210103287273052203988822378723970342")
1859 .unwrap(),
1860 },
1861 ],
1862 )
1863 .unwrap();
1864
1865 let t0 = usdc();
1866 let t1 = eth();
1867
1868 let out = usv4_state
1869 .get_amount_out(BigUint::from_u64(2000000).unwrap(), &t0, &t1)
1870 .unwrap();
1871
1872 assert_eq!(out.amount, BigUint::from_str("436478419853848").unwrap())
1873 }
1874
1875 #[test]
1876 fn test_get_amount_out_euler_hook() {
1877 let block = BlockHeader {
1893 number: 22689128,
1894 hash: Bytes::from_str(
1895 "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
1896 )
1897 .expect("Invalid block hash"),
1898 timestamp: 1749739055,
1899 ..Default::default()
1900 };
1901
1902 let mut usv4_state = UniswapV4State::new(
1905 0,
1906 U256::from_str("4295128740").unwrap(),
1907 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
1908 0,
1909 1,
1910 vec![],
1911 )
1912 .unwrap();
1913
1914 let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
1915 .expect("Invalid hook address");
1916
1917 let db = SimulationDB::new(
1918 get_client(None).expect("Failed to create client"),
1919 get_runtime().expect("Failed to get runtime"),
1920 Some(block.clone()),
1921 );
1922 let engine = create_engine(db, true).expect("Failed to create simulation engine");
1923 let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
1924 .expect("Invalid pool manager address");
1925
1926 let hook_handler = GenericVMHookHandler::new(
1927 hook_address,
1928 engine,
1929 pool_manager,
1930 HashMap::new(),
1931 HashMap::new(),
1932 None,
1933 true, )
1935 .unwrap();
1936
1937 let t0 = usdc();
1938 let t1 = weth();
1939
1940 usv4_state.set_hook_handler(Box::new(hook_handler));
1941 let out = usv4_state
1942 .get_amount_out(BigUint::from_u64(7407000000).unwrap(), &t0, &t1)
1943 .unwrap();
1944
1945 assert_eq!(out.amount, BigUint::from_str("2681115183499232721").unwrap())
1946 }
1947
1948 #[test]
1949 fn test_get_amount_out_angstrom_hook() {
1950 let mut usv4_state = UniswapV4State::new(
1952 66319800403673162,
1954 U256::from_str("1314588940601923011323000261788004").unwrap(),
1955 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 8388608 },
1957 194343,
1958 10,
1959 vec![
1960 TickInfo::new(-887270, 198117767801).unwrap(),
1961 TickInfo::new(191990, 24561988698695).unwrap(),
1962 TickInfo::new(192280, 2839631428751224).unwrap(),
1963 TickInfo::new(193130, 318786492813931).unwrap(),
1964 TickInfo::new(194010, 26209207141081).unwrap(),
1965 TickInfo::new(194210, -26209207141081).unwrap(),
1966 TickInfo::new(194220, 63136622375641511).unwrap(),
1967 TickInfo::new(194420, -63136622375641511).unwrap(),
1968 TickInfo::new(195130, -318786492813931).unwrap(),
1969 TickInfo::new(196330, -2839631428751224).unwrap(),
1970 TickInfo::new(197100, -24561988698695).unwrap(),
1971 TickInfo::new(887270, -198117767801).unwrap(),
1972 ],
1973 )
1974 .unwrap();
1975
1976 let fees = AngstromFees {
1977 unlock: U24::from(338),
1984 protocol_unlock: U24::from(112),
1985 };
1986 let hook_handler = AngstromHookHandler::new(
1987 Address::from_str("0x0000000aa232009084bd71a5797d089aa4edfad4").unwrap(),
1988 Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90").unwrap(),
1989 fees,
1990 false,
1991 );
1992
1993 let t0 = usdc();
1994 let t1 = weth();
1995
1996 usv4_state.set_hook_handler(Box::new(hook_handler));
1997 let out = usv4_state
1998 .get_amount_out(
1999 BigUint::from_u64(
2000 6645198144, )
2002 .unwrap(),
2003 &t0, &t1, )
2006 .unwrap();
2007
2008 assert_eq!(out.amount, BigUint::from_str("1825627051870330472").unwrap())
2009 }
2010
2011 #[derive(Debug, Clone, PartialEq, Eq)]
2013 struct AfterSwapTestHook {
2014 address: Address,
2015 delta: I128,
2016 analytic_fee_bps: Option<u32>,
2019 spot_price_override: Option<u64>,
2022 }
2023
2024 impl HookHandler for AfterSwapTestHook {
2025 fn address(&self) -> Address {
2026 self.address
2027 }
2028
2029 fn before_swap(
2030 &self,
2031 _: BeforeSwapParameters,
2032 _: Option<HashMap<Address, HashMap<U256, U256>>>,
2033 _: Option<HashMap<Address, HashMap<U256, U256>>>,
2034 ) -> Result<WithGasEstimate<BeforeSwapOutput>, SimulationError> {
2035 Err(SimulationError::RecoverableError("not implemented".into()))
2036 }
2037
2038 fn after_swap(
2039 &self,
2040 _: AfterSwapParameters,
2041 _: Option<HashMap<Address, HashMap<U256, U256>>>,
2042 _: Option<HashMap<Address, HashMap<U256, U256>>>,
2043 ) -> Result<WithGasEstimate<AfterSwapDelta>, SimulationError> {
2044 Ok(WithGasEstimate { gas_estimate: AFTER_SWAP_TEST_HOOK_GAS, result: self.delta })
2045 }
2046
2047 fn fee(&self, _: &UniswapV4State, _: SwapParams) -> Result<f64, SimulationError> {
2048 Err(SimulationError::RecoverableError("not implemented".into()))
2049 }
2050
2051 fn spot_price(&self, _: &Token, _: &Token) -> Result<f64, SimulationError> {
2052 match self.spot_price_override {
2053 Some(price) => Ok(price as f64),
2054 None => Err(SimulationError::RecoverableError("not implemented".into())),
2055 }
2056 }
2057
2058 fn unspecified_fee_amount(
2059 &self,
2060 unspecified: U256,
2061 _: bool,
2062 ) -> Result<Option<U256>, SimulationError> {
2063 let Some(bps) = self.analytic_fee_bps else { return Ok(None) };
2064 Ok(Some(unspecified * U256::from(bps) / U256::from(10_000u64)))
2065 }
2066
2067 fn get_amount_ranges(&self, _: Bytes, _: Bytes) -> Result<AmountRanges, SimulationError> {
2068 Err(SimulationError::RecoverableError("not implemented".into()))
2069 }
2070
2071 fn delta_transition(
2072 &mut self,
2073 _: ProtocolStateDelta,
2074 _: &HashMap<Bytes, Token>,
2075 _: &Balances,
2076 ) -> Result<(), TransitionError> {
2077 Ok(())
2078 }
2079
2080 fn clone_box(&self) -> Box<dyn HookHandler> {
2081 Box::new(self.clone())
2082 }
2083
2084 fn as_any(&self) -> &dyn Any {
2085 self
2086 }
2087
2088 fn is_equal(&self, other: &dyn HookHandler) -> bool {
2089 other.as_any().downcast_ref::<Self>() == Some(self)
2090 }
2091 }
2092
2093 const AFTER_SWAP_TEST_HOOK_GAS: u64 = 1_000;
2096 const AFTER_SWAP_TEST_HOOK_DELTA: u64 = 777;
2097
2098 fn construct_hook_address(hook_options: &[HookOptions]) -> Address {
2099 let mut hook_flags = U160::ZERO;
2100 let one = U160::from_limbs([1, 0, 0]);
2101 for hook_option in hook_options {
2102 hook_flags |= one << (*hook_option as u8);
2103 }
2104 Address::from(hook_flags)
2105 }
2106
2107 fn after_swap_test_hook(hook_options: &[HookOptions]) -> Box<dyn HookHandler> {
2108 Box::new(AfterSwapTestHook {
2109 address: construct_hook_address(hook_options),
2110 delta: I128::unchecked_from(AFTER_SWAP_TEST_HOOK_DELTA),
2111 analytic_fee_bps: None,
2112 spot_price_override: None,
2113 })
2114 }
2115
2116 fn analytic_fee_test_hook(analytic_fee_bps: u32) -> Box<dyn HookHandler> {
2118 Box::new(AfterSwapTestHook {
2119 address: construct_hook_address(&[
2120 HookOptions::AfterSwap,
2121 HookOptions::AfterSwapReturnsDelta,
2122 ]),
2123 delta: I128::ZERO,
2124 analytic_fee_bps: Some(analytic_fee_bps),
2125 spot_price_override: None,
2126 })
2127 }
2128
2129 const TEST_HOOK_SPOT_PRICE: u64 = 1_000_000;
2132
2133 fn pricing_test_hook(analytic_fee_bps: Option<u32>) -> Box<dyn HookHandler> {
2136 Box::new(AfterSwapTestHook {
2137 address: construct_hook_address(&[
2138 HookOptions::AfterSwap,
2139 HookOptions::AfterSwapReturnsDelta,
2140 ]),
2141 delta: I128::ZERO,
2142 analytic_fee_bps,
2143 spot_price_override: Some(TEST_HOOK_SPOT_PRICE),
2144 })
2145 }
2146
2147 fn basic_v4_test_pool_tokens(zero_for_one: bool) -> (Token, Token) {
2148 if zero_for_one {
2149 (token_x(), token_y())
2150 } else {
2151 (token_y(), token_x())
2152 }
2153 }
2154
2155 #[rstest]
2158 #[case::zero_for_one(true)]
2159 #[case::one_for_zero(false)]
2160 fn test_after_swap_delta_ignored_without_returns_delta_permission(#[case] zero_for_one: bool) {
2161 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2162 let amount_in = BigUint::from(1_000_000_000_000_000u64);
2163
2164 let hookless = create_basic_v4_test_pool()
2165 .get_amount_out(amount_in.clone(), &token_in, &token_out)
2166 .expect("hookless swap should succeed");
2167
2168 let mut pool = create_basic_v4_test_pool();
2169 pool.set_hook_handler(after_swap_test_hook(&[HookOptions::AfterSwap]));
2170 let gated = pool
2171 .get_amount_out(amount_in, &token_in, &token_out)
2172 .expect("gated swap should succeed");
2173
2174 assert_eq!(gated.amount, hookless.amount);
2175 assert_eq!(
2176 gated.gas,
2177 hookless.gas + BigUint::from(AFTER_SWAP_TEST_HOOK_GAS + PM_PER_HOOK_CALL_OVERHEAD)
2178 );
2179 }
2180
2181 #[rstest]
2184 #[case::zero_for_one(true)]
2185 #[case::one_for_zero(false)]
2186 fn test_after_swap_delta_applied_with_returns_delta_permission(#[case] zero_for_one: bool) {
2187 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2188 let amount_in = BigUint::from(1_000_000_000_000_000u64);
2189
2190 let hookless = create_basic_v4_test_pool()
2191 .get_amount_out(amount_in.clone(), &token_in, &token_out)
2192 .expect("hookless swap should succeed");
2193
2194 let mut pool = create_basic_v4_test_pool();
2195 pool.set_hook_handler(after_swap_test_hook(&[
2196 HookOptions::AfterSwap,
2197 HookOptions::AfterSwapReturnsDelta,
2198 ]));
2199 let with_delta = pool
2200 .get_amount_out(amount_in, &token_in, &token_out)
2201 .expect("swap with hook delta should succeed");
2202
2203 assert_eq!(with_delta.amount, &hookless.amount - BigUint::from(AFTER_SWAP_TEST_HOOK_DELTA));
2204 assert_eq!(
2205 with_delta.gas,
2206 hookless.gas + BigUint::from(AFTER_SWAP_TEST_HOOK_GAS + PM_PER_HOOK_CALL_OVERHEAD)
2207 );
2208 }
2209
2210 const FEELESS_POOL_LIQUIDITY: u128 = 100_000_000_000_000_000_000; fn create_feeless_v4_test_pool(liquidity: u128) -> UniswapV4State {
2216 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2217 .expect("a price of two has a square root");
2218 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("the sqrt price maps to a tick");
2219 let position = FEELESS_POOL_LIQUIDITY as i128;
2220
2221 UniswapV4State::new(
2222 liquidity,
2223 sqrt_price,
2224 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 0 },
2225 tick,
2226 60,
2227 vec![
2228 TickInfo::new(-46_080, position).unwrap(),
2229 TickInfo::new(46_080, -position).unwrap(),
2230 ],
2231 )
2232 .expect("the pool builds")
2233 }
2234
2235 fn pons_test_handler() -> PonsV2HookHandler {
2236 PonsV2HookHandler::new(PONS_V2_HOOK_ROBINHOOD, 100, 100)
2237 }
2238
2239 #[test]
2244 fn hookless_spot_price_is_the_documented_buy_price_with_lp_fee_markup() {
2245 const LP_FEE_PIPS: u32 = 3_000;
2246 const LP_FEE: f64 = LP_FEE_PIPS as f64 / 1_000_000.0;
2247 const LIQUIDITY: u128 = 100_000_000_000_000_000_000; const TOLERANCE: f64 = 1e-12;
2249
2250 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2251 .expect("a price of two has a square root");
2252 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("the sqrt price maps to a tick");
2253 let pool = UniswapV4State::new(
2254 LIQUIDITY,
2255 sqrt_price,
2256 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: LP_FEE_PIPS },
2257 tick,
2258 60,
2259 vec![
2260 TickInfo::new(-46_080, LIQUIDITY as i128).unwrap(),
2261 TickInfo::new(46_080, -(LIQUIDITY as i128)).unwrap(),
2262 ],
2263 )
2264 .expect("the pool builds");
2265
2266 assert!(pool.hook.is_none(), "the pinned values only hold with no hook handler");
2267
2268 let (t0, t1) = (token_x(), token_y());
2269 let pre_fee = sqrt_price_q96_to_f64(sqrt_price, t0.decimals, t1.decimals)
2270 .expect("the sqrt price converts to a price");
2271
2272 let expected_buy_t0 = pre_fee / (1.0 - LP_FEE);
2273 let expected_buy_t1 = (1.0 / pre_fee) / (1.0 - LP_FEE);
2274
2275 let buy_t0 = pool
2276 .spot_price(&t0, &t1)
2277 .expect("a hookless pool always prices");
2278 let buy_t1 = pool
2279 .spot_price(&t1, &t0)
2280 .expect("a hookless pool always prices");
2281
2282 assert!(
2283 (buy_t0 / expected_buy_t0 - 1.0).abs() < TOLERANCE,
2284 "buying t0 quoted {buy_t0}, expected {expected_buy_t0}"
2285 );
2286 assert!(
2287 (buy_t1 / expected_buy_t1 - 1.0).abs() < TOLERANCE,
2288 "buying t1 quoted {buy_t1}, expected {expected_buy_t1}"
2289 );
2290 }
2291
2292 #[rstest]
2296 #[case::base_is_currency0(true)]
2297 #[case::base_is_currency1(false)]
2298 fn test_spot_price_marks_up_an_analytic_hook_fee(#[case] base_is_currency0: bool) {
2299 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2300 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2301 hooked.set_hook_handler(Box::new(pons_test_handler()));
2302
2303 let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2304 .spot_price(&base, "e)
2305 .expect("a hookless pool always prices");
2306 let price = hooked
2307 .spot_price(&base, "e)
2308 .expect("the hook prices its own fee");
2309
2310 assert!(price > core, "hooked {price} is not above hookless {core}");
2311 let ratio = price / core;
2312 assert!((ratio * 0.98 - 1.0).abs() < 1e-9, "hooked/hookless is {ratio}, not 1/0.98");
2313 }
2314
2315 #[rstest]
2318 #[case::base_is_currency0(true)]
2319 #[case::base_is_currency1(false)]
2320 fn test_hooked_spot_price_matches_a_small_hooked_buy(#[case] base_is_currency0: bool) {
2321 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2322 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2323 hooked.set_hook_handler(Box::new(pons_test_handler()));
2324
2325 let quote_in = BigUint::from(100_000_000_000_000u64);
2326 let base_out = hooked
2327 .get_amount_out(quote_in.clone(), "e, &base)
2328 .expect("a tiny buy always fits the pool")
2329 .amount;
2330
2331 let executed = quote_in.to_f64().unwrap() / base_out.to_f64().unwrap();
2332 let spot = hooked
2333 .spot_price(&base, "e)
2334 .expect("the hook prices its own fee");
2335
2336 assert!((executed / spot - 1.0).abs() < 1e-4, "executed {executed}, quoted {spot}");
2337 }
2338
2339 #[rstest]
2343 #[case::base_is_currency0(true)]
2344 #[case::base_is_currency1(false)]
2345 fn test_spot_price_prefers_an_analytic_fee_over_the_hooks_own_price(
2346 #[case] base_is_currency0: bool,
2347 ) {
2348 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2349 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2350 hooked.set_hook_handler(pricing_test_hook(Some(200)));
2351
2352 let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2353 .spot_price(&base, "e)
2354 .expect("a hookless pool always prices");
2355 let price = hooked
2356 .spot_price(&base, "e)
2357 .expect("the hook prices its own fee");
2358
2359 assert_ne!(
2360 price, TEST_HOOK_SPOT_PRICE as f64,
2361 "the pool asked the hook for a price instead of marking up its own"
2362 );
2363 let ratio = price / core;
2364 assert!((ratio * 0.98 - 1.0).abs() < 1e-9, "hooked/hookless is {ratio}, not 1/0.98");
2365 }
2366
2367 #[rstest]
2371 #[case::base_is_currency0(true)]
2372 #[case::base_is_currency1(false)]
2373 fn test_spot_price_uses_the_hooks_own_price_without_an_analytic_fee(
2374 #[case] base_is_currency0: bool,
2375 ) {
2376 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2377 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2378 hooked.set_hook_handler(pricing_test_hook(None));
2379
2380 let price = hooked
2381 .spot_price(&base, "e)
2382 .expect("the hook answers with a price");
2383
2384 assert_eq!(price, TEST_HOOK_SPOT_PRICE as f64);
2385 }
2386
2387 #[rstest]
2390 #[case::base_is_currency0(true)]
2391 #[case::base_is_currency1(false)]
2392 fn test_spot_price_falls_back_to_finite_difference_without_an_analytic_fee(
2393 #[case] base_is_currency0: bool,
2394 ) {
2395 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2396 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2397 hooked.set_hook_handler(after_swap_test_hook(&[
2398 HookOptions::AfterSwap,
2399 HookOptions::AfterSwapReturnsDelta,
2400 ]));
2401
2402 let x1 = BigUint::from(10u64).pow(base.decimals) / BigUint::from(100u64);
2403 let x2 = &x1 + (&x1 / BigUint::from(100u64));
2404 let y1 = hooked
2405 .get_amount_out(x1.clone(), &base, "e)
2406 .expect("the smaller probe swap fits")
2407 .amount;
2408 let y2 = hooked
2409 .get_amount_out(x2.clone(), &base, "e)
2410 .expect("the larger probe swap fits")
2411 .amount;
2412 let slope = (&y2 - &y1).to_f64().unwrap() / (&x2 - &x1).to_f64().unwrap();
2413
2414 let price = hooked
2415 .spot_price(&base, "e)
2416 .expect("the fallback always prices");
2417
2418 assert_eq!(price, slope);
2419 let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2420 .spot_price(&base, "e)
2421 .expect("a hookless pool always prices");
2422 assert!((price / core - 1.0).abs() < 1e-3, "fallback {price} strayed from core {core}");
2423 }
2424
2425 #[rstest]
2428 #[case::zero_for_one(true)]
2429 #[case::one_for_zero(false)]
2430 fn test_get_limits_reports_the_output_net_of_an_analytic_hook_fee(#[case] zero_for_one: bool) {
2431 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2432 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2433 hooked.set_hook_handler(Box::new(pons_test_handler()));
2434
2435 let (hookless_in, hookless_out) = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2436 .get_limits(token_in.address.clone(), token_out.address.clone())
2437 .expect("a pool with liquidity has limits");
2438 let (limit_in, limit_out) = hooked
2439 .get_limits(token_in.address.clone(), token_out.address.clone())
2440 .expect("a pool with liquidity has limits");
2441
2442 assert!(hookless_out > BigUint::zero(), "the reference pool must move some output");
2443 assert_eq!(limit_in, hookless_in);
2444 let taken = pons_test_handler()
2445 .fee_and_tax(biguint_to_u256(&hookless_out))
2446 .expect("a pool sized output never overflows");
2447 assert_eq!(limit_out, &hookless_out - u256_to_biguint(taken));
2448 assert!(limit_out < hookless_out);
2449 }
2450
2451 #[rstest]
2454 #[case::zero_for_one(true)]
2455 #[case::one_for_zero(false)]
2456 fn test_get_limits_unchanged_without_an_analytic_hook_fee(#[case] zero_for_one: bool) {
2457 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2458 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2459 hooked.set_hook_handler(after_swap_test_hook(&[
2460 HookOptions::AfterSwap,
2461 HookOptions::AfterSwapReturnsDelta,
2462 ]));
2463
2464 let hookless = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2465 .get_limits(token_in.address.clone(), token_out.address.clone())
2466 .expect("a pool with liquidity has limits");
2467 let limits = hooked
2468 .get_limits(token_in.address.clone(), token_out.address.clone())
2469 .expect("a pool with liquidity has limits");
2470
2471 assert_eq!(limits, hookless);
2472 }
2473
2474 #[test]
2477 fn test_get_limits_on_a_drained_pool_is_zero_with_an_analytic_hook() {
2478 let (token_in, token_out) = basic_v4_test_pool_tokens(true);
2479 let mut drained = create_feeless_v4_test_pool(0);
2480 drained.set_hook_handler(Box::new(pons_test_handler()));
2481
2482 let limits = drained
2483 .get_limits(token_in.address, token_out.address)
2484 .expect("a drained pool reports zero rather than failing");
2485
2486 assert_eq!(limits, (BigUint::zero(), BigUint::zero()));
2487 }
2488
2489 #[rstest]
2492 #[case::the_whole_output(10_000)]
2493 #[case::more_than_the_whole_output(20_000)]
2494 fn test_spot_price_rejects_a_hook_that_takes_the_whole_output(#[case] analytic_fee_bps: u32) {
2495 let mut pool = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2496 pool.set_hook_handler(analytic_fee_test_hook(analytic_fee_bps));
2497
2498 let error = pool
2499 .spot_price(&token_x(), &token_y())
2500 .expect_err("a rate of one or more is not a price");
2501
2502 assert!(matches!(error, SimulationError::FatalError(_)), "{error:?}");
2503 }
2504
2505 #[test]
2506 fn test_spot_price_with_recoverable_error() {
2507 let usv4_state = UniswapV4State::new(
2511 1000000000000000000u128, U256::from_str("79228162514264337593543950336").unwrap(), UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 },
2514 0,
2515 60,
2516 vec![
2517 TickInfo::new(-600, 500000000000000000i128).unwrap(),
2518 TickInfo::new(600, -500000000000000000i128).unwrap(),
2519 ],
2520 )
2521 .unwrap();
2522
2523 let spot_price_result = usv4_state.spot_price(&usdc(), &weth());
2525 assert!(spot_price_result.is_ok());
2526
2527 let price = spot_price_result.unwrap();
2530 assert!(price > 0.0);
2531 }
2532
2533 #[test]
2534 fn test_get_limits_with_hook_managed_liquidity_no_ranges_entrypoint() {
2535 let block = BlockHeader {
2540 number: 22689128,
2541 hash: Bytes::from_str(
2542 "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
2543 )
2544 .expect("Invalid block hash"),
2545 timestamp: 1749739055,
2546 ..Default::default()
2547 };
2548
2549 let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
2550 .expect("Invalid hook address");
2551
2552 let db = SimulationDB::new(
2553 get_client(None).expect("Failed to create client"),
2554 get_runtime().expect("Failed to get runtime"),
2555 Some(block.clone()),
2556 );
2557 let engine = create_engine(db, true).expect("Failed to create simulation engine");
2558 let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
2559 .expect("Invalid pool manager address");
2560
2561 let hook_handler = GenericVMHookHandler::new(
2564 hook_address,
2565 engine,
2566 pool_manager,
2567 HashMap::new(),
2568 HashMap::new(),
2569 None,
2570 true, )
2572 .unwrap();
2573
2574 let mut usv4_state = UniswapV4State::new(
2576 0, U256::from_str("4295128740").unwrap(),
2578 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
2579 0, 1, vec![], )
2583 .unwrap();
2584
2585 usv4_state.set_hook_handler(Box::new(hook_handler));
2586
2587 let token_in = usdc().address;
2588 let token_out = weth().address;
2589
2590 let (amount_in_limit, amount_out_limit) = usv4_state
2591 .get_limits(token_in, token_out)
2592 .expect("Should find limits through experimental swapping");
2593
2594 assert!(amount_in_limit > BigUint::from(10u64).pow(12));
2597 assert!(amount_in_limit < BigUint::from(10u64).pow(14));
2598
2599 assert!(amount_out_limit > BigUint::from(10u64).pow(20));
2601 assert!(amount_out_limit < BigUint::from(10u64).pow(22));
2602 }
2603
2604 #[rstest]
2605 #[case::high_liquidity(u128::MAX / 2)] #[case::medium_liquidity(10000000000000000000u128)] #[case::minimal_liquidity(1000u128)] fn test_find_max_amount(#[case] liquidity: u128) {
2609 let fees = UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 };
2611 let tick_spacing = 60;
2612 let ticks = vec![
2613 TickInfo::new(-600, (liquidity / 4) as i128).unwrap(),
2614 TickInfo::new(600, -((liquidity / 4) as i128)).unwrap(),
2615 ];
2616
2617 let usv4_state = UniswapV4State::new(
2618 liquidity,
2619 U256::from_str("79228162514264337593543950336").unwrap(),
2620 fees,
2621 0,
2622 tick_spacing,
2623 ticks,
2624 )
2625 .unwrap();
2626
2627 let token_in = usdc();
2628 let token_out = weth();
2629
2630 let (max_amount_in, _max_amount_out) = usv4_state
2631 .find_max_amount(&token_in, &token_out)
2632 .unwrap();
2633
2634 let success = usv4_state
2635 .get_amount_out(max_amount_in.clone(), &token_in, &token_out)
2636 .is_ok();
2637 assert!(success, "Should be able to swap the exact max amount.");
2638
2639 let one_more = &max_amount_in + BigUint::from(1u64);
2640 let should_fail = usv4_state
2641 .get_amount_out(one_more, &token_in, &token_out)
2642 .is_err();
2643 assert!(should_fail, "Swapping max_amount + 1 should fail.");
2644 }
2645
2646 #[test]
2647 fn test_calculate_swap_fees_with_override() {
2648 let fees = UniswapV4Fees::new(100, 90, 500);
2650
2651 let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
2654 assert_eq!(total_zero_for_one, 600);
2656
2657 let total_with_override = fees.calculate_swap_fees_pips(true, Some(1000));
2659 assert_eq!(total_with_override, 1100);
2661 }
2662
2663 #[test]
2664 fn test_max_combined_fees_stays_valid() {
2665 let fees = UniswapV4Fees::new(1000, 1000, 1000);
2667 let total = fees.calculate_swap_fees_pips(true, Some(lp_fee::MAX_LP_FEE));
2668
2669 assert_eq!(total, 1_000_000);
2672 }
2673
2674 #[test]
2675 fn test_get_limits_graceful_underflow() {
2676 let usv4_state = UniswapV4State::new(
2678 1000000,
2679 U256::from_str("79228162514264337593543950336").unwrap(), UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2681 0,
2682 60,
2683 vec![
2684 TickInfo {
2687 index: -60,
2688 net_liquidity: 2000000, sqrt_price: U256::from_str("79051508376726796163471739988").unwrap(),
2690 },
2691 ],
2692 )
2693 .unwrap();
2694
2695 let usdc = usdc();
2696 let weth = weth();
2697
2698 let (limit_in, limit_out) = usv4_state
2699 .get_limits(usdc.address.clone(), weth.address.clone())
2700 .unwrap();
2701
2702 assert!(limit_in > BigUint::zero());
2704 assert!(limit_out > BigUint::zero());
2705 }
2706
2707 const MAX_PROTOCOL_FEE: u32 = 1000;
2712
2713 #[rstest]
2714 #[case::max_protocol_and_max_lp(MAX_PROTOCOL_FEE, lp_fee::MAX_LP_FEE, lp_fee::MAX_LP_FEE)]
2715 #[case::max_protocol_with_3000_lp(MAX_PROTOCOL_FEE, 3000, 3997)]
2716 #[case::max_protocol_with_zero_lp(MAX_PROTOCOL_FEE, 0, MAX_PROTOCOL_FEE)]
2717 #[case::zero_protocol_zero_lp(0, 0, 0)]
2718 #[case::zero_protocol_with_1000_lp(0, 1000, 1000)]
2719 fn test_calculate_swap_fees_uniswap_test_cases(
2720 #[case] protocol_fee: u32,
2721 #[case] lp_fee: u32,
2722 #[case] expected: u32,
2723 ) {
2724 let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
2725 let result = fees.calculate_swap_fees_pips(true, None);
2726 assert_eq!(result, expected);
2727 }
2728
2729 #[test]
2730 fn test_calculate_swap_fees_with_dynamic_fee() {
2731 let fees = UniswapV4Fees::new(100, 90, lp_fee::DYNAMIC_FEE_FLAG);
2733
2734 let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
2736 assert_eq!(total_zero_for_one, 100);
2738
2739 let total_with_override = fees.calculate_swap_fees_pips(true, Some(500));
2741 assert_eq!(total_with_override, 600);
2743 }
2744
2745 #[test]
2746 fn test_calculate_swap_fees_direction_matters() {
2747 let fees = UniswapV4Fees::new(100, 200, 500);
2749
2750 let zero_for_one_fee = fees.calculate_swap_fees_pips(true, None);
2751 assert_eq!(zero_for_one_fee, 600);
2753
2754 let one_for_zero_fee = fees.calculate_swap_fees_pips(false, None);
2755 assert_eq!(one_for_zero_fee, 700);
2757 }
2758
2759 #[rstest]
2760 #[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(
2764 #[case] protocol_fee: u32,
2765 #[case] lp_fee: u32,
2766 #[case] expected: u32,
2767 ) {
2768 let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
2771 let result = fees.calculate_swap_fees_pips(true, None);
2772 assert_eq!(result, expected, "Failed for protocol={}, lp={}", protocol_fee, lp_fee);
2773 }
2774
2775 #[test]
2776 fn test_calculate_swap_fees_override_takes_precedence() {
2777 let fees = UniswapV4Fees::new(100, 100, 3000);
2779
2780 let result = fees.calculate_swap_fees_pips(true, Some(5000));
2782 assert_eq!(result, 5100);
2784
2785 let result_no_override = fees.calculate_swap_fees_pips(true, None);
2787 assert_eq!(result_no_override, 3100);
2789 }
2790
2791 #[test]
2792 fn test_calculate_swap_fees_zero_protocol_fee() {
2793 let fees = UniswapV4Fees::new(0, 0, 3000);
2795 let result = fees.calculate_swap_fees_pips(true, None);
2796 assert_eq!(result, 3000);
2798 }
2799
2800 #[test]
2801 fn test_calculate_swap_fees_zero_lp_fee() {
2802 let fees = UniswapV4Fees::new(500, 500, 0);
2804 let result = fees.calculate_swap_fees_pips(true, None);
2805 assert_eq!(result, 500);
2807 }
2808
2809 fn create_basic_v4_test_pool() -> UniswapV4State {
2811 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))
2813 .expect("Failed to calculate sqrt price");
2814 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2815
2816 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2817
2818 UniswapV4State::new(
2819 liquidity,
2820 sqrt_price,
2821 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 }, tick,
2823 60, ticks,
2825 )
2826 .expect("Failed to create pool")
2827 }
2828
2829 fn create_tick_boundary_v4_test_pool() -> UniswapV4State {
2830 let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
2831 let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
2832
2833 UniswapV4State::new(
2834 100_000_000_000_000_000_000u128,
2835 sqrt_price,
2836 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2837 0,
2838 60,
2839 ticks,
2840 )
2841 .expect("Failed to create pool")
2842 }
2843
2844 #[test]
2845 fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
2846 let pool = create_tick_boundary_v4_test_pool();
2847 let amount = -I256::from_raw(U256::from(100_000_000_000_000_000u64));
2848
2849 let result = pool
2850 .swap(true, amount, None, None)
2851 .expect("swap should stay within the current liquidity range");
2852 let expected_tick =
2853 get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
2854
2855 assert_ne!(result.sqrt_price, pool.sqrt_price);
2856 assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
2857 assert_ne!(expected_tick, pool.tick);
2858 assert_eq!(result.tick, expected_tick);
2859 }
2860
2861 #[test]
2862 fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
2863 let mut pool = create_tick_boundary_v4_test_pool();
2864 pool.tick = -1;
2865 let amount = -I256::from_raw(U256::from(1u64));
2866
2867 let result = pool
2868 .swap(true, amount, None, None)
2869 .expect("swap should consume the input as fee without moving price");
2870
2871 assert_eq!(result.sqrt_price, pool.sqrt_price);
2872 assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
2873 assert_eq!(result.tick, pool.tick);
2874 }
2875
2876 #[test]
2877 fn test_swap_to_price_price_too_high() {
2878 let pool = create_basic_v4_test_pool();
2879
2880 let token_x = token_x();
2881 let token_y = token_y();
2882
2883 let target_price = Price::new(BigUint::from(10_000_000u64), BigUint::from(1_000_000u64));
2885
2886 let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
2887 token_x,
2888 token_y,
2889 SwapConstraint::PoolTargetPrice {
2890 target: target_price,
2891 tolerance: 0f64,
2892 min_amount_in: None,
2893 max_amount_in: None,
2894 },
2895 ));
2896 assert!(result.is_err(), "Should return error when target price is unreachable");
2897 }
2898
2899 #[test]
2900 fn test_swap_to_price_no_liquidity() {
2901 let pool = UniswapV4State::new(
2903 0, U256::from_str("79228162514264337593543950336").unwrap(),
2905 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2906 0,
2907 60,
2908 vec![],
2909 )
2910 .unwrap();
2911
2912 let token_x = token_x();
2913 let token_y = token_y();
2914
2915 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_000_000u64));
2916
2917 let pool_swap = pool.query_pool_swap(&QueryPoolSwapParams::new(
2918 token_x,
2919 token_y,
2920 SwapConstraint::PoolTargetPrice {
2921 target: target_price,
2922 tolerance: 0f64,
2923 min_amount_in: None,
2924 max_amount_in: None,
2925 },
2926 ));
2927
2928 assert!(pool_swap.is_err());
2929 }
2930
2931 #[test]
2932 fn test_swap_to_price_with_protocol_fees() {
2933 let liquidity = 100_000_000_000_000_000_000u128;
2934 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2935 .expect("Failed to calculate sqrt price");
2936 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2937
2938 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2939
2940 let pool = UniswapV4State::new(
2942 liquidity,
2943 sqrt_price,
2944 UniswapV4Fees {
2945 zero_for_one: 1000, one_for_zero: 200, lp_fee: 3000, },
2949 tick,
2950 60,
2951 ticks,
2952 )
2953 .expect("Failed to create pool");
2954
2955 let token_x = token_x();
2956 let token_y = token_y();
2957
2958 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
2962 let pool_swap_forward = pool
2963 .query_pool_swap(&QueryPoolSwapParams::new(
2964 token_x.clone(),
2965 token_y.clone(),
2966 SwapConstraint::PoolTargetPrice {
2967 target: target_price,
2968 tolerance: 0f64,
2969 min_amount_in: None,
2970 max_amount_in: None,
2971 },
2972 ))
2973 .expect("swap_to_price failed");
2974
2975 let target_price_reverse =
2977 Price::new(BigUint::from(1_010_000u64), BigUint::from(2_040_000u64));
2978 let pool_swap_backward = pool
2979 .query_pool_swap(&QueryPoolSwapParams::new(
2980 token_y,
2981 token_x,
2982 SwapConstraint::PoolTargetPrice {
2983 target: target_price_reverse,
2984 tolerance: 0f64,
2985 min_amount_in: None,
2986 max_amount_in: None,
2987 },
2988 ))
2989 .expect("swap_to_price failed");
2990
2991 assert!(
2992 pool_swap_backward.amount_out().clone() > BigUint::ZERO,
2993 "One for zero swap should return non-zero output"
2994 );
2995
2996 assert!(
2999 pool_swap_forward.amount_out() < pool_swap_backward.amount_in(),
3000 "Backward fees should be lower therefore backward swap should be bigger"
3001 );
3002 assert!(
3003 pool_swap_forward.amount_in() < pool_swap_backward.amount_out(),
3004 "Backward fees should be lower therefore backward swap should be bigger"
3005 );
3006 }
3007
3008 #[test]
3009 fn test_swap_to_price_different_targets() {
3010 let pool = create_basic_v4_test_pool();
3012
3013 let token_x = token_x();
3014 let token_y = token_y();
3015
3016 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
3019 let pool_swap_close = pool
3020 .query_pool_swap(&QueryPoolSwapParams::new(
3021 token_x.clone(),
3022 token_y.clone(),
3023 SwapConstraint::PoolTargetPrice {
3024 target: target_price,
3025 tolerance: 0f64,
3026 min_amount_in: None,
3027 max_amount_in: None,
3028 },
3029 ))
3030 .expect("swap_to_price failed");
3031 assert!(
3032 *pool_swap_close.amount_out() > BigUint::ZERO,
3033 "Expected non-zero for 1.98 Y/X target"
3034 );
3035
3036 let target_price = Price::new(BigUint::from(1_900_000u64), BigUint::from(1_000_000u64));
3038 let pool_swap_below = pool
3039 .query_pool_swap(&QueryPoolSwapParams::new(
3040 token_x.clone(),
3041 token_y.clone(),
3042 SwapConstraint::PoolTargetPrice {
3043 target: target_price,
3044 tolerance: 0f64,
3045 min_amount_in: None,
3046 max_amount_in: None,
3047 },
3048 ))
3049 .expect("swap_to_price failed");
3050 assert!(
3051 pool_swap_below.amount_out().clone() > BigUint::ZERO,
3052 "Expected non-zero for 1.90 Y/X target"
3053 );
3054
3055 let target_price = Price::new(BigUint::from(1_500_000u64), BigUint::from(1_000_000u64));
3057 let pool_swap_far = pool
3058 .query_pool_swap(&QueryPoolSwapParams::new(
3059 token_x,
3060 token_y,
3061 SwapConstraint::PoolTargetPrice {
3062 target: target_price,
3063 tolerance: 0f64,
3064 min_amount_in: None,
3065 max_amount_in: None,
3066 },
3067 ))
3068 .expect("swap_to_price failed");
3069 assert!(
3070 pool_swap_far.amount_out().clone() > BigUint::ZERO,
3071 "Expected non-zero for 1.5 Y/X target"
3072 );
3073
3074 assert!(
3076 pool_swap_close.amount_out().clone() < pool_swap_below.amount_out().clone(),
3077 "Closer target (1.98 Y/X) should require less volume than medium target (1.90 Y/X). \
3078 Got close: {}, medium: {}",
3079 pool_swap_close.amount_out().clone(),
3080 pool_swap_below.amount_out().clone()
3081 );
3082 assert!(
3083 pool_swap_below.amount_out().clone() < pool_swap_far.amount_out().clone(),
3084 "Medium target (1.90 Y/X) should require less volume than far target (1.5 Y/X). \
3085 Got medium: {}, far: {}",
3086 pool_swap_below.amount_out().clone(),
3087 pool_swap_far.amount_out().clone()
3088 );
3089 }
3090
3091 #[test]
3092 fn test_swap_to_price_around_spot_price() {
3093 let liquidity = 10_000_000_000_000_000u128;
3094 let sqrt_price =
3095 get_sqrt_price_q96(U256::from(2_000_000_000u64), U256::from(1_000_000_000u64))
3096 .expect("Failed to calculate sqrt price");
3097 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
3098
3099 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
3100
3101 let pool = UniswapV4State::new(
3103 liquidity,
3104 sqrt_price,
3105 UniswapV4Fees {
3106 zero_for_one: 0,
3107 one_for_zero: 0,
3108 lp_fee: 500, },
3110 tick,
3111 60,
3112 ticks,
3113 )
3114 .expect("Failed to create pool");
3115
3116 let token_x = token_x();
3117 let token_y = token_y();
3118
3119 let target_price = Price::new(BigUint::from(1_999_750u64), BigUint::from(1_000_250u64));
3121
3122 let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
3123 token_x.clone(),
3124 token_y.clone(),
3125 SwapConstraint::PoolTargetPrice {
3126 target: target_price,
3127 tolerance: 0f64,
3128 min_amount_in: None,
3129 max_amount_in: None,
3130 },
3131 ));
3132 assert!(result.is_err(), "Should return error when target price is unreachable");
3133
3134 let target_price = Price::new(BigUint::from(1_999_000u64), BigUint::from(1_001_000u64));
3136
3137 let pool_swap = pool
3138 .query_pool_swap(&QueryPoolSwapParams::new(
3139 token_x,
3140 token_y,
3141 SwapConstraint::PoolTargetPrice {
3142 target: target_price,
3143 tolerance: 0f64,
3144 min_amount_in: None,
3145 max_amount_in: None,
3146 },
3147 ))
3148 .expect("swap_to_price failed");
3149
3150 let expected_amount_out =
3152 BigUint::from_str("7062236922008").expect("Failed to parse expected value");
3153 assert_eq!(
3154 pool_swap.amount_out().clone(),
3155 expected_amount_out,
3156 "V4 should match V3 output with same fees (0.05%)"
3157 );
3158 }
3159
3160 #[test]
3161 fn test_swap_to_price_matches_get_amount_out() {
3162 let pool = create_basic_v4_test_pool();
3163
3164 let token_x = token_x();
3165 let token_y = token_y();
3166
3167 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
3169 let pool_swap = pool
3170 .query_pool_swap(&QueryPoolSwapParams::new(
3171 token_x.clone(),
3172 token_y.clone(),
3173 SwapConstraint::PoolTargetPrice {
3174 target: target_price,
3175 tolerance: 0f64,
3176 min_amount_in: None,
3177 max_amount_in: None,
3178 },
3179 ))
3180 .expect("swap_to_price failed");
3181 assert!(*pool_swap.amount_in() > BigUint::ZERO, "Amount in should be positive");
3182
3183 let result = pool
3185 .get_amount_out(pool_swap.amount_in().clone(), &token_x, &token_y)
3186 .expect("get_amount_out failed");
3187
3188 assert!(result.amount > BigUint::ZERO);
3191 assert!(result.amount >= *pool_swap.amount_out());
3192 }
3193
3194 #[test]
3195 fn test_swap_to_price_basic() {
3196 let liquidity = 100_000_000_000_000_000_000u128;
3197 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
3198 .expect("Failed to calculate sqrt price");
3199 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
3200
3201 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
3202
3203 let pool = UniswapV4State::new(
3204 liquidity,
3205 sqrt_price,
3206 UniswapV4Fees {
3207 zero_for_one: 0,
3208 one_for_zero: 0,
3209 lp_fee: 3000, },
3211 tick,
3212 60,
3213 ticks,
3214 )
3215 .expect("Failed to create pool");
3216
3217 let token_x = token_x();
3218 let token_y = token_y();
3219
3220 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
3222
3223 let pool_swap = pool
3224 .query_pool_swap(&QueryPoolSwapParams::new(
3225 token_x,
3226 token_y,
3227 SwapConstraint::PoolTargetPrice {
3228 target: target_price,
3229 tolerance: 0f64,
3230 min_amount_in: None,
3231 max_amount_in: None,
3232 },
3233 ))
3234 .expect("swap_to_price failed");
3235
3236 let expected_amount_in = BigUint::from_str("246739021727519745").unwrap();
3238 let expected_amount_out = BigUint::from_str("490291909043340795").unwrap();
3239
3240 assert_eq!(
3241 *pool_swap.amount_in(),
3242 expected_amount_in,
3243 "amount_in should match expected value"
3244 );
3245 assert_eq!(
3246 *pool_swap.amount_out(),
3247 expected_amount_out,
3248 "amount_out should match expected value"
3249 );
3250 }
3251
3252 #[test]
3253 fn test_swap_price_limit_out_of_range_returns_error() {
3254 let pool = create_basic_v4_test_pool();
3255 let amount = -I256::from_raw(U256::from(1000u64)); let result = pool.swap(true, amount, Some(pool.sqrt_price), None);
3259 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3260
3261 let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO), None);
3263 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3264
3265 let result = pool.swap(false, amount, Some(pool.sqrt_price), None);
3267 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3268
3269 let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO), None);
3271 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3272 }
3273
3274 #[test]
3275 fn test_swap_at_extreme_price_returns_error() {
3276 let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
3279 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
3280 let aligned_tick = (MIN_TICK / 60) * 60 + 60; let ticks = vec![
3283 TickInfo::new(aligned_tick, 0).unwrap(),
3284 TickInfo::new(aligned_tick + 60, 0).unwrap(),
3285 ];
3286 let pool = UniswapV4State::new(
3287 100_000_000_000_000_000_000u128,
3288 sqrt_price,
3289 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
3290 tick,
3291 60,
3292 ticks,
3293 )
3294 .unwrap();
3295
3296 let amount = -I256::from_raw(U256::from(1000u64));
3297 let result = pool.swap(true, amount, None, None);
3299 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3300 }
3301}