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