1use std::{any::Any, borrow::Cow, 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 serde::{Deserialize, Serialize};
8use tracing::trace;
9use tycho_common::{
10 dto::ProtocolStateDelta,
11 models::token::Token,
12 simulation::{
13 errors::{SimulationError, TransitionError},
14 protocol_sim::{
15 Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
16 SwapConstraint,
17 },
18 },
19 Bytes,
20};
21
22use super::hooks::utils::{has_permission, HookOptions};
23use crate::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 pool_tick::PoolTick,
41 sqrt_price_math::{get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64},
42 swap_math::{self, SwapStepResult},
43 swap_step_cache::{StepCacheKey, StepCacheRun, SwapStepCache},
44 tick_list::{TickInfo, TickList, TickListErrorKind},
45 tick_math::{
46 get_sqrt_ratio_at_tick_cached, MAX_SQRT_RATIO, MAX_TICK, MIN_SQRT_RATIO,
47 MIN_TICK,
48 },
49 StepComputation, SwapResults, SwapState,
50 },
51 },
52 vm::constants::EXTERNAL_ACCOUNT,
53 },
54 simulation::PendingOverrides,
55};
56
57const SWAP_BASE_GAS: u64 = 185_000;
60const GAS_PER_BITMAP_LOOKUP: u64 = 3_500;
63const GAS_PER_TICK: u64 = 29_000;
67const V4_CALLBACK_SETTLEMENT_GAS: u64 = 30_000;
69const PM_PER_HOOK_CALL_OVERHEAD: u64 = 25_000;
73const MAX_SWAP_GAS: u64 = 16_700_000;
75const MAX_TICKS_CROSSED: u64 = (MAX_SWAP_GAS - SWAP_BASE_GAS) / GAS_PER_TICK;
76const HOOK_FEE_PROBE_EXP: u64 = 30;
81
82#[derive(Clone, Deserialize)]
84#[serde(try_from = "UniswapV4StateData<'static>")]
85pub struct UniswapV4State {
86 liquidity: u128,
87 sqrt_price: U256,
88 fees: UniswapV4Fees,
89 tick: PoolTick,
90 ticks: Arc<TickList>,
94 tick_spacing: i32,
95 pub hook: Option<Box<dyn HookHandler>>,
96 pending_overrides: Option<Arc<PendingOverrides>>,
99 step_cache: SwapStepCache,
104}
105
106#[derive(Serialize, Deserialize)]
107struct UniswapV4StateData<'a> {
108 liquidity: u128,
109 sqrt_price: U256,
110 fees: Cow<'a, UniswapV4Fees>,
111 tick: i32,
112 tick_spacing: i32,
113 ticks: Cow<'a, [TickInfo]>,
114}
115
116impl TryFrom<UniswapV4StateData<'_>> for UniswapV4State {
117 type Error = SimulationError;
118
119 fn try_from(data: UniswapV4StateData<'_>) -> Result<Self, Self::Error> {
120 UniswapV4State::new(
121 data.liquidity,
122 data.sqrt_price,
123 data.fees.into_owned(),
124 data.tick,
125 data.tick_spacing,
126 data.ticks.into_owned(),
127 )
128 }
129}
130
131impl Serialize for UniswapV4State {
132 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
133 if self.hook.is_some() {
134 return Err(serde::ser::Error::custom(
135 "UniswapV4State with a hook cannot be serialized: HookHandler is a trait object",
136 ));
137 }
138 UniswapV4StateData {
139 liquidity: self.liquidity,
140 sqrt_price: self.sqrt_price,
141 fees: Cow::Borrowed(&self.fees),
142 tick: self.tick.value(),
143 tick_spacing: self.tick_spacing,
144 ticks: Cow::Borrowed(self.ticks.ticks()),
145 }
146 .serialize(serializer)
147 }
148}
149
150impl fmt::Debug for UniswapV4State {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 f.debug_struct("UniswapV4State")
153 .field("liquidity", &self.liquidity)
154 .field("sqrt_price", &self.sqrt_price)
155 .field("fees", &self.fees)
156 .field("tick", &self.tick)
157 .field("tick_spacing", &self.tick_spacing)
158 .field("pending_overrides", &self.pending_overrides.is_some())
159 .finish_non_exhaustive()
160 }
161}
162
163impl PartialEq for UniswapV4State {
164 fn eq(&self, other: &Self) -> bool {
165 match (&self.hook, &other.hook) {
166 (Some(a), Some(b)) => a.is_equal(&**b),
167 (None, None) => true,
168 _ => false,
169 }
170 }
171}
172
173impl Eq for UniswapV4State {}
174
175#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
176pub struct UniswapV4Fees {
177 pub zero_for_one: u32,
179 pub one_for_zero: u32,
181 pub lp_fee: u32,
183}
184
185impl UniswapV4Fees {
186 pub fn new(zero_for_one: u32, one_for_zero: u32, lp_fee: u32) -> Self {
187 Self { zero_for_one, one_for_zero, lp_fee }
188 }
189
190 fn calculate_swap_fees_pips(&self, zero_for_one: bool, lp_fee_override: Option<u32>) -> u32 {
191 let protocol_fee = if zero_for_one { self.zero_for_one } else { self.one_for_zero };
192 let lp_fee = lp_fee_override.unwrap_or_else(|| {
193 if is_dynamic(self.lp_fee) {
195 0
196 } else {
197 self.lp_fee
198 }
199 });
200
201 protocol_fee + lp_fee - ((protocol_fee as u64 * lp_fee as u64 / 1_000_000) as u32)
206 }
207}
208
209impl UniswapV4State {
210 pub fn new(
212 liquidity: u128,
213 sqrt_price: U256,
214 fees: UniswapV4Fees,
215 tick: i32,
216 tick_spacing: i32,
217 ticks: Vec<TickInfo>,
218 ) -> Result<Self, SimulationError> {
219 let tick_spacing_u16 = tick_spacing.try_into().map_err(|_| {
220 SimulationError::FatalError(format!(
223 "tick_spacing {} must be positive (int24 -> u16 conversion failed)",
224 tick_spacing
225 ))
226 })?;
227 let tick_list = TickList::from(tick_spacing_u16, ticks)?;
228 Ok(UniswapV4State {
229 liquidity,
230 sqrt_price,
231 fees,
232 tick: tick.into(),
233 ticks: Arc::new(tick_list),
234 tick_spacing,
235 hook: None,
236 pending_overrides: None,
237 step_cache: SwapStepCache::default(),
238 })
239 }
240
241 pub fn pending_overrides(&self) -> Option<&PendingOverrides> {
242 self.pending_overrides.as_deref()
243 }
244
245 fn clone_post_swap(&self, sqrt_price: U256, tick: PoolTick, liquidity: u128) -> Self {
248 let mut state = self.clone();
249 state.sqrt_price = sqrt_price;
250 state.tick = tick;
251 state.liquidity = liquidity;
252 state.step_cache = SwapStepCache::default();
253 state
254 }
255
256 fn swap(
257 &self,
258 zero_for_one: bool,
259 amount_specified: I256,
260 sqrt_price_limit: Option<U256>,
261 lp_fee_override: Option<u32>,
262 ) -> Result<SwapResults, SimulationError> {
263 if amount_specified == I256::ZERO {
264 return Ok(SwapResults {
265 amount_calculated: I256::ZERO,
266 amount_specified: I256::ZERO,
267 amount_remaining: I256::ZERO,
268 sqrt_price: self.sqrt_price,
269 liquidity: self.liquidity,
270 tick: self.tick.clone(),
271 gas_used: U256::from(3_000), });
273 }
274
275 if self.liquidity == 0 {
276 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
277 }
278 let price_limit = if let Some(limit) = sqrt_price_limit {
279 limit
280 } else if zero_for_one {
281 safe_add_u256(MIN_SQRT_RATIO, U256::from(1u64))?
282 } else {
283 safe_sub_u256(MAX_SQRT_RATIO, U256::from(1u64))?
284 };
285
286 let price_limit_valid = if zero_for_one {
287 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
288 } else {
289 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
290 };
291 if !price_limit_valid {
292 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
293 }
294
295 let exact_input = amount_specified < I256::ZERO;
296
297 let mut state = SwapState {
298 amount_remaining: amount_specified,
299 amount_calculated: I256::ZERO,
300 sqrt_price: self.sqrt_price,
301 tick: self.tick.value().into(),
302 liquidity: self.liquidity,
303 };
304 let mut gas_used = U256::from(SWAP_BASE_GAS);
305
306 let cache_key = if exact_input && sqrt_price_limit.is_none() && lp_fee_override.is_none() {
312 let fee_pips = self
313 .fees
314 .calculate_swap_fees_pips(zero_for_one, None);
315 StepCacheKey::new(
316 self.sqrt_price,
317 state.tick.value(),
318 self.liquidity,
319 fee_pips,
320 gas_used,
321 )
322 } else {
323 None
324 };
325 let mut cache_run = StepCacheRun::new(
326 &self.step_cache,
327 zero_for_one,
328 cache_key,
329 amount_specified.unsigned_abs(),
330 );
331 if let Some(progress) = cache_run.apply_cached_progress(&mut state) {
332 gas_used = progress.gas;
333 }
334
335 while state.amount_remaining != I256::ZERO && state.sqrt_price != price_limit {
336 let (mut next_tick, next_tick_info) = match self
337 .ticks
338 .next_initialized_tick_within_one_word(state.tick.value(), zero_for_one)
339 {
340 Ok((tick, info)) => {
341 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_BITMAP_LOOKUP))?;
342 (tick, info)
343 }
344 Err(tick_err) => match tick_err.kind {
345 TickListErrorKind::TicksExeeded => {
346 let new_state = self.clone_post_swap(
347 state.sqrt_price,
348 state.tick.clone(),
349 state.liquidity,
350 );
351 return Err(SimulationError::InvalidInput(
352 "Ticks exceeded".into(),
353 Some(GetAmountOutResult::new(
354 u256_to_biguint(state.amount_calculated.abs().into_raw()),
355 u256_to_biguint(gas_used),
356 Box::new(new_state),
357 )),
358 ));
359 }
360 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
361 },
362 };
363
364 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
365
366 let sqrt_price_next = match next_tick_info {
367 Some(info) => info.sqrt_price,
368 None => get_sqrt_ratio_at_tick_cached(next_tick)?,
369 };
370 let fee_pips = self
371 .fees
372 .calculate_swap_fees_pips(zero_for_one, lp_fee_override);
373
374 let sqrt_price_start = state.sqrt_price;
375 let SwapStepResult { sqrt_price, amount_in_with_fee, amount_out } =
376 swap_math::compute_swap_step(
377 state.sqrt_price,
378 UniswapV4State::get_sqrt_ratio_target(
379 sqrt_price_next,
380 price_limit,
381 zero_for_one,
382 ),
383 state.liquidity,
384 -state.amount_remaining,
388 fee_pips,
389 )?;
390 state.sqrt_price = sqrt_price;
391
392 let step = StepComputation {
393 sqrt_price_start,
394 tick_next: next_tick,
395 sqrt_price_next,
396 amount_in_with_fee,
397 amount_out,
398 };
399 if exact_input {
400 state.amount_remaining +=
401 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_in_with_fee)
402 .unwrap();
403 state.amount_calculated -=
404 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
405 } else {
406 state.amount_remaining -=
407 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
408 state.amount_calculated +=
409 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_in_with_fee)
410 .unwrap();
411 }
412 if state.sqrt_price == step.sqrt_price_next {
413 if let Some(info) = next_tick_info {
414 let liquidity_raw = info.net_liquidity;
415 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
416 state.liquidity =
417 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
418 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_TICK))?;
419 }
420 state.tick =
421 PoolTick::from(if zero_for_one { step.tick_next - 1 } else { step.tick_next });
422 } else if state.sqrt_price != step.sqrt_price_start {
423 state.tick = PoolTick::at_sqrt_price(state.sqrt_price)?;
424 }
425 cache_run.cache_progress(&step, &state, gas_used);
426 }
427
428 Ok(SwapResults {
429 amount_calculated: state.amount_calculated,
430 amount_specified,
431 amount_remaining: state.amount_remaining,
432 sqrt_price: state.sqrt_price,
433 liquidity: state.liquidity,
434 tick: state.tick,
435 gas_used: safe_add_u256(gas_used, U256::from(V4_CALLBACK_SETTLEMENT_GAS))?,
436 })
437 }
438
439 pub fn set_hook_handler(&mut self, handler: Box<dyn HookHandler>) {
440 self.hook = Some(handler);
441 }
442
443 fn get_sqrt_ratio_target(
444 sqrt_price_next: U256,
445 sqrt_price_limit: U256,
446 zero_for_one: bool,
447 ) -> U256 {
448 let cond1 = if zero_for_one {
449 sqrt_price_next < sqrt_price_limit
450 } else {
451 sqrt_price_next > sqrt_price_limit
452 };
453
454 if cond1 {
455 sqrt_price_limit
456 } else {
457 sqrt_price_next
458 }
459 }
460
461 fn find_limits_experimentally(
462 &self,
463 token_in: Bytes,
464 token_out: Bytes,
465 ) -> Result<(BigUint, BigUint), SimulationError> {
466 let token_in_obj =
469 Token::new(&token_in, "TOKEN_IN", 18, 0, &[Some(10_000)], Default::default(), 100);
470 let token_out_obj =
471 Token::new(&token_out, "TOKEN_OUT", 18, 0, &[Some(10_000)], Default::default(), 100);
472
473 self.find_max_amount(&token_in_obj, &token_out_obj)
474 }
475
476 fn find_max_amount(
489 &self,
490 token_in: &Token,
491 token_out: &Token,
492 ) -> Result<(BigUint, BigUint), SimulationError> {
493 let mut low = BigUint::from(1u64);
494
495 let mut high = BigUint::from(10u64).pow(18); let mut last_successful_amount_in = BigUint::from(1u64);
499 let mut last_successful_amount_out = BigUint::from(0u64);
500
501 while let Ok(result) = self.get_amount_out(high.clone(), token_in, token_out) {
504 low = last_successful_amount_in.clone();
507 last_successful_amount_in = high.clone();
508 last_successful_amount_out = result.amount;
509 high *= BigUint::from(10u64);
510
511 if high > BigUint::from(10u64).pow(75) {
513 return Ok((last_successful_amount_in, last_successful_amount_out));
514 }
515 }
516
517 while &high - &low > BigUint::from(1u64) {
519 let mid = (&low + &high) / BigUint::from(2u64);
520
521 match self.get_amount_out(mid.clone(), token_in, token_out) {
522 Ok(result) => {
523 last_successful_amount_in = mid.clone();
524 last_successful_amount_out = result.amount;
525 low = mid;
526 }
527 Err(_) => {
528 high = mid;
529 }
530 }
531 }
532
533 Ok((last_successful_amount_in, last_successful_amount_out))
534 }
535
536 fn has_no_initialized_ticks(&self) -> bool {
538 !self.ticks.has_initialized_ticks()
539 }
540
541 fn core_spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
544 let base_is_currency0 = base < quote;
545 let fee_pips = self
546 .fees
547 .calculate_swap_fees_pips(base_is_currency0, None);
548 let fee = fee_pips as f64 / 1_000_000.0;
549
550 let price = if base_is_currency0 {
551 sqrt_price_q96_to_f64(self.sqrt_price, base.decimals, quote.decimals)?
552 } else {
553 1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, quote.decimals, base.decimals)?
554 };
555
556 Ok(add_fee_markup(price, fee))
557 }
558
559 fn hook_fee_rate(&self, zero_for_one: bool) -> Result<Option<f64>, SimulationError> {
566 let Some(hook) = &self.hook else { return Ok(None) };
567
568 let probe = U256::from(10u64).pow(U256::from(HOOK_FEE_PROBE_EXP));
569 let Some(fee) = hook.unspecified_fee_amount(probe, zero_for_one)? else {
570 return Ok(None);
571 };
572
573 let rate = u256_to_f64(fee)? / u256_to_f64(probe)?;
574 if rate >= 1.0 {
575 return Err(SimulationError::FatalError(format!(
576 "Hook {} keeps {rate} of the output, leaving no price to quote",
577 hook.address()
578 )));
579 }
580
581 Ok(Some(rate))
582 }
583}
584
585#[typetag::serde]
586impl ProtocolSim for UniswapV4State {
587 fn set_pending_overrides(
590 &mut self,
591 overrides: Arc<dyn Any + Send + Sync>,
592 ) -> Result<(), SimulationError> {
593 let overrides = overrides
594 .downcast::<PendingOverrides>()
595 .map_err(|_| {
596 SimulationError::FatalError(
597 "Uniswap V4 pending overrides must be `PendingOverrides`".to_string(),
598 )
599 })?;
600 if let Some(hook) = &mut self.hook {
601 hook.set_pending_overrides(Arc::clone(&overrides));
602 }
603 self.pending_overrides = Some(overrides);
604 Ok(())
605 }
606
607 fn fee(&self) -> f64 {
610 todo!()
611 }
612
613 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
614 if let Some(hook) = &self.hook {
615 if let Some(rate) = self.hook_fee_rate(quote < base)? {
620 return Ok(add_fee_markup(self.core_spot_price(base, quote)?, rate));
621 }
622
623 match hook.spot_price(base, quote) {
624 Ok(price) => return Ok(price),
625 Err(SimulationError::RecoverableError(_)) => {
626 let x1 = BigUint::from(10u64).pow(base.decimals) / BigUint::from(100u64); let x2 = &x1 + (&x1 / BigUint::from(100u64));
634
635 let y1 = self.get_amount_out(x1.clone(), base, quote)?;
637 let y2 = self.get_amount_out(x2.clone(), base, quote)?;
638
639 let num = y2
641 .amount
642 .checked_sub(&y1.amount)
643 .ok_or_else(|| {
644 SimulationError::FatalError(
645 "Cannot calculate spot price: y2 < y1".to_string(),
646 )
647 })?;
648 let den = x2.checked_sub(&x1).ok_or_else(|| {
649 SimulationError::FatalError(
650 "Cannot calculate spot price: x2 < x1".to_string(),
651 )
652 })?;
653
654 if den == BigUint::from(0u64) {
655 return Err(SimulationError::FatalError(
656 "Cannot calculate spot price: denominator is zero".to_string(),
657 ));
658 }
659
660 let num_f64 = num.to_f64().ok_or_else(|| {
662 SimulationError::FatalError(
663 "Failed to convert numerator to f64".to_string(),
664 )
665 })?;
666 let den_f64 = den.to_f64().ok_or_else(|| {
667 SimulationError::FatalError(
668 "Failed to convert denominator to f64".to_string(),
669 )
670 })?;
671
672 let token_correction = 10f64.powi(base.decimals as i32 - quote.decimals as i32);
673
674 return Ok(num_f64 / den_f64 * token_correction);
675 }
676 Err(e) => return Err(e),
677 }
678 }
679
680 self.core_spot_price(base, quote)
681 }
682
683 fn get_amount_out(
684 &self,
685 amount_in: BigUint,
686 token_in: &Token,
687 token_out: &Token,
688 ) -> Result<GetAmountOutResult, SimulationError> {
689 let zero_for_one = token_in < token_out;
690 let amount_specified = I256::checked_from_sign_and_abs(
691 Sign::Negative,
692 U256::from_be_slice(&amount_in.to_bytes_be()),
693 )
694 .ok_or_else(|| {
695 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
696 })?;
697
698 let mut amount_to_swap = amount_specified;
699 let mut lp_fee_override: Option<u32> = None;
700 let mut before_swap_gas = 0u64;
701 let mut after_swap_gas = 0u64;
702 let mut before_swap_delta = BeforeSwapDelta(I256::ZERO);
703 let mut storage_overwrites = None;
704
705 let token_in_address = Address::from_slice(&token_in.address);
706 let token_out_address = Address::from_slice(&token_out.address);
707
708 let state_context = StateContext {
709 currency_0: if zero_for_one { token_in_address } else { token_out_address },
710 currency_1: if zero_for_one { token_out_address } else { token_in_address },
711 fees: self.fees.clone(),
712 tick_spacing: self.tick_spacing,
713 };
714
715 let swap_params = SwapParams {
716 zero_for_one,
717 amount_specified: amount_to_swap,
718 sqrt_price_limit: self.sqrt_price,
719 };
720
721 if let Some(ref hook) = self.hook {
723 if has_permission(hook.address(), HookOptions::BeforeSwap) {
724 let before_swap_params = BeforeSwapParameters {
725 context: state_context.clone(),
726 sender: *EXTERNAL_ACCOUNT,
727 swap_params: swap_params.clone(),
728 hook_data: Bytes::new(),
729 };
730
731 let before_swap_result = hook
732 .before_swap(before_swap_params, None, None)
733 .map_err(|e| {
734 SimulationError::FatalError(format!(
735 "BeforeSwap hook simulation failed: {e:?}"
736 ))
737 })?;
738
739 before_swap_gas = before_swap_result.gas_estimate;
740 before_swap_delta = before_swap_result.result.amount_delta;
741 storage_overwrites = Some(before_swap_result.result.overwrites);
742
743 if before_swap_delta.as_i256() != I256::ZERO {
746 amount_to_swap += I256::from(before_swap_delta.get_specified_delta());
747 if amount_to_swap > I256::ZERO {
748 return Err(SimulationError::FatalError(
749 "Hook delta exceeds swap amount".into(),
750 ));
751 }
752 }
753
754 let hook_fee = before_swap_result
759 .result
760 .fee
761 .to::<u32>();
762 if hook_fee != 0 {
763 let cleaned_fee = lp_fee::remove_override_flag(hook_fee);
765
766 if !lp_fee::is_valid(cleaned_fee) {
768 return Err(SimulationError::FatalError(format!(
769 "LP fee override {} exceeds maximum {} pips",
770 cleaned_fee,
771 lp_fee::MAX_LP_FEE
772 )));
773 }
774
775 lp_fee_override = Some(cleaned_fee);
776 }
777 }
778 }
779
780 let result = self.swap(zero_for_one, amount_to_swap, None, lp_fee_override)?;
782
783 let mut swap_delta = BalanceDelta::from_swap_result(result.amount_calculated, zero_for_one);
785
786 let hook_delta_specified = before_swap_delta.get_specified_delta();
789 let mut hook_delta_unspecified = before_swap_delta.get_unspecified_delta();
790
791 if let Some(ref hook) = self.hook {
792 if has_permission(hook.address(), HookOptions::AfterSwap) {
793 let after_swap_params = AfterSwapParameters {
794 context: state_context,
795 sender: *EXTERNAL_ACCOUNT,
796 swap_params,
797 delta: swap_delta,
798 hook_data: Bytes::new(),
799 };
800
801 let after_swap_result = hook
802 .after_swap(after_swap_params, storage_overwrites, None)
803 .map_err(|e| {
804 SimulationError::FatalError(format!(
805 "AfterSwap hook simulation failed: {e:?}"
806 ))
807 })?;
808 after_swap_gas = after_swap_result.gas_estimate;
809 if has_permission(hook.address(), HookOptions::AfterSwapReturnsDelta) {
814 hook_delta_unspecified += after_swap_result.result;
815 }
816 }
817 }
818
819 if (hook_delta_specified != I128::ZERO) || (hook_delta_unspecified != I128::ZERO) {
822 let hook_delta = if (amount_specified < I256::ZERO) == zero_for_one {
823 BalanceDelta::new(hook_delta_specified, hook_delta_unspecified)
824 } else {
825 BalanceDelta::new(hook_delta_unspecified, hook_delta_specified)
826 };
827 swap_delta = swap_delta - hook_delta
829 }
830
831 let amount_out = if (amount_specified < I256::ZERO) == zero_for_one {
832 swap_delta.amount1()
833 } else {
834 swap_delta.amount0()
835 };
836
837 trace!(?amount_in, ?token_in, ?token_out, ?zero_for_one, ?result, "V4 SWAP");
838 let new_state = self.clone_post_swap(result.sqrt_price, result.tick, result.liquidity);
839
840 let mut hook_overhead = before_swap_gas + after_swap_gas;
846 if before_swap_gas > 0 {
847 hook_overhead += PM_PER_HOOK_CALL_OVERHEAD;
848 }
849 if after_swap_gas > 0 {
850 hook_overhead += PM_PER_HOOK_CALL_OVERHEAD;
851 }
852 let total_gas_used = result.gas_used + U256::from(hook_overhead);
853 Ok(GetAmountOutResult::new(
854 u256_to_biguint(U256::from(amount_out.abs())),
855 u256_to_biguint(total_gas_used),
856 Box::new(new_state),
857 ))
858 }
859
860 fn get_limits(
861 &self,
862 token_in: Bytes,
863 token_out: Bytes,
864 ) -> Result<(BigUint, BigUint), SimulationError> {
865 if let Some(hook) = &self.hook {
866 if self.liquidity == 0 && self.has_no_initialized_ticks() {
868 match hook.get_amount_ranges(token_in.clone(), token_out.clone()) {
870 Ok(amount_ranges) => {
871 return Ok((
872 u256_to_biguint(amount_ranges.amount_in_range.1),
873 u256_to_biguint(amount_ranges.amount_out_range.1),
874 ))
875 }
876 Err(SimulationError::RecoverableError(msg))
879 if msg.contains("not implemented") || msg.contains("not set") =>
880 {
881 return self.find_limits_experimentally(token_in, token_out);
884 }
886 Err(e) => return Err(e),
887 }
888 }
889 }
890
891 if self.liquidity == 0 {
893 return Ok((BigUint::zero(), BigUint::zero()));
894 }
895
896 let zero_for_one = token_in < token_out;
897 let mut current_tick = self.tick.value();
898 let mut current_sqrt_price = self.sqrt_price;
899 let mut current_liquidity = self.liquidity;
900 let mut total_amount_in = U256::ZERO;
901 let mut total_amount_out = U256::ZERO;
902 let mut ticks_crossed: u64 = 0;
903
904 while let Ok((tick, next_tick_info)) = self
907 .ticks
908 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
909 {
910 if ticks_crossed >= MAX_TICKS_CROSSED {
912 break;
913 }
914 ticks_crossed += 1;
915
916 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
918
919 let sqrt_price_next = match next_tick_info {
920 Some(info) => info.sqrt_price,
921 None => get_sqrt_ratio_at_tick_cached(next_tick)?,
922 };
923
924 let (amount_in, amount_out) = if zero_for_one {
927 let amount0 = get_amount0_delta(
928 sqrt_price_next,
929 current_sqrt_price,
930 current_liquidity,
931 true,
932 )?;
933 let amount1 = get_amount1_delta(
934 sqrt_price_next,
935 current_sqrt_price,
936 current_liquidity,
937 false,
938 )?;
939 (amount0, amount1)
940 } else {
941 let amount0 = get_amount0_delta(
942 sqrt_price_next,
943 current_sqrt_price,
944 current_liquidity,
945 false,
946 )?;
947 let amount1 = get_amount1_delta(
948 sqrt_price_next,
949 current_sqrt_price,
950 current_liquidity,
951 true,
952 )?;
953 (amount1, amount0)
954 };
955
956 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
958 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
959
960 if let Some(info) = next_tick_info {
965 let liquidity_raw = info.net_liquidity;
966 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
967
968 match liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta) {
971 Ok(new_liquidity) => {
972 current_liquidity = new_liquidity;
973 }
974 Err(_) => {
975 break;
978 }
979 }
980 }
981
982 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
984 current_sqrt_price = sqrt_price_next;
985
986 if current_liquidity == 0 {
988 break;
989 }
990 }
991
992 if let Some(hook) = &self.hook {
995 if let Some(fee) = hook.unspecified_fee_amount(total_amount_out, zero_for_one)? {
996 total_amount_out = safe_sub_u256(total_amount_out, fee)?;
997 }
998 }
999
1000 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
1001 }
1002
1003 fn delta_transition(
1004 &mut self,
1005 delta: ProtocolStateDelta,
1006 tokens: &HashMap<Bytes, Token>,
1007 balances: &Balances,
1008 ) -> Result<(), TransitionError> {
1009 self.step_cache = SwapStepCache::default();
1011 if let Some(mut hook) = self.hook.clone() {
1012 match hook.delta_transition(delta.clone(), tokens, balances) {
1013 Ok(()) => self.set_hook_handler(hook),
1014 Err(TransitionError::SimulationError(SimulationError::RecoverableError(msg)))
1015 if msg.contains("not implemented") =>
1016 {
1017 }
1019 Err(e) => return Err(e),
1020 }
1021 }
1022
1023 if let Some(liquidity) = delta
1025 .updated_attributes
1026 .get("liquidity")
1027 {
1028 self.liquidity = u128::from(liquidity.clone());
1029 }
1030 if let Some(sqrt_price) = delta
1031 .updated_attributes
1032 .get("sqrt_price_x96")
1033 {
1034 self.sqrt_price = U256::from_be_slice(sqrt_price);
1035 }
1036 if let Some(tick) = delta.updated_attributes.get("tick") {
1037 self.tick = i24_be_bytes_to_i32(tick).into();
1038 }
1039 if let Some(lp_fee) = delta.updated_attributes.get("fee") {
1040 self.fees.lp_fee = u32::from(lp_fee.clone());
1041 }
1042 if let Some(zero2one_protocol_fee) = delta
1043 .updated_attributes
1044 .get("protocol_fees/zero2one")
1045 {
1046 self.fees.zero_for_one = u32::from(zero2one_protocol_fee.clone());
1047 }
1048 if let Some(one2zero_protocol_fee) = delta
1049 .updated_attributes
1050 .get("protocol_fees/one2zero")
1051 {
1052 self.fees.one_for_zero = u32::from(one2zero_protocol_fee.clone());
1053 }
1054
1055 for (key, value) in delta.updated_attributes.iter() {
1057 if key.starts_with("ticks/") {
1059 let parts: Vec<&str> = key.split('/').collect();
1060 Arc::make_mut(&mut self.ticks)
1061 .set_tick_liquidity(
1062 parts[1]
1063 .parse::<i32>()
1064 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
1065 i128::from(value.clone()),
1066 )
1067 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
1068 }
1069 }
1070 for key in delta.deleted_attributes.iter() {
1072 if key.starts_with("ticks/") {
1074 let parts: Vec<&str> = key.split('/').collect();
1075 Arc::make_mut(&mut self.ticks)
1076 .set_tick_liquidity(
1077 parts[1]
1078 .parse::<i32>()
1079 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
1080 0,
1081 )
1082 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
1083 }
1084 }
1085
1086 Ok(())
1087 }
1088
1089 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
1098 if self.liquidity == 0 {
1099 return Err(SimulationError::FatalError("No liquidity".to_string()));
1100 }
1101
1102 let zero_for_one = params.token_in().address < params.token_out().address;
1104 let fee_pips = self
1105 .fees
1106 .calculate_swap_fees_pips(zero_for_one, None);
1107
1108 match params.swap_constraint() {
1109 SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
1110 "Uniswap V4 does not support TradeLimitPrice constraint in query_pool_swap"
1111 .to_string(),
1112 None,
1113 )),
1114 SwapConstraint::PoolTargetPrice {
1115 target,
1116 tolerance: _,
1117 min_amount_in: _,
1118 max_amount_in: _,
1119 } => {
1120 if self.liquidity == 0 {
1121 return Err(SimulationError::FatalError("No liquidity".to_string()));
1122 }
1123
1124 let (amount_in, amount_out, swap_result) = clmm_swap_to_price(
1125 self.sqrt_price,
1126 ¶ms.token_in().address,
1127 ¶ms.token_out().address,
1128 target,
1129 fee_pips,
1130 Sign::Negative, |zero_for_one, amount_specified, sqrt_price_limit| {
1132 self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit), None)
1133 },
1134 )?;
1135
1136 let new_state = self.clone_post_swap(
1137 swap_result.sqrt_price,
1138 swap_result.tick,
1139 swap_result.liquidity,
1140 );
1141
1142 Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
1143 }
1144 }
1145 }
1146
1147 fn clone_box(&self) -> Box<dyn ProtocolSim> {
1148 Box::new(self.clone())
1149 }
1150
1151 fn as_any(&self) -> &dyn Any {
1152 self
1153 }
1154
1155 fn as_any_mut(&mut self) -> &mut dyn Any {
1156 self
1157 }
1158
1159 fn eq(&self, other: &dyn ProtocolSim) -> bool {
1160 if let Some(other_state) = other
1161 .as_any()
1162 .downcast_ref::<UniswapV4State>()
1163 {
1164 self.liquidity == other_state.liquidity &&
1165 self.sqrt_price == other_state.sqrt_price &&
1166 self.fees == other_state.fees &&
1167 self.tick == other_state.tick &&
1168 self.ticks == other_state.ticks
1169 } else {
1170 false
1171 }
1172 }
1173}
1174
1175#[cfg(test)]
1176mod tests {
1177 use std::{collections::HashSet, fs, path::Path, str::FromStr};
1178
1179 use alloy::primitives::{aliases::U24, U160};
1180 use num_traits::FromPrimitive;
1181 use rstest::rstest;
1182 use serde_json::Value;
1183 use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
1184 use tycho_common::{models::Chain, simulation::protocol_sim::Price};
1185
1186 use super::*;
1187 use crate::{
1188 evm::{
1189 engine_db::{
1190 create_engine,
1191 simulation_db::SimulationDB,
1192 utils::{get_client, get_runtime},
1193 },
1194 protocol::{
1195 u256_num::biguint_to_u256,
1196 uniswap_v4::hooks::{
1197 angstrom::hook_handler::{AngstromFees, AngstromHookHandler},
1198 generic_vm_hook_handler::GenericVMHookHandler,
1199 models::{AfterSwapDelta, AmountRanges, BeforeSwapOutput, WithGasEstimate},
1200 pons_v2::hook_handler::{PonsV2HookHandler, PONS_V2_HOOK_ROBINHOOD},
1201 },
1202 utils::uniswap::{
1203 lp_fee,
1204 sqrt_price_math::get_sqrt_price_q96,
1205 tick_math::{get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio},
1206 },
1207 },
1208 },
1209 protocol::models::{DecoderContext, TryFromWithBlock},
1210 };
1211
1212 fn usdc() -> Token {
1214 Token::new(
1215 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
1216 "USDC",
1217 6,
1218 0,
1219 &[Some(10_000)],
1220 Default::default(),
1221 100,
1222 )
1223 }
1224
1225 fn weth() -> Token {
1226 Token::new(
1227 &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
1228 "WETH",
1229 18,
1230 0,
1231 &[Some(10_000)],
1232 Default::default(),
1233 100,
1234 )
1235 }
1236
1237 fn eth() -> Token {
1238 Token::new(
1239 &Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
1240 "ETH",
1241 18,
1242 0,
1243 &[Some(10_000)],
1244 Default::default(),
1245 100,
1246 )
1247 }
1248
1249 fn token_x() -> Token {
1250 Token::new(
1251 &Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap(),
1252 "X",
1253 18,
1254 0,
1255 &[Some(10_000)],
1256 Default::default(),
1257 100,
1258 )
1259 }
1260
1261 fn token_y() -> Token {
1262 Token::new(
1263 &Bytes::from_str("0x0000000000000000000000000000000000000002").unwrap(),
1264 "Y",
1265 18,
1266 0,
1267 &[Some(10_000)],
1268 Default::default(),
1269 100,
1270 )
1271 }
1272
1273 #[test]
1274 fn test_delta_transition() {
1275 let mut pool = UniswapV4State::new(
1276 1000,
1277 U256::from_str("1000").unwrap(),
1278 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 700 },
1279 100,
1280 60,
1281 vec![TickInfo::new(120, 10000).unwrap(), TickInfo::new(180, -10000).unwrap()],
1282 )
1283 .unwrap();
1284
1285 let attributes: HashMap<String, Bytes> = [
1286 ("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1287 ("sqrt_price_x96".to_string(), Bytes::from(1001_u64.to_be_bytes().to_vec())),
1288 ("tick".to_string(), Bytes::from(120_i32.to_be_bytes().to_vec())),
1289 ("protocol_fees/zero2one".to_string(), Bytes::from(50_u32.to_be_bytes().to_vec())),
1290 ("protocol_fees/one2zero".to_string(), Bytes::from(75_u32.to_be_bytes().to_vec())),
1291 ("fee".to_string(), Bytes::from(100_u32.to_be_bytes().to_vec())),
1292 ("ticks/-120/net_liquidity".to_string(), Bytes::from(10200_u64.to_be_bytes().to_vec())),
1293 ("ticks/120/net_liquidity".to_string(), Bytes::from(9800_u64.to_be_bytes().to_vec())),
1294 ("block_number".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
1295 ("block_timestamp".to_string(), Bytes::from(1758201935_u64.to_be_bytes().to_vec())),
1296 ]
1297 .into_iter()
1298 .collect();
1299
1300 let delta = ProtocolStateDelta {
1301 component_id: "State1".to_owned(),
1302 updated_attributes: attributes,
1303 deleted_attributes: HashSet::new(),
1304 };
1305
1306 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
1307 .unwrap();
1308
1309 assert_eq!(pool.liquidity, 2000);
1310 assert_eq!(pool.sqrt_price, U256::from(1001));
1311 assert_eq!(pool.tick.value(), 120);
1312 assert_eq!(pool.fees.zero_for_one, 50);
1313 assert_eq!(pool.fees.one_for_zero, 75);
1314 assert_eq!(pool.fees.lp_fee, 100);
1315 assert_eq!(
1316 pool.ticks
1317 .get_tick(-120)
1318 .unwrap()
1319 .net_liquidity,
1320 10200
1321 );
1322 assert_eq!(
1323 pool.ticks
1324 .get_tick(120)
1325 .unwrap()
1326 .net_liquidity,
1327 9800
1328 );
1329 }
1330
1331 #[tokio::test]
1332 async fn test_swap_sim() {
1334 use tycho_client::feed::dto;
1335 let project_root = env!("CARGO_MANIFEST_DIR");
1336 let asset_path = Path::new(project_root)
1337 .join("tests/assets/decoder/uniswap_v4_snapshot_sepolia_block_7239119.json");
1338 let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1339 let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1340 let state: ComponentWithState = serde_json::from_value::<dto::ComponentWithState>(data)
1341 .expect("Expected json to match ComponentWithState structure")
1342 .into();
1343
1344 let block = BlockHeader {
1345 number: 7239119,
1346 hash: Bytes::from_str(
1347 "0x28d41d40f2ac275a4f5f621a636b9016b527d11d37d610a45ac3a821346ebf8c",
1348 )
1349 .expect("Invalid block hash"),
1350 parent_hash: Bytes::from(vec![0; 32]),
1351 ..Default::default()
1352 };
1353
1354 let t0 = Token::new(
1355 &Bytes::from_str("0x647e32181a64f4ffd4f0b0b4b052ec05b277729c").unwrap(),
1356 "T0",
1357 18,
1358 0,
1359 &[Some(10_000)],
1360 Chain::Ethereum,
1361 100,
1362 );
1363 let t1 = Token::new(
1364 &Bytes::from_str("0xe390a1c311b26f14ed0d55d3b0261c2320d15ca5").unwrap(),
1365 "T1",
1366 18,
1367 0,
1368 &[Some(10_000)],
1369 Chain::Ethereum,
1370 100,
1371 );
1372
1373 let all_tokens = [t0.clone(), t1.clone()]
1374 .iter()
1375 .map(|t| (t.address.clone(), t.clone()))
1376 .collect();
1377
1378 let usv4_state = UniswapV4State::try_from_with_header(
1379 state,
1380 block,
1381 &Default::default(),
1382 &all_tokens,
1383 &DecoderContext::new(),
1384 )
1385 .await
1386 .unwrap();
1387
1388 assert!(usv4_state.hook.is_none());
1391
1392 let res = usv4_state
1393 .get_amount_out(BigUint::from_u64(1000000000000000000).unwrap(), &t0, &t1)
1394 .unwrap();
1395
1396 let expected_amount = BigUint::from(9999909699895_u64);
1397 assert_eq!(res.amount, expected_amount);
1398 }
1399
1400 #[tokio::test]
1401 async fn test_get_limits() {
1402 use tycho_client::feed::dto;
1403 let block = BlockHeader {
1404 number: 22689129,
1405 hash: Bytes::from_str(
1406 "0x7763ea30d11aef68da729b65250c09a88ad00458c041064aad8c9a9dbf17adde",
1407 )
1408 .expect("Invalid block hash"),
1409 parent_hash: Bytes::from(vec![0; 32]),
1410 ..Default::default()
1411 };
1412
1413 let project_root = env!("CARGO_MANIFEST_DIR");
1414 let asset_path =
1415 Path::new(project_root).join("tests/assets/decoder/uniswap_v4_snapshot.json");
1416 let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1417 let data: Value = serde_json::from_str(&json_data).expect("Failed to parse JSON");
1418 let state: ComponentWithState = serde_json::from_value::<dto::ComponentWithState>(data)
1419 .expect("Expected json to match ComponentWithState structure")
1420 .into();
1421
1422 let t0 = Token::new(
1423 &Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(),
1424 "WBTC",
1425 8,
1426 0,
1427 &[Some(10_000)],
1428 Chain::Ethereum,
1429 100,
1430 );
1431 let t1 = Token::new(
1432 &Bytes::from_str("0xdac17f958d2ee523a2206206994597c13d831ec7").unwrap(),
1433 "USDT",
1434 6,
1435 0,
1436 &[Some(10_000)],
1437 Chain::Ethereum,
1438 100,
1439 );
1440
1441 let all_tokens = [t0.clone(), t1.clone()]
1442 .iter()
1443 .map(|t| (t.address.clone(), t.clone()))
1444 .collect();
1445
1446 let usv4_state = UniswapV4State::try_from_with_header(
1447 state,
1448 block,
1449 &Default::default(),
1450 &all_tokens,
1451 &DecoderContext::new(),
1452 )
1453 .await
1454 .unwrap();
1455
1456 assert!(usv4_state.hook.is_none());
1459
1460 let res = usv4_state
1461 .get_limits(t0.address.clone(), t1.address.clone())
1462 .unwrap();
1463
1464 assert_eq!(&res.0, &BigUint::from_u128(71698353688830259750744466706).unwrap());
1465
1466 let out = usv4_state
1467 .get_amount_out(res.0, &t0, &t1)
1468 .expect("swap for limit in didn't work");
1469
1470 assert_eq!(&res.1, &out.amount);
1471 }
1472 type SeenCalls = Vec<(&'static str, Option<PendingOverrides>)>;
1474
1475 #[derive(Debug, Clone)]
1478 struct ConfigurableFeeHook {
1479 address: Address,
1480 pending_overrides: Option<Arc<PendingOverrides>>,
1481 seen: Arc<std::sync::Mutex<SeenCalls>>,
1482 }
1483
1484 impl ConfigurableFeeHook {
1485 fn new() -> Self {
1487 let mut address = [0u8; 20];
1488 address[19] = 0xC0;
1489 Self {
1490 address: Address::from_slice(&address),
1491 pending_overrides: None,
1492 seen: Arc::default(),
1493 }
1494 }
1495
1496 fn pending_fee(&self) -> U256 {
1497 self.pending_overrides
1498 .as_ref()
1499 .and_then(|p| p.storage.as_ref())
1500 .and_then(|storage| storage.get(&self.address))
1501 .and_then(|slots| slots.get(&U256::ZERO))
1502 .copied()
1503 .unwrap_or(U256::ZERO)
1504 }
1505 }
1506
1507 impl HookHandler for ConfigurableFeeHook {
1508 fn address(&self) -> Address {
1509 self.address
1510 }
1511
1512 fn before_swap(
1513 &self,
1514 _params: BeforeSwapParameters,
1515 _overwrites: Option<HashMap<Address, HashMap<U256, U256>>>,
1516 _transient_storage: Option<HashMap<Address, HashMap<U256, U256>>>,
1517 ) -> Result<WithGasEstimate<BeforeSwapOutput>, SimulationError> {
1518 self.seen.lock().unwrap().push((
1519 "before",
1520 self.pending_overrides
1521 .as_deref()
1522 .cloned(),
1523 ));
1524 Ok(WithGasEstimate {
1525 gas_estimate: 0,
1526 result: BeforeSwapOutput {
1527 amount_delta: BeforeSwapDelta(I256::ZERO),
1528 fee: U24::from(self.pending_fee()),
1529 overwrites: HashMap::new(),
1530 transient_storage: HashMap::new(),
1531 },
1532 })
1533 }
1534
1535 fn after_swap(
1536 &self,
1537 _params: AfterSwapParameters,
1538 _overwrites: Option<HashMap<Address, HashMap<U256, U256>>>,
1539 _transient_storage_params: Option<HashMap<Address, HashMap<U256, U256>>>,
1540 ) -> Result<WithGasEstimate<AfterSwapDelta>, SimulationError> {
1541 self.seen.lock().unwrap().push((
1542 "after",
1543 self.pending_overrides
1544 .as_deref()
1545 .cloned(),
1546 ));
1547 Ok(WithGasEstimate { gas_estimate: 0, result: I128::ZERO })
1548 }
1549
1550 fn fee(
1551 &self,
1552 _context: &UniswapV4State,
1553 _params: SwapParams,
1554 ) -> Result<f64, SimulationError> {
1555 Ok(0.0)
1556 }
1557
1558 fn spot_price(&self, _base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
1559 Err(SimulationError::RecoverableError("not implemented".into()))
1560 }
1561
1562 fn get_amount_ranges(
1563 &self,
1564 _token_in: Bytes,
1565 _token_out: Bytes,
1566 ) -> Result<AmountRanges, SimulationError> {
1567 Err(SimulationError::RecoverableError("not implemented".into()))
1568 }
1569
1570 fn set_pending_overrides(&mut self, overrides: Arc<PendingOverrides>) {
1571 self.pending_overrides = Some(overrides);
1572 }
1573
1574 fn delta_transition(
1575 &mut self,
1576 _delta: ProtocolStateDelta,
1577 _tokens: &HashMap<Bytes, Token>,
1578 _balances: &Balances,
1579 ) -> Result<(), TransitionError> {
1580 Ok(())
1581 }
1582
1583 fn clone_box(&self) -> Box<dyn HookHandler> {
1584 Box::new(self.clone())
1585 }
1586
1587 fn as_any(&self) -> &dyn Any {
1588 self
1589 }
1590
1591 fn is_equal(&self, other: &dyn HookHandler) -> bool {
1592 other
1593 .as_any()
1594 .downcast_ref::<Self>()
1595 .is_some_and(|o| o.address == self.address)
1596 }
1597 }
1598
1599 fn hooked_pool(hook: &ConfigurableFeeHook) -> UniswapV4State {
1601 let liquidity = 1_000_000_000_000_000_000u128;
1602 let mut pool = UniswapV4State::new(
1603 liquidity,
1604 U256::from(1u8) << 96,
1605 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: lp_fee::DYNAMIC_FEE_FLAG },
1606 0,
1607 60,
1608 vec![
1609 TickInfo::new(-60, liquidity as i128).unwrap(),
1610 TickInfo::new(60, -(liquidity as i128)).unwrap(),
1611 ],
1612 )
1613 .unwrap();
1614 pool.set_hook_handler(Box::new(hook.clone()));
1615 pool
1616 }
1617
1618 fn pending_fee_update(hook: &ConfigurableFeeHook, fee_pips: u64) -> PendingOverrides {
1619 PendingOverrides {
1620 storage: Some(HashMap::from([(
1621 hook.address,
1622 HashMap::from([(U256::ZERO, U256::from(fee_pips))]),
1623 )])),
1624 native_balances: None,
1625 block: None,
1626 }
1627 }
1628
1629 #[test]
1630 fn test_pending_overrides_reach_both_hook_calls_and_change_the_quote() {
1631 let hook = ConfigurableFeeHook::new();
1632 let amount_in = BigUint::from(1_000_000u64);
1633
1634 let confirmed = hooked_pool(&hook)
1635 .get_amount_out(amount_in.clone(), &token_x(), &token_y())
1636 .unwrap();
1637 let mut pool = hooked_pool(&hook);
1638 pool.set_pending_overrides(Arc::new(pending_fee_update(&hook, 100_000)))
1639 .unwrap();
1640 let pending = pool
1641 .get_amount_out(amount_in, &token_x(), &token_y())
1642 .unwrap();
1643
1644 assert!(
1645 pending.amount < confirmed.amount,
1646 "a 10% fee the pending block wrote must cut the output: {} vs {}",
1647 pending.amount,
1648 confirmed.amount
1649 );
1650 let seen = hook.seen.lock().unwrap();
1651 let calls: Vec<(&str, bool)> = seen
1652 .iter()
1653 .map(|(call, pending)| {
1654 (
1655 *call,
1656 pending
1657 .as_ref()
1658 .is_some_and(|p| p.storage.is_some()),
1659 )
1660 })
1661 .collect();
1662 assert_eq!(
1663 calls,
1664 vec![("before", false), ("after", false), ("before", true), ("after", true)],
1665 "the confirmed quote runs both calls without overrides, the pending one with them"
1666 );
1667 }
1668
1669 #[test]
1670 fn test_pending_overrides_survive_clone_box() {
1671 let hook = ConfigurableFeeHook::new();
1672 let mut pool = hooked_pool(&hook);
1673 pool.set_pending_overrides(Arc::new(pending_fee_update(&hook, 1)))
1674 .unwrap();
1675
1676 let cloned = pool.clone_box();
1677 let cloned = cloned
1678 .as_any()
1679 .downcast_ref::<UniswapV4State>()
1680 .unwrap();
1681
1682 assert!(cloned.pending_overrides().is_some(), "a clone quotes under the same overrides");
1683 }
1684
1685 #[test]
1686 fn test_set_pending_overrides_rejects_another_payload_type() {
1687 let hook = ConfigurableFeeHook::new();
1688 let mut pool = hooked_pool(&hook);
1689
1690 let result = pool.set_pending_overrides(Arc::new(0u8));
1691
1692 assert!(matches!(result, Err(SimulationError::FatalError(_))));
1693 assert!(pool.pending_overrides().is_none(), "a rejected payload sets nothing");
1694 assert!(hook.seen.lock().unwrap().is_empty());
1695 }
1696
1697 #[test]
1698 fn test_pending_overrides_are_not_part_of_equality() {
1699 let hook = ConfigurableFeeHook::new();
1700 let confirmed = hooked_pool(&hook);
1701 let mut pending = hooked_pool(&hook);
1702 pending
1703 .set_pending_overrides(Arc::new(pending_fee_update(&hook, 1)))
1704 .unwrap();
1705
1706 assert!(confirmed == pending, "PartialEq ignores the overrides");
1707 assert!(ProtocolSim::eq(&confirmed, &pending), "ProtocolSim::eq ignores the overrides");
1708 }
1709
1710 #[test]
1711 fn test_get_amount_out_no_hook() {
1712 let usv4_state = UniswapV4State::new(
1717 541501951282951892,
1718 U256::from_str("5362798333066270795901222").unwrap(), UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 100 },
1720 -192022,
1721 1,
1722 vec![
1724 TickInfo {
1725 index: -887272,
1726 net_liquidity: 460382969070005,
1727 sqrt_price: U256::from(4295128739_u64),
1728 },
1729 TickInfo {
1730 index: -207244,
1731 net_liquidity: 561268407024557,
1732 sqrt_price: U256::from_str("2505291706254206075074035").unwrap(),
1733 },
1734 TickInfo {
1735 index: -196411,
1736 net_liquidity: 825711941800452,
1737 sqrt_price: U256::from_str("4306080513146952705853399").unwrap(),
1738 },
1739 TickInfo {
1740 index: -196257,
1741 net_liquidity: 64844666874010,
1742 sqrt_price: U256::from_str("4339363644587371378270009").unwrap(),
1743 },
1744 TickInfo {
1745 index: -195611,
1746 net_liquidity: 2344045150766798,
1747 sqrt_price: U256::from_str("4481806029599743916020126").unwrap(),
1748 },
1749 TickInfo {
1750 index: -194715,
1751 net_liquidity: 391037380558274654,
1752 sqrt_price: U256::from_str("4687145946111116896040494").unwrap(),
1753 },
1754 TickInfo {
1755 index: -194599,
1756 net_liquidity: 89032603464508,
1757 sqrt_price: U256::from_str("4714409015946702405379370").unwrap(),
1758 },
1759 TickInfo {
1760 index: -194389,
1761 net_liquidity: 66635600426483168,
1762 sqrt_price: U256::from_str("4764168603367683402636621").unwrap(),
1763 },
1764 TickInfo {
1765 index: -194160,
1766 net_liquidity: 6123093436523361,
1767 sqrt_price: U256::from_str("4819029067726467394386780").unwrap(),
1768 },
1769 TickInfo {
1770 index: -194025,
1771 net_liquidity: 79940813798964,
1772 sqrt_price: U256::from_str("4851665907541490407930032").unwrap(),
1773 },
1774 TickInfo {
1775 index: -193922,
1776 net_liquidity: 415630967437234,
1777 sqrt_price: U256::from_str("4876715181040466809166531").unwrap(),
1778 },
1779 TickInfo {
1780 index: -193876,
1781 net_liquidity: 9664144015186047,
1782 sqrt_price: U256::from_str("4887943972687250473582419").unwrap(),
1783 },
1784 TickInfo {
1785 index: -193818,
1786 net_liquidity: 435344726052344,
1787 sqrt_price: U256::from_str("4902138873132735049121973").unwrap(),
1788 },
1789 TickInfo {
1790 index: -193804,
1791 net_liquidity: 221726179374067,
1792 sqrt_price: U256::from_str("4905571399964683340605904").unwrap(),
1793 },
1794 TickInfo {
1795 index: -193719,
1796 net_liquidity: 101340835774487,
1797 sqrt_price: U256::from_str("4926463397882393957462188").unwrap(),
1798 },
1799 TickInfo {
1800 index: -193690,
1801 net_liquidity: 193367475630077,
1802 sqrt_price: U256::from_str("4933611593595025190448924").unwrap(),
1803 },
1804 TickInfo {
1805 index: -193643,
1806 net_liquidity: 357016631583746,
1807 sqrt_price: U256::from_str("4945218633428068823432932").unwrap(),
1808 },
1809 TickInfo {
1810 index: -193520,
1811 net_liquidity: 917243184365178,
1812 sqrt_price: U256::from_str("4975723910367862081017120").unwrap(),
1813 },
1814 TickInfo {
1815 index: -193440,
1816 net_liquidity: 114125890211958292,
1817 sqrt_price: U256::from_str("4995665665861492533686137").unwrap(),
1818 },
1819 TickInfo {
1820 index: -193380,
1821 net_liquidity: -65980729148766579,
1822 sqrt_price: U256::from_str("5010674414300823856025303").unwrap(),
1823 },
1824 TickInfo {
1825 index: -192891,
1826 net_liquidity: 1687883551433195,
1827 sqrt_price: U256::from_str("5134689105039642314202223").unwrap(),
1828 },
1829 TickInfo {
1830 index: -192573,
1831 net_liquidity: 11108903221360975,
1832 sqrt_price: U256::from_str("5216979018647067786855495").unwrap(),
1833 },
1834 TickInfo {
1835 index: -192448,
1836 net_liquidity: 32888457482352,
1837 sqrt_price: U256::from_str("5249685603828944002327927").unwrap(),
1838 },
1839 TickInfo {
1840 index: -191525,
1841 net_liquidity: -221726179374067,
1842 sqrt_price: U256::from_str("5497623359964843320146512").unwrap(),
1843 },
1844 TickInfo {
1845 index: -191447,
1846 net_liquidity: -32888457482352,
1847 sqrt_price: U256::from_str("5519104878745833608097296").unwrap(),
1848 },
1849 TickInfo {
1850 index: -191444,
1851 net_liquidity: -114125890211958292,
1852 sqrt_price: U256::from_str("5519932765173943847315221").unwrap(),
1853 },
1854 TickInfo {
1855 index: -191417,
1856 net_liquidity: -101340835774487,
1857 sqrt_price: U256::from_str("5527389333636021285046380").unwrap(),
1858 },
1859 TickInfo {
1860 index: -191384,
1861 net_liquidity: -9664144015186047,
1862 sqrt_price: U256::from_str("5536516597603056457376182").unwrap(),
1863 },
1864 TickInfo {
1865 index: -191148,
1866 net_liquidity: -561268407024557,
1867 sqrt_price: U256::from_str("5602231161238705865493165").unwrap(),
1868 },
1869 TickInfo {
1870 index: -191147,
1871 net_liquidity: -1687883551433195,
1872 sqrt_price: U256::from_str("5602511265794328966803451").unwrap(),
1873 },
1874 TickInfo {
1875 index: -191091,
1876 net_liquidity: -89032603464508,
1877 sqrt_price: U256::from_str("5618219493196441347292357").unwrap(),
1878 },
1879 TickInfo {
1880 index: -190950,
1881 net_liquidity: -189177935487638,
1882 sqrt_price: U256::from_str("5657965894785859782969011").unwrap(),
1883 },
1884 TickInfo {
1885 index: -190756,
1886 net_liquidity: -6123093436523361,
1887 sqrt_price: U256::from_str("5713112435031881967192022").unwrap(),
1888 },
1889 TickInfo {
1890 index: -190548,
1891 net_liquidity: -193367475630077,
1892 sqrt_price: U256::from_str("5772835841671084402427710").unwrap(),
1893 },
1894 TickInfo {
1895 index: -190430,
1896 net_liquidity: -11108903221360975,
1897 sqrt_price: U256::from_str("5806994534290341208820930").unwrap(),
1898 },
1899 TickInfo {
1900 index: -190195,
1901 net_liquidity: -391583014714302569,
1902 sqrt_price: U256::from_str("5875625707132601785181387").unwrap(),
1903 },
1904 TickInfo {
1905 index: -190043,
1906 net_liquidity: -357016631583746,
1907 sqrt_price: U256::from_str("5920448331650864936739481").unwrap(),
1908 },
1909 TickInfo {
1910 index: -189779,
1911 net_liquidity: -917243184365178,
1912 sqrt_price: U256::from_str("5999112356918485175181346").unwrap(),
1913 },
1914 TickInfo {
1915 index: -189663,
1916 net_liquidity: -2344045150766798,
1917 sqrt_price: U256::from_str("6034006559279282606084981").unwrap(),
1918 },
1919 TickInfo {
1920 index: -189620,
1921 net_liquidity: -435344726052344,
1922 sqrt_price: U256::from_str("6046992979471024289177519").unwrap(),
1923 },
1924 TickInfo {
1925 index: -189409,
1926 net_liquidity: -825711941800452,
1927 sqrt_price: U256::from_str("6111123241285165242130911").unwrap(),
1928 },
1929 TickInfo {
1930 index: -189325,
1931 net_liquidity: -3947182209207,
1932 sqrt_price: U256::from_str("6136842645893819031257990").unwrap(),
1933 },
1934 TickInfo {
1935 index: -189324,
1936 net_liquidity: -415630967437234,
1937 sqrt_price: U256::from_str("6137149480355443943537284").unwrap(),
1938 },
1939 TickInfo {
1940 index: -115136,
1941 net_liquidity: 462452451821,
1942 sqrt_price: U256::from_str("250529060232794967902094762").unwrap(),
1943 },
1944 TickInfo {
1945 index: -92109,
1946 net_liquidity: -462452451821,
1947 sqrt_price: U256::from_str("792242363124136400178523925").unwrap(),
1948 },
1949 TickInfo {
1950 index: 887272,
1951 net_liquidity: -521280453734808,
1952 sqrt_price: U256::from_str("1461446703485210103287273052203988822378723970342")
1953 .unwrap(),
1954 },
1955 ],
1956 )
1957 .unwrap();
1958
1959 let t0 = usdc();
1960 let t1 = eth();
1961
1962 let out = usv4_state
1963 .get_amount_out(BigUint::from_u64(2000000).unwrap(), &t0, &t1)
1964 .unwrap();
1965
1966 assert_eq!(out.amount, BigUint::from_str("436478419853848").unwrap())
1967 }
1968
1969 #[test]
1970 fn test_get_amount_out_euler_hook() {
1971 let block = BlockHeader {
1987 number: 22689128,
1988 hash: Bytes::from_str(
1989 "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
1990 )
1991 .expect("Invalid block hash"),
1992 timestamp: 1749739055,
1993 ..Default::default()
1994 };
1995
1996 let mut usv4_state = UniswapV4State::new(
1999 0,
2000 U256::from_str("4295128740").unwrap(),
2001 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
2002 0,
2003 1,
2004 vec![],
2005 )
2006 .unwrap();
2007
2008 let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
2009 .expect("Invalid hook address");
2010
2011 let db = SimulationDB::new(
2012 get_client(None).expect("Failed to create client"),
2013 get_runtime().expect("Failed to get runtime"),
2014 Some(block.clone()),
2015 );
2016 let engine = create_engine(db, true).expect("Failed to create simulation engine");
2017 let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
2018 .expect("Invalid pool manager address");
2019
2020 let hook_handler = GenericVMHookHandler::new(
2021 hook_address,
2022 engine,
2023 pool_manager,
2024 HashMap::new(),
2025 HashMap::new(),
2026 None,
2027 true, )
2029 .unwrap();
2030
2031 let t0 = usdc();
2032 let t1 = weth();
2033
2034 usv4_state.set_hook_handler(Box::new(hook_handler));
2035 let out = usv4_state
2036 .get_amount_out(BigUint::from_u64(7407000000).unwrap(), &t0, &t1)
2037 .unwrap();
2038
2039 assert_eq!(out.amount, BigUint::from_str("2681115183499232721").unwrap())
2040 }
2041
2042 #[test]
2043 fn test_get_amount_out_angstrom_hook() {
2044 let mut usv4_state = UniswapV4State::new(
2046 66319800403673162,
2048 U256::from_str("1314588940601923011323000261788004").unwrap(),
2049 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 8388608 },
2051 194343,
2052 10,
2053 vec![
2054 TickInfo::new(-887270, 198117767801).unwrap(),
2055 TickInfo::new(191990, 24561988698695).unwrap(),
2056 TickInfo::new(192280, 2839631428751224).unwrap(),
2057 TickInfo::new(193130, 318786492813931).unwrap(),
2058 TickInfo::new(194010, 26209207141081).unwrap(),
2059 TickInfo::new(194210, -26209207141081).unwrap(),
2060 TickInfo::new(194220, 63136622375641511).unwrap(),
2061 TickInfo::new(194420, -63136622375641511).unwrap(),
2062 TickInfo::new(195130, -318786492813931).unwrap(),
2063 TickInfo::new(196330, -2839631428751224).unwrap(),
2064 TickInfo::new(197100, -24561988698695).unwrap(),
2065 TickInfo::new(887270, -198117767801).unwrap(),
2066 ],
2067 )
2068 .unwrap();
2069
2070 let fees = AngstromFees {
2071 unlock: U24::from(338),
2078 protocol_unlock: U24::from(112),
2079 };
2080 let hook_handler = AngstromHookHandler::new(
2081 Address::from_str("0x0000000aa232009084bd71a5797d089aa4edfad4").unwrap(),
2082 Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90").unwrap(),
2083 fees,
2084 false,
2085 );
2086
2087 let t0 = usdc();
2088 let t1 = weth();
2089
2090 usv4_state.set_hook_handler(Box::new(hook_handler));
2091 let out = usv4_state
2092 .get_amount_out(
2093 BigUint::from_u64(
2094 6645198144, )
2096 .unwrap(),
2097 &t0, &t1, )
2100 .unwrap();
2101
2102 assert_eq!(out.amount, BigUint::from_str("1825627051870330472").unwrap())
2103 }
2104
2105 #[derive(Debug, Clone, PartialEq, Eq)]
2107 struct AfterSwapTestHook {
2108 address: Address,
2109 delta: I128,
2110 analytic_fee_bps: Option<u32>,
2113 spot_price_override: Option<u64>,
2116 }
2117
2118 impl HookHandler for AfterSwapTestHook {
2119 fn address(&self) -> Address {
2120 self.address
2121 }
2122
2123 fn before_swap(
2124 &self,
2125 _: BeforeSwapParameters,
2126 _: Option<HashMap<Address, HashMap<U256, U256>>>,
2127 _: Option<HashMap<Address, HashMap<U256, U256>>>,
2128 ) -> Result<WithGasEstimate<BeforeSwapOutput>, SimulationError> {
2129 Err(SimulationError::RecoverableError("not implemented".into()))
2130 }
2131
2132 fn after_swap(
2133 &self,
2134 _: AfterSwapParameters,
2135 _: Option<HashMap<Address, HashMap<U256, U256>>>,
2136 _: Option<HashMap<Address, HashMap<U256, U256>>>,
2137 ) -> Result<WithGasEstimate<AfterSwapDelta>, SimulationError> {
2138 Ok(WithGasEstimate { gas_estimate: AFTER_SWAP_TEST_HOOK_GAS, result: self.delta })
2139 }
2140
2141 fn fee(&self, _: &UniswapV4State, _: SwapParams) -> Result<f64, SimulationError> {
2142 Err(SimulationError::RecoverableError("not implemented".into()))
2143 }
2144
2145 fn spot_price(&self, _: &Token, _: &Token) -> Result<f64, SimulationError> {
2146 match self.spot_price_override {
2147 Some(price) => Ok(price as f64),
2148 None => Err(SimulationError::RecoverableError("not implemented".into())),
2149 }
2150 }
2151
2152 fn unspecified_fee_amount(
2153 &self,
2154 unspecified: U256,
2155 _: bool,
2156 ) -> Result<Option<U256>, SimulationError> {
2157 let Some(bps) = self.analytic_fee_bps else { return Ok(None) };
2158 Ok(Some(unspecified * U256::from(bps) / U256::from(10_000u64)))
2159 }
2160
2161 fn get_amount_ranges(&self, _: Bytes, _: Bytes) -> Result<AmountRanges, SimulationError> {
2162 Err(SimulationError::RecoverableError("not implemented".into()))
2163 }
2164
2165 fn delta_transition(
2166 &mut self,
2167 _: ProtocolStateDelta,
2168 _: &HashMap<Bytes, Token>,
2169 _: &Balances,
2170 ) -> Result<(), TransitionError> {
2171 Ok(())
2172 }
2173
2174 fn clone_box(&self) -> Box<dyn HookHandler> {
2175 Box::new(self.clone())
2176 }
2177
2178 fn as_any(&self) -> &dyn Any {
2179 self
2180 }
2181
2182 fn is_equal(&self, other: &dyn HookHandler) -> bool {
2183 other.as_any().downcast_ref::<Self>() == Some(self)
2184 }
2185 }
2186
2187 const AFTER_SWAP_TEST_HOOK_GAS: u64 = 1_000;
2190 const AFTER_SWAP_TEST_HOOK_DELTA: u64 = 777;
2191
2192 fn construct_hook_address(hook_options: &[HookOptions]) -> Address {
2193 let mut hook_flags = U160::ZERO;
2194 let one = U160::from_limbs([1, 0, 0]);
2195 for hook_option in hook_options {
2196 hook_flags |= one << (*hook_option as u8);
2197 }
2198 Address::from(hook_flags)
2199 }
2200
2201 fn after_swap_test_hook(hook_options: &[HookOptions]) -> Box<dyn HookHandler> {
2202 Box::new(AfterSwapTestHook {
2203 address: construct_hook_address(hook_options),
2204 delta: I128::unchecked_from(AFTER_SWAP_TEST_HOOK_DELTA),
2205 analytic_fee_bps: None,
2206 spot_price_override: None,
2207 })
2208 }
2209
2210 fn analytic_fee_test_hook(analytic_fee_bps: u32) -> Box<dyn HookHandler> {
2212 Box::new(AfterSwapTestHook {
2213 address: construct_hook_address(&[
2214 HookOptions::AfterSwap,
2215 HookOptions::AfterSwapReturnsDelta,
2216 ]),
2217 delta: I128::ZERO,
2218 analytic_fee_bps: Some(analytic_fee_bps),
2219 spot_price_override: None,
2220 })
2221 }
2222
2223 const TEST_HOOK_SPOT_PRICE: u64 = 1_000_000;
2226
2227 fn pricing_test_hook(analytic_fee_bps: Option<u32>) -> Box<dyn HookHandler> {
2230 Box::new(AfterSwapTestHook {
2231 address: construct_hook_address(&[
2232 HookOptions::AfterSwap,
2233 HookOptions::AfterSwapReturnsDelta,
2234 ]),
2235 delta: I128::ZERO,
2236 analytic_fee_bps,
2237 spot_price_override: Some(TEST_HOOK_SPOT_PRICE),
2238 })
2239 }
2240
2241 fn basic_v4_test_pool_tokens(zero_for_one: bool) -> (Token, Token) {
2242 if zero_for_one {
2243 (token_x(), token_y())
2244 } else {
2245 (token_y(), token_x())
2246 }
2247 }
2248
2249 #[rstest]
2252 #[case::zero_for_one(true)]
2253 #[case::one_for_zero(false)]
2254 fn test_after_swap_delta_ignored_without_returns_delta_permission(#[case] zero_for_one: bool) {
2255 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2256 let amount_in = BigUint::from(1_000_000_000_000_000u64);
2257
2258 let hookless = create_basic_v4_test_pool()
2259 .get_amount_out(amount_in.clone(), &token_in, &token_out)
2260 .expect("hookless swap should succeed");
2261
2262 let mut pool = create_basic_v4_test_pool();
2263 pool.set_hook_handler(after_swap_test_hook(&[HookOptions::AfterSwap]));
2264 let gated = pool
2265 .get_amount_out(amount_in, &token_in, &token_out)
2266 .expect("gated swap should succeed");
2267
2268 assert_eq!(gated.amount, hookless.amount);
2269 assert_eq!(
2270 gated.gas,
2271 hookless.gas + BigUint::from(AFTER_SWAP_TEST_HOOK_GAS + PM_PER_HOOK_CALL_OVERHEAD)
2272 );
2273 }
2274
2275 #[rstest]
2278 #[case::zero_for_one(true)]
2279 #[case::one_for_zero(false)]
2280 fn test_after_swap_delta_applied_with_returns_delta_permission(#[case] zero_for_one: bool) {
2281 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2282 let amount_in = BigUint::from(1_000_000_000_000_000u64);
2283
2284 let hookless = create_basic_v4_test_pool()
2285 .get_amount_out(amount_in.clone(), &token_in, &token_out)
2286 .expect("hookless swap should succeed");
2287
2288 let mut pool = create_basic_v4_test_pool();
2289 pool.set_hook_handler(after_swap_test_hook(&[
2290 HookOptions::AfterSwap,
2291 HookOptions::AfterSwapReturnsDelta,
2292 ]));
2293 let with_delta = pool
2294 .get_amount_out(amount_in, &token_in, &token_out)
2295 .expect("swap with hook delta should succeed");
2296
2297 assert_eq!(with_delta.amount, &hookless.amount - BigUint::from(AFTER_SWAP_TEST_HOOK_DELTA));
2298 assert_eq!(
2299 with_delta.gas,
2300 hookless.gas + BigUint::from(AFTER_SWAP_TEST_HOOK_GAS + PM_PER_HOOK_CALL_OVERHEAD)
2301 );
2302 }
2303
2304 const FEELESS_POOL_LIQUIDITY: u128 = 100_000_000_000_000_000_000; fn create_feeless_v4_test_pool(liquidity: u128) -> UniswapV4State {
2310 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2311 .expect("a price of two has a square root");
2312 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("the sqrt price maps to a tick");
2313 let position = FEELESS_POOL_LIQUIDITY as i128;
2314
2315 UniswapV4State::new(
2316 liquidity,
2317 sqrt_price,
2318 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 0 },
2319 tick,
2320 60,
2321 vec![
2322 TickInfo::new(-46_080, position).unwrap(),
2323 TickInfo::new(46_080, -position).unwrap(),
2324 ],
2325 )
2326 .expect("the pool builds")
2327 }
2328
2329 fn pons_test_handler() -> PonsV2HookHandler {
2330 PonsV2HookHandler::new(PONS_V2_HOOK_ROBINHOOD, 100, 100)
2331 }
2332
2333 #[test]
2338 fn hookless_spot_price_is_the_documented_buy_price_with_lp_fee_markup() {
2339 const LP_FEE_PIPS: u32 = 3_000;
2340 const LP_FEE: f64 = LP_FEE_PIPS as f64 / 1_000_000.0;
2341 const LIQUIDITY: u128 = 100_000_000_000_000_000_000; const TOLERANCE: f64 = 1e-12;
2343
2344 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
2345 .expect("a price of two has a square root");
2346 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("the sqrt price maps to a tick");
2347 let pool = UniswapV4State::new(
2348 LIQUIDITY,
2349 sqrt_price,
2350 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: LP_FEE_PIPS },
2351 tick,
2352 60,
2353 vec![
2354 TickInfo::new(-46_080, LIQUIDITY as i128).unwrap(),
2355 TickInfo::new(46_080, -(LIQUIDITY as i128)).unwrap(),
2356 ],
2357 )
2358 .expect("the pool builds");
2359
2360 assert!(pool.hook.is_none(), "the pinned values only hold with no hook handler");
2361
2362 let (t0, t1) = (token_x(), token_y());
2363 let pre_fee = sqrt_price_q96_to_f64(sqrt_price, t0.decimals, t1.decimals)
2364 .expect("the sqrt price converts to a price");
2365
2366 let expected_buy_t0 = pre_fee / (1.0 - LP_FEE);
2367 let expected_buy_t1 = (1.0 / pre_fee) / (1.0 - LP_FEE);
2368
2369 let buy_t0 = pool
2370 .spot_price(&t0, &t1)
2371 .expect("a hookless pool always prices");
2372 let buy_t1 = pool
2373 .spot_price(&t1, &t0)
2374 .expect("a hookless pool always prices");
2375
2376 assert!(
2377 (buy_t0 / expected_buy_t0 - 1.0).abs() < TOLERANCE,
2378 "buying t0 quoted {buy_t0}, expected {expected_buy_t0}"
2379 );
2380 assert!(
2381 (buy_t1 / expected_buy_t1 - 1.0).abs() < TOLERANCE,
2382 "buying t1 quoted {buy_t1}, expected {expected_buy_t1}"
2383 );
2384 }
2385
2386 #[rstest]
2390 #[case::base_is_currency0(true)]
2391 #[case::base_is_currency1(false)]
2392 fn test_spot_price_marks_up_an_analytic_hook_fee(#[case] base_is_currency0: bool) {
2393 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2394 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2395 hooked.set_hook_handler(Box::new(pons_test_handler()));
2396
2397 let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2398 .spot_price(&base, "e)
2399 .expect("a hookless pool always prices");
2400 let price = hooked
2401 .spot_price(&base, "e)
2402 .expect("the hook prices its own fee");
2403
2404 assert!(price > core, "hooked {price} is not above hookless {core}");
2405 let ratio = price / core;
2406 assert!((ratio * 0.98 - 1.0).abs() < 1e-9, "hooked/hookless is {ratio}, not 1/0.98");
2407 }
2408
2409 #[rstest]
2412 #[case::base_is_currency0(true)]
2413 #[case::base_is_currency1(false)]
2414 fn test_hooked_spot_price_matches_a_small_hooked_buy(#[case] base_is_currency0: bool) {
2415 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2416 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2417 hooked.set_hook_handler(Box::new(pons_test_handler()));
2418
2419 let quote_in = BigUint::from(100_000_000_000_000u64);
2420 let base_out = hooked
2421 .get_amount_out(quote_in.clone(), "e, &base)
2422 .expect("a tiny buy always fits the pool")
2423 .amount;
2424
2425 let executed = quote_in.to_f64().unwrap() / base_out.to_f64().unwrap();
2426 let spot = hooked
2427 .spot_price(&base, "e)
2428 .expect("the hook prices its own fee");
2429
2430 assert!((executed / spot - 1.0).abs() < 1e-4, "executed {executed}, quoted {spot}");
2431 }
2432
2433 #[rstest]
2437 #[case::base_is_currency0(true)]
2438 #[case::base_is_currency1(false)]
2439 fn test_spot_price_prefers_an_analytic_fee_over_the_hooks_own_price(
2440 #[case] base_is_currency0: bool,
2441 ) {
2442 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2443 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2444 hooked.set_hook_handler(pricing_test_hook(Some(200)));
2445
2446 let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2447 .spot_price(&base, "e)
2448 .expect("a hookless pool always prices");
2449 let price = hooked
2450 .spot_price(&base, "e)
2451 .expect("the hook prices its own fee");
2452
2453 assert_ne!(
2454 price, TEST_HOOK_SPOT_PRICE as f64,
2455 "the pool asked the hook for a price instead of marking up its own"
2456 );
2457 let ratio = price / core;
2458 assert!((ratio * 0.98 - 1.0).abs() < 1e-9, "hooked/hookless is {ratio}, not 1/0.98");
2459 }
2460
2461 #[rstest]
2465 #[case::base_is_currency0(true)]
2466 #[case::base_is_currency1(false)]
2467 fn test_spot_price_uses_the_hooks_own_price_without_an_analytic_fee(
2468 #[case] base_is_currency0: bool,
2469 ) {
2470 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2471 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2472 hooked.set_hook_handler(pricing_test_hook(None));
2473
2474 let price = hooked
2475 .spot_price(&base, "e)
2476 .expect("the hook answers with a price");
2477
2478 assert_eq!(price, TEST_HOOK_SPOT_PRICE as f64);
2479 }
2480
2481 #[rstest]
2484 #[case::base_is_currency0(true)]
2485 #[case::base_is_currency1(false)]
2486 fn test_spot_price_falls_back_to_finite_difference_without_an_analytic_fee(
2487 #[case] base_is_currency0: bool,
2488 ) {
2489 let (base, quote) = basic_v4_test_pool_tokens(base_is_currency0);
2490 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2491 hooked.set_hook_handler(after_swap_test_hook(&[
2492 HookOptions::AfterSwap,
2493 HookOptions::AfterSwapReturnsDelta,
2494 ]));
2495
2496 let x1 = BigUint::from(10u64).pow(base.decimals) / BigUint::from(100u64);
2497 let x2 = &x1 + (&x1 / BigUint::from(100u64));
2498 let y1 = hooked
2499 .get_amount_out(x1.clone(), &base, "e)
2500 .expect("the smaller probe swap fits")
2501 .amount;
2502 let y2 = hooked
2503 .get_amount_out(x2.clone(), &base, "e)
2504 .expect("the larger probe swap fits")
2505 .amount;
2506 let slope = (&y2 - &y1).to_f64().unwrap() / (&x2 - &x1).to_f64().unwrap();
2507
2508 let price = hooked
2509 .spot_price(&base, "e)
2510 .expect("the fallback always prices");
2511
2512 assert_eq!(price, slope);
2513 let core = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2514 .spot_price(&base, "e)
2515 .expect("a hookless pool always prices");
2516 assert!((price / core - 1.0).abs() < 1e-3, "fallback {price} strayed from core {core}");
2517 }
2518
2519 #[rstest]
2522 #[case::zero_for_one(true)]
2523 #[case::one_for_zero(false)]
2524 fn test_get_limits_reports_the_output_net_of_an_analytic_hook_fee(#[case] zero_for_one: bool) {
2525 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2526 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2527 hooked.set_hook_handler(Box::new(pons_test_handler()));
2528
2529 let (hookless_in, hookless_out) = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2530 .get_limits(token_in.address.clone(), token_out.address.clone())
2531 .expect("a pool with liquidity has limits");
2532 let (limit_in, limit_out) = hooked
2533 .get_limits(token_in.address.clone(), token_out.address.clone())
2534 .expect("a pool with liquidity has limits");
2535
2536 assert!(hookless_out > BigUint::zero(), "the reference pool must move some output");
2537 assert_eq!(limit_in, hookless_in);
2538 let taken = pons_test_handler()
2539 .fee_and_tax(biguint_to_u256(&hookless_out))
2540 .expect("a pool sized output never overflows");
2541 assert_eq!(limit_out, &hookless_out - u256_to_biguint(taken));
2542 assert!(limit_out < hookless_out);
2543 }
2544
2545 #[rstest]
2548 #[case::zero_for_one(true)]
2549 #[case::one_for_zero(false)]
2550 fn test_get_limits_unchanged_without_an_analytic_hook_fee(#[case] zero_for_one: bool) {
2551 let (token_in, token_out) = basic_v4_test_pool_tokens(zero_for_one);
2552 let mut hooked = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2553 hooked.set_hook_handler(after_swap_test_hook(&[
2554 HookOptions::AfterSwap,
2555 HookOptions::AfterSwapReturnsDelta,
2556 ]));
2557
2558 let hookless = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY)
2559 .get_limits(token_in.address.clone(), token_out.address.clone())
2560 .expect("a pool with liquidity has limits");
2561 let limits = hooked
2562 .get_limits(token_in.address.clone(), token_out.address.clone())
2563 .expect("a pool with liquidity has limits");
2564
2565 assert_eq!(limits, hookless);
2566 }
2567
2568 #[test]
2571 fn test_get_limits_on_a_drained_pool_is_zero_with_an_analytic_hook() {
2572 let (token_in, token_out) = basic_v4_test_pool_tokens(true);
2573 let mut drained = create_feeless_v4_test_pool(0);
2574 drained.set_hook_handler(Box::new(pons_test_handler()));
2575
2576 let limits = drained
2577 .get_limits(token_in.address, token_out.address)
2578 .expect("a drained pool reports zero rather than failing");
2579
2580 assert_eq!(limits, (BigUint::zero(), BigUint::zero()));
2581 }
2582
2583 #[rstest]
2586 #[case::the_whole_output(10_000)]
2587 #[case::more_than_the_whole_output(20_000)]
2588 fn test_spot_price_rejects_a_hook_that_takes_the_whole_output(#[case] analytic_fee_bps: u32) {
2589 let mut pool = create_feeless_v4_test_pool(FEELESS_POOL_LIQUIDITY);
2590 pool.set_hook_handler(analytic_fee_test_hook(analytic_fee_bps));
2591
2592 let error = pool
2593 .spot_price(&token_x(), &token_y())
2594 .expect_err("a rate of one or more is not a price");
2595
2596 assert!(matches!(error, SimulationError::FatalError(_)), "{error:?}");
2597 }
2598
2599 #[test]
2600 fn test_spot_price_with_recoverable_error() {
2601 let usv4_state = UniswapV4State::new(
2605 1000000000000000000u128, U256::from_str("79228162514264337593543950336").unwrap(), UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 },
2608 0,
2609 60,
2610 vec![
2611 TickInfo::new(-600, 500000000000000000i128).unwrap(),
2612 TickInfo::new(600, -500000000000000000i128).unwrap(),
2613 ],
2614 )
2615 .unwrap();
2616
2617 let spot_price_result = usv4_state.spot_price(&usdc(), &weth());
2619 assert!(spot_price_result.is_ok());
2620
2621 let price = spot_price_result.unwrap();
2624 assert!(price > 0.0);
2625 }
2626
2627 #[test]
2628 fn test_get_limits_with_hook_managed_liquidity_no_ranges_entrypoint() {
2629 let block = BlockHeader {
2634 number: 22689128,
2635 hash: Bytes::from_str(
2636 "0xfbfa716523d25d6d5248c18d001ca02b1caf10cabd1ab7321465e2262c41157b",
2637 )
2638 .expect("Invalid block hash"),
2639 timestamp: 1749739055,
2640 ..Default::default()
2641 };
2642
2643 let hook_address: Address = Address::from_str("0x69058613588536167ba0aa94f0cc1fe420ef28a8")
2644 .expect("Invalid hook address");
2645
2646 let db = SimulationDB::new(
2647 get_client(None).expect("Failed to create client"),
2648 get_runtime().expect("Failed to get runtime"),
2649 Some(block.clone()),
2650 );
2651 let engine = create_engine(db, true).expect("Failed to create simulation engine");
2652 let pool_manager = Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90")
2653 .expect("Invalid pool manager address");
2654
2655 let hook_handler = GenericVMHookHandler::new(
2658 hook_address,
2659 engine,
2660 pool_manager,
2661 HashMap::new(),
2662 HashMap::new(),
2663 None,
2664 true, )
2666 .unwrap();
2667
2668 let mut usv4_state = UniswapV4State::new(
2670 0, U256::from_str("4295128740").unwrap(),
2672 UniswapV4Fees { zero_for_one: 100, one_for_zero: 90, lp_fee: 500 },
2673 0, 1, vec![], )
2677 .unwrap();
2678
2679 usv4_state.set_hook_handler(Box::new(hook_handler));
2680
2681 let token_in = usdc().address;
2682 let token_out = weth().address;
2683
2684 let (amount_in_limit, amount_out_limit) = usv4_state
2685 .get_limits(token_in, token_out)
2686 .expect("Should find limits through experimental swapping");
2687
2688 assert!(amount_in_limit > BigUint::from(10u64).pow(12));
2691 assert!(amount_in_limit < BigUint::from(10u64).pow(14));
2692
2693 assert!(amount_out_limit > BigUint::from(10u64).pow(20));
2695 assert!(amount_out_limit < BigUint::from(10u64).pow(22));
2696 }
2697
2698 #[rstest]
2699 #[case::high_liquidity(u128::MAX / 2)] #[case::medium_liquidity(10000000000000000000u128)] #[case::minimal_liquidity(1000u128)] fn test_find_max_amount(#[case] liquidity: u128) {
2703 let fees = UniswapV4Fees { zero_for_one: 100, one_for_zero: 100, lp_fee: 100 };
2705 let tick_spacing = 60;
2706 let ticks = vec![
2707 TickInfo::new(-600, (liquidity / 4) as i128).unwrap(),
2708 TickInfo::new(600, -((liquidity / 4) as i128)).unwrap(),
2709 ];
2710
2711 let usv4_state = UniswapV4State::new(
2712 liquidity,
2713 U256::from_str("79228162514264337593543950336").unwrap(),
2714 fees,
2715 0,
2716 tick_spacing,
2717 ticks,
2718 )
2719 .unwrap();
2720
2721 let token_in = usdc();
2722 let token_out = weth();
2723
2724 let (max_amount_in, _max_amount_out) = usv4_state
2725 .find_max_amount(&token_in, &token_out)
2726 .unwrap();
2727
2728 let success = usv4_state
2729 .get_amount_out(max_amount_in.clone(), &token_in, &token_out)
2730 .is_ok();
2731 assert!(success, "Should be able to swap the exact max amount.");
2732
2733 let one_more = &max_amount_in + BigUint::from(1u64);
2734 let should_fail = usv4_state
2735 .get_amount_out(one_more, &token_in, &token_out)
2736 .is_err();
2737 assert!(should_fail, "Swapping max_amount + 1 should fail.");
2738 }
2739
2740 #[test]
2741 fn test_calculate_swap_fees_with_override() {
2742 let fees = UniswapV4Fees::new(100, 90, 500);
2744
2745 let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
2748 assert_eq!(total_zero_for_one, 600);
2750
2751 let total_with_override = fees.calculate_swap_fees_pips(true, Some(1000));
2753 assert_eq!(total_with_override, 1100);
2755 }
2756
2757 #[test]
2758 fn test_max_combined_fees_stays_valid() {
2759 let fees = UniswapV4Fees::new(1000, 1000, 1000);
2761 let total = fees.calculate_swap_fees_pips(true, Some(lp_fee::MAX_LP_FEE));
2762
2763 assert_eq!(total, 1_000_000);
2766 }
2767
2768 #[test]
2769 fn test_get_limits_graceful_underflow() {
2770 let usv4_state = UniswapV4State::new(
2772 1000000,
2773 U256::from_str("79228162514264337593543950336").unwrap(), UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2775 0,
2776 60,
2777 vec![
2778 TickInfo {
2781 index: -60,
2782 net_liquidity: 2000000, sqrt_price: U256::from_str("79051508376726796163471739988").unwrap(),
2784 },
2785 ],
2786 )
2787 .unwrap();
2788
2789 let usdc = usdc();
2790 let weth = weth();
2791
2792 let (limit_in, limit_out) = usv4_state
2793 .get_limits(usdc.address.clone(), weth.address.clone())
2794 .unwrap();
2795
2796 assert!(limit_in > BigUint::zero());
2798 assert!(limit_out > BigUint::zero());
2799 }
2800
2801 const MAX_PROTOCOL_FEE: u32 = 1000;
2806
2807 #[rstest]
2808 #[case::max_protocol_and_max_lp(MAX_PROTOCOL_FEE, lp_fee::MAX_LP_FEE, lp_fee::MAX_LP_FEE)]
2809 #[case::max_protocol_with_3000_lp(MAX_PROTOCOL_FEE, 3000, 3997)]
2810 #[case::max_protocol_with_zero_lp(MAX_PROTOCOL_FEE, 0, MAX_PROTOCOL_FEE)]
2811 #[case::zero_protocol_zero_lp(0, 0, 0)]
2812 #[case::zero_protocol_with_1000_lp(0, 1000, 1000)]
2813 fn test_calculate_swap_fees_uniswap_test_cases(
2814 #[case] protocol_fee: u32,
2815 #[case] lp_fee: u32,
2816 #[case] expected: u32,
2817 ) {
2818 let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
2819 let result = fees.calculate_swap_fees_pips(true, None);
2820 assert_eq!(result, expected);
2821 }
2822
2823 #[test]
2824 fn test_calculate_swap_fees_with_dynamic_fee() {
2825 let fees = UniswapV4Fees::new(100, 90, lp_fee::DYNAMIC_FEE_FLAG);
2827
2828 let total_zero_for_one = fees.calculate_swap_fees_pips(true, None);
2830 assert_eq!(total_zero_for_one, 100);
2832
2833 let total_with_override = fees.calculate_swap_fees_pips(true, Some(500));
2835 assert_eq!(total_with_override, 600);
2837 }
2838
2839 #[test]
2840 fn test_calculate_swap_fees_direction_matters() {
2841 let fees = UniswapV4Fees::new(100, 200, 500);
2843
2844 let zero_for_one_fee = fees.calculate_swap_fees_pips(true, None);
2845 assert_eq!(zero_for_one_fee, 600);
2847
2848 let one_for_zero_fee = fees.calculate_swap_fees_pips(false, None);
2849 assert_eq!(one_for_zero_fee, 700);
2851 }
2852
2853 #[rstest]
2854 #[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(
2858 #[case] protocol_fee: u32,
2859 #[case] lp_fee: u32,
2860 #[case] expected: u32,
2861 ) {
2862 let fees = UniswapV4Fees::new(protocol_fee, protocol_fee, lp_fee);
2865 let result = fees.calculate_swap_fees_pips(true, None);
2866 assert_eq!(result, expected, "Failed for protocol={}, lp={}", protocol_fee, lp_fee);
2867 }
2868
2869 #[test]
2870 fn test_calculate_swap_fees_override_takes_precedence() {
2871 let fees = UniswapV4Fees::new(100, 100, 3000);
2873
2874 let result = fees.calculate_swap_fees_pips(true, Some(5000));
2876 assert_eq!(result, 5100);
2878
2879 let result_no_override = fees.calculate_swap_fees_pips(true, None);
2881 assert_eq!(result_no_override, 3100);
2883 }
2884
2885 #[test]
2886 fn test_calculate_swap_fees_zero_protocol_fee() {
2887 let fees = UniswapV4Fees::new(0, 0, 3000);
2889 let result = fees.calculate_swap_fees_pips(true, None);
2890 assert_eq!(result, 3000);
2892 }
2893
2894 #[test]
2895 fn test_calculate_swap_fees_zero_lp_fee() {
2896 let fees = UniswapV4Fees::new(500, 500, 0);
2898 let result = fees.calculate_swap_fees_pips(true, None);
2899 assert_eq!(result, 500);
2901 }
2902
2903 fn create_basic_v4_test_pool() -> UniswapV4State {
2905 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))
2907 .expect("Failed to calculate sqrt price");
2908 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
2909
2910 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
2911
2912 UniswapV4State::new(
2913 liquidity,
2914 sqrt_price,
2915 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 }, tick,
2917 60, ticks,
2919 )
2920 .expect("Failed to create pool")
2921 }
2922
2923 fn create_tick_boundary_v4_test_pool() -> UniswapV4State {
2924 let sqrt_price = get_sqrt_ratio_at_tick(0).expect("Failed to calculate sqrt price");
2925 let ticks = vec![TickInfo::new(-120, 0).unwrap(), TickInfo::new(120, 0).unwrap()];
2926
2927 UniswapV4State::new(
2928 100_000_000_000_000_000_000u128,
2929 sqrt_price,
2930 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
2931 0,
2932 60,
2933 ticks,
2934 )
2935 .expect("Failed to create pool")
2936 }
2937
2938 #[test]
2939 fn test_partial_step_updates_tick_when_price_moves_without_crossing_initialized_tick() {
2940 let pool = create_tick_boundary_v4_test_pool();
2941 let amount = -I256::from_raw(U256::from(100_000_000_000_000_000u64));
2942
2943 let result = pool
2944 .swap(true, amount, None, None)
2945 .expect("swap should stay within the current liquidity range");
2946 let expected_tick =
2947 get_tick_at_sqrt_ratio(result.sqrt_price).expect("new sqrt price should map to a tick");
2948
2949 assert_ne!(result.sqrt_price, pool.sqrt_price);
2950 assert_ne!(result.sqrt_price, get_sqrt_ratio_at_tick(-120).unwrap());
2951 assert_ne!(expected_tick, pool.tick.value());
2952 assert_eq!(result.tick.value(), expected_tick);
2953 }
2954
2955 #[test]
2956 fn test_swap_keeps_boundary_tick_when_price_does_not_move() {
2957 let mut pool = create_tick_boundary_v4_test_pool();
2958 pool.tick = PoolTick::from(-1);
2959 let amount = -I256::from_raw(U256::from(1u64));
2960
2961 let result = pool
2962 .swap(true, amount, None, None)
2963 .expect("swap should consume the input as fee without moving price");
2964
2965 assert_eq!(result.sqrt_price, pool.sqrt_price);
2966 assert_eq!(get_tick_at_sqrt_ratio(result.sqrt_price).unwrap(), 0);
2967 assert_eq!(result.tick, pool.tick);
2968 }
2969
2970 #[test]
2971 fn test_swap_to_price_price_too_high() {
2972 let pool = create_basic_v4_test_pool();
2973
2974 let token_x = token_x();
2975 let token_y = token_y();
2976
2977 let target_price = Price::new(BigUint::from(10_000_000u64), BigUint::from(1_000_000u64));
2979
2980 let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
2981 token_x,
2982 token_y,
2983 SwapConstraint::PoolTargetPrice {
2984 target: target_price,
2985 tolerance: 0f64,
2986 min_amount_in: None,
2987 max_amount_in: None,
2988 },
2989 ));
2990 assert!(result.is_err(), "Should return error when target price is unreachable");
2991 }
2992
2993 #[test]
2994 fn test_swap_to_price_no_liquidity() {
2995 let pool = UniswapV4State::new(
2997 0, U256::from_str("79228162514264337593543950336").unwrap(),
2999 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
3000 0,
3001 60,
3002 vec![],
3003 )
3004 .unwrap();
3005
3006 let token_x = token_x();
3007 let token_y = token_y();
3008
3009 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_000_000u64));
3010
3011 let pool_swap = pool.query_pool_swap(&QueryPoolSwapParams::new(
3012 token_x,
3013 token_y,
3014 SwapConstraint::PoolTargetPrice {
3015 target: target_price,
3016 tolerance: 0f64,
3017 min_amount_in: None,
3018 max_amount_in: None,
3019 },
3020 ));
3021
3022 assert!(pool_swap.is_err());
3023 }
3024
3025 #[test]
3026 fn test_swap_to_price_with_protocol_fees() {
3027 let liquidity = 100_000_000_000_000_000_000u128;
3028 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
3029 .expect("Failed to calculate sqrt price");
3030 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
3031
3032 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
3033
3034 let pool = UniswapV4State::new(
3036 liquidity,
3037 sqrt_price,
3038 UniswapV4Fees {
3039 zero_for_one: 1000, one_for_zero: 200, lp_fee: 3000, },
3043 tick,
3044 60,
3045 ticks,
3046 )
3047 .expect("Failed to create pool");
3048
3049 let token_x = token_x();
3050 let token_y = token_y();
3051
3052 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
3056 let pool_swap_forward = pool
3057 .query_pool_swap(&QueryPoolSwapParams::new(
3058 token_x.clone(),
3059 token_y.clone(),
3060 SwapConstraint::PoolTargetPrice {
3061 target: target_price,
3062 tolerance: 0f64,
3063 min_amount_in: None,
3064 max_amount_in: None,
3065 },
3066 ))
3067 .expect("swap_to_price failed");
3068
3069 let target_price_reverse =
3071 Price::new(BigUint::from(1_010_000u64), BigUint::from(2_040_000u64));
3072 let pool_swap_backward = pool
3073 .query_pool_swap(&QueryPoolSwapParams::new(
3074 token_y,
3075 token_x,
3076 SwapConstraint::PoolTargetPrice {
3077 target: target_price_reverse,
3078 tolerance: 0f64,
3079 min_amount_in: None,
3080 max_amount_in: None,
3081 },
3082 ))
3083 .expect("swap_to_price failed");
3084
3085 assert!(
3086 pool_swap_backward.amount_out().clone() > BigUint::ZERO,
3087 "One for zero swap should return non-zero output"
3088 );
3089
3090 assert!(
3093 pool_swap_forward.amount_out() < pool_swap_backward.amount_in(),
3094 "Backward fees should be lower therefore backward swap should be bigger"
3095 );
3096 assert!(
3097 pool_swap_forward.amount_in() < pool_swap_backward.amount_out(),
3098 "Backward fees should be lower therefore backward swap should be bigger"
3099 );
3100 }
3101
3102 #[test]
3103 fn test_swap_to_price_different_targets() {
3104 let pool = create_basic_v4_test_pool();
3106
3107 let token_x = token_x();
3108 let token_y = token_y();
3109
3110 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
3113 let pool_swap_close = pool
3114 .query_pool_swap(&QueryPoolSwapParams::new(
3115 token_x.clone(),
3116 token_y.clone(),
3117 SwapConstraint::PoolTargetPrice {
3118 target: target_price,
3119 tolerance: 0f64,
3120 min_amount_in: None,
3121 max_amount_in: None,
3122 },
3123 ))
3124 .expect("swap_to_price failed");
3125 assert!(
3126 *pool_swap_close.amount_out() > BigUint::ZERO,
3127 "Expected non-zero for 1.98 Y/X target"
3128 );
3129
3130 let target_price = Price::new(BigUint::from(1_900_000u64), BigUint::from(1_000_000u64));
3132 let pool_swap_below = pool
3133 .query_pool_swap(&QueryPoolSwapParams::new(
3134 token_x.clone(),
3135 token_y.clone(),
3136 SwapConstraint::PoolTargetPrice {
3137 target: target_price,
3138 tolerance: 0f64,
3139 min_amount_in: None,
3140 max_amount_in: None,
3141 },
3142 ))
3143 .expect("swap_to_price failed");
3144 assert!(
3145 pool_swap_below.amount_out().clone() > BigUint::ZERO,
3146 "Expected non-zero for 1.90 Y/X target"
3147 );
3148
3149 let target_price = Price::new(BigUint::from(1_500_000u64), BigUint::from(1_000_000u64));
3151 let pool_swap_far = pool
3152 .query_pool_swap(&QueryPoolSwapParams::new(
3153 token_x,
3154 token_y,
3155 SwapConstraint::PoolTargetPrice {
3156 target: target_price,
3157 tolerance: 0f64,
3158 min_amount_in: None,
3159 max_amount_in: None,
3160 },
3161 ))
3162 .expect("swap_to_price failed");
3163 assert!(
3164 pool_swap_far.amount_out().clone() > BigUint::ZERO,
3165 "Expected non-zero for 1.5 Y/X target"
3166 );
3167
3168 assert!(
3170 pool_swap_close.amount_out().clone() < pool_swap_below.amount_out().clone(),
3171 "Closer target (1.98 Y/X) should require less volume than medium target (1.90 Y/X). \
3172 Got close: {}, medium: {}",
3173 pool_swap_close.amount_out().clone(),
3174 pool_swap_below.amount_out().clone()
3175 );
3176 assert!(
3177 pool_swap_below.amount_out().clone() < pool_swap_far.amount_out().clone(),
3178 "Medium target (1.90 Y/X) should require less volume than far target (1.5 Y/X). \
3179 Got medium: {}, far: {}",
3180 pool_swap_below.amount_out().clone(),
3181 pool_swap_far.amount_out().clone()
3182 );
3183 }
3184
3185 #[test]
3186 fn test_swap_to_price_around_spot_price() {
3187 let liquidity = 10_000_000_000_000_000u128;
3188 let sqrt_price =
3189 get_sqrt_price_q96(U256::from(2_000_000_000u64), U256::from(1_000_000_000u64))
3190 .expect("Failed to calculate sqrt price");
3191 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
3192
3193 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
3194
3195 let pool = UniswapV4State::new(
3197 liquidity,
3198 sqrt_price,
3199 UniswapV4Fees {
3200 zero_for_one: 0,
3201 one_for_zero: 0,
3202 lp_fee: 500, },
3204 tick,
3205 60,
3206 ticks,
3207 )
3208 .expect("Failed to create pool");
3209
3210 let token_x = token_x();
3211 let token_y = token_y();
3212
3213 let target_price = Price::new(BigUint::from(1_999_750u64), BigUint::from(1_000_250u64));
3215
3216 let result = pool.query_pool_swap(&QueryPoolSwapParams::new(
3217 token_x.clone(),
3218 token_y.clone(),
3219 SwapConstraint::PoolTargetPrice {
3220 target: target_price,
3221 tolerance: 0f64,
3222 min_amount_in: None,
3223 max_amount_in: None,
3224 },
3225 ));
3226 assert!(result.is_err(), "Should return error when target price is unreachable");
3227
3228 let target_price = Price::new(BigUint::from(1_999_000u64), BigUint::from(1_001_000u64));
3230
3231 let pool_swap = pool
3232 .query_pool_swap(&QueryPoolSwapParams::new(
3233 token_x,
3234 token_y,
3235 SwapConstraint::PoolTargetPrice {
3236 target: target_price,
3237 tolerance: 0f64,
3238 min_amount_in: None,
3239 max_amount_in: None,
3240 },
3241 ))
3242 .expect("swap_to_price failed");
3243
3244 let expected_amount_out =
3246 BigUint::from_str("7062236922008").expect("Failed to parse expected value");
3247 assert_eq!(
3248 pool_swap.amount_out().clone(),
3249 expected_amount_out,
3250 "V4 should match V3 output with same fees (0.05%)"
3251 );
3252 }
3253
3254 #[test]
3255 fn test_swap_to_price_matches_get_amount_out() {
3256 let pool = create_basic_v4_test_pool();
3257
3258 let token_x = token_x();
3259 let token_y = token_y();
3260
3261 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
3263 let pool_swap = pool
3264 .query_pool_swap(&QueryPoolSwapParams::new(
3265 token_x.clone(),
3266 token_y.clone(),
3267 SwapConstraint::PoolTargetPrice {
3268 target: target_price,
3269 tolerance: 0f64,
3270 min_amount_in: None,
3271 max_amount_in: None,
3272 },
3273 ))
3274 .expect("swap_to_price failed");
3275 assert!(*pool_swap.amount_in() > BigUint::ZERO, "Amount in should be positive");
3276
3277 let result = pool
3279 .get_amount_out(pool_swap.amount_in().clone(), &token_x, &token_y)
3280 .expect("get_amount_out failed");
3281
3282 assert!(result.amount > BigUint::ZERO);
3285 assert!(result.amount >= *pool_swap.amount_out());
3286 }
3287
3288 #[test]
3289 fn test_swap_to_price_basic() {
3290 let liquidity = 100_000_000_000_000_000_000u128;
3291 let sqrt_price = get_sqrt_price_q96(U256::from(20_000_000u64), U256::from(10_000_000u64))
3292 .expect("Failed to calculate sqrt price");
3293 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
3294
3295 let ticks = vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(46080, 0).unwrap()];
3296
3297 let pool = UniswapV4State::new(
3298 liquidity,
3299 sqrt_price,
3300 UniswapV4Fees {
3301 zero_for_one: 0,
3302 one_for_zero: 0,
3303 lp_fee: 3000, },
3305 tick,
3306 60,
3307 ticks,
3308 )
3309 .expect("Failed to create pool");
3310
3311 let token_x = token_x();
3312 let token_y = token_y();
3313
3314 let target_price = Price::new(BigUint::from(2_000_000u64), BigUint::from(1_010_000u64));
3316
3317 let pool_swap = pool
3318 .query_pool_swap(&QueryPoolSwapParams::new(
3319 token_x,
3320 token_y,
3321 SwapConstraint::PoolTargetPrice {
3322 target: target_price,
3323 tolerance: 0f64,
3324 min_amount_in: None,
3325 max_amount_in: None,
3326 },
3327 ))
3328 .expect("swap_to_price failed");
3329
3330 let expected_amount_in = BigUint::from_str("246739021727519745").unwrap();
3332 let expected_amount_out = BigUint::from_str("490291909043340795").unwrap();
3333
3334 assert_eq!(
3335 *pool_swap.amount_in(),
3336 expected_amount_in,
3337 "amount_in should match expected value"
3338 );
3339 assert_eq!(
3340 *pool_swap.amount_out(),
3341 expected_amount_out,
3342 "amount_out should match expected value"
3343 );
3344 }
3345
3346 #[test]
3347 fn test_swap_price_limit_out_of_range_returns_error() {
3348 let pool = create_basic_v4_test_pool();
3349 let amount = -I256::from_raw(U256::from(1000u64)); let result = pool.swap(true, amount, Some(pool.sqrt_price), None);
3353 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3354
3355 let result = pool.swap(true, amount, Some(MIN_SQRT_RATIO), None);
3357 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3358
3359 let result = pool.swap(false, amount, Some(pool.sqrt_price), None);
3361 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3362
3363 let result = pool.swap(false, amount, Some(MAX_SQRT_RATIO), None);
3365 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3366 }
3367
3368 #[test]
3369 fn test_swap_at_extreme_price_returns_error() {
3370 let sqrt_price = MIN_SQRT_RATIO + U256::from(1u64);
3373 let tick = get_tick_at_sqrt_ratio(sqrt_price).expect("Failed to calculate tick");
3374 let aligned_tick = (MIN_TICK / 60) * 60 + 60; let ticks = vec![
3377 TickInfo::new(aligned_tick, 0).unwrap(),
3378 TickInfo::new(aligned_tick + 60, 0).unwrap(),
3379 ];
3380 let pool = UniswapV4State::new(
3381 100_000_000_000_000_000_000u128,
3382 sqrt_price,
3383 UniswapV4Fees { zero_for_one: 0, one_for_zero: 0, lp_fee: 3000 },
3384 tick,
3385 60,
3386 ticks,
3387 )
3388 .unwrap();
3389
3390 let amount = -I256::from_raw(U256::from(1000u64));
3391 let result = pool.swap(true, amount, None, None);
3393 assert!(matches!(result, Err(SimulationError::InvalidInput(_, None))));
3394 }
3395
3396 #[test]
3397 fn test_serde_roundtrip_via_protocol_sim_typetag() {
3398 let pool = create_basic_v4_test_pool();
3399
3400 let json =
3401 serde_json::to_string(&pool as &dyn ProtocolSim).expect("state should serialize");
3402 let deserialized: Box<dyn ProtocolSim> =
3403 serde_json::from_str(&json).expect("state should deserialize");
3404
3405 assert!(deserialized.eq(&pool));
3406 }
3407
3408 #[test]
3409 fn test_serialize_with_hook() {
3410 let mut pool = create_basic_v4_test_pool();
3411 let handler = AngstromHookHandler::new(
3412 Address::from_str("0x0000000aa232009084bd71a5797d089aa4edfad4").unwrap(),
3413 Address::from_str("0x000000000004444c5dc75cb358380d2e3de08a90").unwrap(),
3414 AngstromFees { unlock: U24::from(238), protocol_unlock: U24::from(112) },
3415 false,
3416 );
3417 pool.set_hook_handler(Box::new(handler));
3418
3419 let result = serde_json::to_string(&pool);
3420 assert!(result
3421 .unwrap_err()
3422 .to_string()
3423 .contains("HookHandler is a trait object"));
3424 }
3425
3426 #[test]
3427 fn test_deserialize_negative_tick_spacing() {
3428 let pool = UniswapV4State::new(
3429 1_000,
3430 U256::from(1u64) << 96,
3431 UniswapV4Fees::new(0, 0, 3000),
3432 0,
3433 60,
3434 vec![TickInfo::new(0, 0).unwrap(), TickInfo::new(60, 0).unwrap()],
3435 )
3436 .expect("valid state");
3437 let mut json = serde_json::to_value(&pool).expect("hookless state should serialize");
3438 json["tick_spacing"] = (-60).into();
3439
3440 let err = serde_json::from_value::<UniswapV4State>(json).unwrap_err();
3441
3442 assert!(
3443 err.to_string()
3444 .contains("must be positive"),
3445 "unexpected error: {err}"
3446 );
3447 }
3448}
3449
3450#[cfg(test)]
3451mod step_cache_tests {
3452 use std::{
3453 collections::{HashMap, HashSet},
3454 str::FromStr,
3455 };
3456
3457 use tycho_common::{dto::ProtocolStateDelta, hex_bytes::Bytes};
3458
3459 use super::*;
3460 use crate::evm::protocol::utils::uniswap::swap_step_cache::test_fixtures::{
3461 amount_orders, test_amounts, test_ticks, wbtc_weth, LIQUIDITY, SQRT_PRICE, TICK,
3462 };
3463
3464 fn test_pool_with_fees(fees: UniswapV4Fees) -> UniswapV4State {
3465 UniswapV4State::new(
3466 LIQUIDITY,
3467 U256::from_str(SQRT_PRICE).unwrap(),
3468 fees,
3469 TICK,
3470 10,
3471 test_ticks(),
3472 )
3473 .unwrap()
3474 }
3475
3476 fn test_pool() -> UniswapV4State {
3477 test_pool_with_fees(UniswapV4Fees::new(0, 0, 500))
3478 }
3479
3480 fn format_quote_result(
3482 pool: &UniswapV4State,
3483 amount: &BigUint,
3484 sell: &Token,
3485 buy: &Token,
3486 ) -> String {
3487 match pool.get_amount_out(amount.clone(), sell, buy) {
3488 Ok(result) => {
3489 let state = result
3490 .new_state
3491 .as_any()
3492 .downcast_ref::<UniswapV4State>()
3493 .unwrap();
3494 format!(
3495 "{} {} {} {} {}",
3496 result.amount,
3497 result.gas,
3498 state.sqrt_price,
3499 state.tick.value(),
3500 state.liquidity
3501 )
3502 }
3503 Err(error) => format!("error {error}"),
3504 }
3505 }
3506
3507 #[test]
3508 fn test_step_cache_quotes_match_a_fresh_state_in_any_order() {
3509 let (wbtc, weth) = wbtc_weth();
3510 for fees in [UniswapV4Fees::new(0, 0, 500), UniswapV4Fees::new(100, 90, 3000)] {
3512 for (sell, buy, smallest) in [(&wbtc, &weth, 1_000u64), (&weth, &wbtc, 1_000_000_000)] {
3513 for order in amount_orders(&test_amounts(smallest)) {
3514 let shared = test_pool_with_fees(fees.clone());
3515 for amount in &order {
3516 assert_eq!(
3517 format_quote_result(&shared, amount, sell, buy),
3518 format_quote_result(
3519 &test_pool_with_fees(fees.clone()),
3520 amount,
3521 sell,
3522 buy
3523 ),
3524 "selling {amount} {} with {fees:?}",
3525 sell.symbol
3526 );
3527 }
3528 let steps = shared
3529 .step_cache
3530 .min_inputs(sell < buy)
3531 .len();
3532 assert!(steps > 2, "only {steps} steps cached");
3533 }
3534 }
3535 }
3536 }
3537
3538 #[test]
3539 fn test_step_cache_quotes_match_a_fresh_state_at_each_cached_step() {
3540 let (wbtc, weth) = wbtc_weth();
3541 for (sell, buy, doublings) in [(&wbtc, &weth, 30u32), (&weth, &wbtc, 70)] {
3544 let cached = test_pool();
3545 let _ = cached.get_amount_out(BigUint::from(10u32) << doublings, sell, buy);
3546 let boundaries = cached.step_cache.min_inputs(sell < buy);
3547 assert!(boundaries.len() > 2, "only {} steps cached", boundaries.len());
3548 for boundary in boundaries.into_iter().skip(1) {
3549 for amount in [boundary - U256::from(1u64), boundary, boundary + U256::from(1u64)] {
3550 let amount = u256_to_biguint(amount);
3551 assert_eq!(
3552 format_quote_result(&cached, &amount, sell, buy),
3553 format_quote_result(&test_pool(), &amount, sell, buy),
3554 "selling {amount} {}",
3555 sell.symbol
3556 );
3557 }
3558 }
3559 }
3560 }
3561
3562 #[test]
3563 fn test_step_cache_restarts_after_delta_transition() {
3564 let (wbtc, weth) = wbtc_weth();
3565 let large = BigUint::from(3_000_000_000u64);
3566 let delta = || ProtocolStateDelta {
3569 component_id: "State1".to_owned(),
3570 updated_attributes: HashMap::from([(
3571 "ticks/255820/net_liquidity".to_string(),
3572 Bytes::from(
3573 1_000_000_000_000_000i128
3574 .to_be_bytes()
3575 .to_vec(),
3576 ),
3577 )]),
3578 deleted_attributes: HashSet::new(),
3579 };
3580 let mut cached = test_pool();
3581 cached
3582 .get_amount_out(large.clone(), &wbtc, &weth)
3583 .unwrap();
3584 let mut fresh = test_pool();
3585
3586 cached
3587 .delta_transition(delta(), &HashMap::new(), &Balances::default())
3588 .unwrap();
3589 fresh
3590 .delta_transition(delta(), &HashMap::new(), &Balances::default())
3591 .unwrap();
3592
3593 assert_eq!(
3594 format_quote_result(&cached, &large, &wbtc, &weth),
3595 format_quote_result(&fresh, &large, &wbtc, &weth)
3596 );
3597 }
3598}