1use std::{any::Any, collections::HashMap, sync::Arc};
2
3use alloy::primitives::{Sign, I256, U256};
4use num_bigint::BigUint;
5use serde::{Deserialize, Serialize};
6use tracing::trace;
7use tycho_common::{
8 dto::ProtocolStateDelta,
9 models::token::Token,
10 simulation::{
11 errors::{SimulationError, TransitionError},
12 protocol_sim::{
13 Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
14 SwapConstraint,
15 },
16 },
17 Bytes,
18};
19
20use crate::evm::protocol::{
21 clmm::clmm_swap_to_price,
22 safe_math::{safe_add_u256, safe_sub_u256},
23 u256_num::u256_to_biguint,
24 utils::{
25 add_fee_markup,
26 uniswap::{
27 liquidity_math,
28 pool_tick::PoolTick,
29 sqrt_price_math::{get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64},
30 swap_math::{self, SwapStepResult},
31 swap_step_cache::{StepCacheKey, StepCacheRun, SwapStepCache},
32 tick_list::{TickInfo, TickList, TickListErrorKind},
33 tick_math::{
34 get_sqrt_ratio_at_tick_cached, MAX_SQRT_RATIO, MAX_TICK, MIN_SQRT_RATIO, MIN_TICK,
35 },
36 StepComputation, SwapResults, SwapState,
37 },
38 },
39};
40
41const SWAP_BASE_GAS: u64 = 78_000;
56const GAS_PER_BITMAP_WORD: u64 = 2_100;
58const GAS_PER_SWAP_MATH_STEP: u64 = 5_400;
60const GAS_PER_INITIALIZED_TICK_CROSS: u64 = 35_000;
64const V3_CALLBACK_SETTLEMENT_GAS: u64 = 70_000;
66const MAX_SWAP_GAS: u64 = 16_700_000;
68const MAX_TICKS_CROSSED: u64 = (MAX_SWAP_GAS - SWAP_BASE_GAS) / GAS_PER_INITIALIZED_TICK_CROSS;
69
70#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
76pub struct RamsesV3State {
77 liquidity: u128,
78 sqrt_price: U256,
79 fee: u32,
80 tick: PoolTick,
81 tick_spacing: u16,
82 ticks: Arc<TickList>,
86 #[serde(skip)]
91 step_cache: SwapStepCache,
92}
93
94impl RamsesV3State {
95 pub fn new(
105 liquidity: u128,
106 sqrt_price: U256,
107 fee: u32,
108 tick_spacing: u16,
109 tick: i32,
110 ticks: Vec<TickInfo>,
111 ) -> Result<Self, SimulationError> {
112 let tick_list = TickList::from(tick_spacing, ticks)?;
113 Ok(RamsesV3State {
114 liquidity,
115 sqrt_price,
116 fee,
117 tick: tick.into(),
118 tick_spacing,
119 ticks: Arc::new(tick_list),
120 step_cache: SwapStepCache::default(),
121 })
122 }
123
124 fn clone_post_swap(&self, sqrt_price: U256, tick: PoolTick, liquidity: u128) -> Self {
127 let mut state = self.clone();
128 state.sqrt_price = sqrt_price;
129 state.tick = tick;
130 state.liquidity = liquidity;
131 state.step_cache = SwapStepCache::default();
132 state
133 }
134
135 fn swap(
136 &self,
137 zero_for_one: bool,
138 amount_specified: I256,
139 sqrt_price_limit: Option<U256>,
140 ) -> Result<SwapResults, SimulationError> {
141 if !self.ticks.has_initialized_ticks() {
142 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
143 }
144 let price_limit = if let Some(limit) = sqrt_price_limit {
145 limit
146 } else if zero_for_one {
147 safe_add_u256(MIN_SQRT_RATIO, U256::ONE)?
148 } else {
149 safe_sub_u256(MAX_SQRT_RATIO, U256::ONE)?
150 };
151
152 let price_limit_valid = if zero_for_one {
153 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
154 } else {
155 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
156 };
157 if !price_limit_valid {
158 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
159 }
160
161 let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
162
163 let mut state = SwapState {
164 amount_remaining: amount_specified,
165 amount_calculated: I256::from_raw(U256::from(0u64)),
166 sqrt_price: self.sqrt_price,
167 tick: self.tick.value().into(),
168 liquidity: self.liquidity,
169 };
170 let mut gas_used = U256::from(SWAP_BASE_GAS);
171
172 let cache_key = if exact_input && sqrt_price_limit.is_none() {
176 StepCacheKey::new(
177 self.sqrt_price,
178 state.tick.value(),
179 self.liquidity,
180 self.fee,
181 gas_used,
182 )
183 } else {
184 None
185 };
186 let mut cache_run = StepCacheRun::new(
187 &self.step_cache,
188 zero_for_one,
189 cache_key,
190 amount_specified.unsigned_abs(),
191 );
192 if let Some(progress) = cache_run.apply_cached_progress(&mut state) {
193 gas_used = progress.gas;
194 }
195 while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
196 state.sqrt_price != price_limit
197 {
198 let (mut next_tick, next_tick_info) = match self
199 .ticks
200 .next_initialized_tick_within_one_word(state.tick.value(), zero_for_one)
201 {
202 Ok((tick, info)) => {
203 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_BITMAP_WORD))?;
204 (tick, info)
205 }
206 Err(tick_err) => match tick_err.kind {
207 TickListErrorKind::TicksExeeded => {
208 let new_state = self.clone_post_swap(
209 state.sqrt_price,
210 state.tick.clone(),
211 state.liquidity,
212 );
213 return Err(SimulationError::InvalidInput(
214 "Ticks exceeded".into(),
215 Some(GetAmountOutResult::new(
216 u256_to_biguint(state.amount_calculated.abs().into_raw()),
217 u256_to_biguint(gas_used),
218 Box::new(new_state),
219 )),
220 ));
221 }
222 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
223 },
224 };
225
226 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
227
228 let sqrt_price_start = state.sqrt_price;
229 let sqrt_price_next = match next_tick_info {
230 Some(info) => info.sqrt_price,
231 None => get_sqrt_ratio_at_tick_cached(next_tick)?,
232 };
233 let SwapStepResult { sqrt_price, amount_in_with_fee, amount_out } =
234 swap_math::compute_swap_step(
235 state.sqrt_price,
236 RamsesV3State::get_sqrt_ratio_target(
237 sqrt_price_next,
238 price_limit,
239 zero_for_one,
240 ),
241 state.liquidity,
242 state.amount_remaining,
243 self.fee,
244 )?;
245 state.sqrt_price = sqrt_price;
246
247 let step = StepComputation {
248 sqrt_price_start,
249 tick_next: next_tick,
250 sqrt_price_next,
251 amount_in_with_fee,
252 amount_out,
253 };
254
255 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_SWAP_MATH_STEP))?;
256
257 if exact_input {
258 state.amount_remaining -=
259 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_in_with_fee)
260 .unwrap();
261 state.amount_calculated -=
262 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
263 } else {
264 state.amount_remaining +=
265 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
266 state.amount_calculated +=
267 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_in_with_fee)
268 .unwrap();
269 }
270 if state.sqrt_price == step.sqrt_price_next {
271 if let Some(info) = next_tick_info {
272 let liquidity_raw = info.net_liquidity;
273 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
274 state.liquidity =
275 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
276 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_INITIALIZED_TICK_CROSS))?;
277 }
278 state.tick =
279 PoolTick::from(if zero_for_one { step.tick_next - 1 } else { step.tick_next });
280 } else if state.sqrt_price != step.sqrt_price_start {
281 state.tick = PoolTick::at_sqrt_price(state.sqrt_price)?;
282 }
283 cache_run.cache_progress(&step, &state, gas_used);
284 }
285 Ok(SwapResults {
286 amount_calculated: state.amount_calculated,
287 amount_specified,
288 amount_remaining: state.amount_remaining,
289 sqrt_price: state.sqrt_price,
290 liquidity: state.liquidity,
291 tick: state.tick,
292 gas_used: safe_add_u256(gas_used, U256::from(V3_CALLBACK_SETTLEMENT_GAS))?,
293 })
294 }
295
296 fn get_sqrt_ratio_target(
297 sqrt_price_next: U256,
298 sqrt_price_limit: U256,
299 zero_for_one: bool,
300 ) -> U256 {
301 let cond1 = if zero_for_one {
302 sqrt_price_next < sqrt_price_limit
303 } else {
304 sqrt_price_next > sqrt_price_limit
305 };
306
307 if cond1 {
308 sqrt_price_limit
309 } else {
310 sqrt_price_next
311 }
312 }
313}
314
315#[typetag::serde]
316impl ProtocolSim for RamsesV3State {
317 fn fee(&self) -> f64 {
318 self.fee as f64 / 1_000_000.0
319 }
320
321 fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
322 let price = if a < b {
323 sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)?
324 } else {
325 1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)?
326 };
327 Ok(add_fee_markup(price, self.fee()))
328 }
329
330 fn get_amount_out(
331 &self,
332 amount_in: BigUint,
333 token_a: &Token,
334 token_b: &Token,
335 ) -> Result<GetAmountOutResult, SimulationError> {
336 let zero_for_one = token_a < token_b;
337 let amount_specified = U256::try_from_be_slice(&amount_in.to_bytes_be())
338 .and_then(|unsigned_amount_in| {
339 I256::checked_from_sign_and_abs(Sign::Positive, unsigned_amount_in)
340 })
341 .ok_or_else(|| {
342 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
343 })?;
344
345 let result = self.swap(zero_for_one, amount_specified, None)?;
346
347 trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "RAMSES V3 SWAP");
348 let new_state = self.clone_post_swap(result.sqrt_price, result.tick, result.liquidity);
349
350 Ok(GetAmountOutResult::new(
351 u256_to_biguint(
352 result
353 .amount_calculated
354 .saturating_abs()
355 .into_raw(),
356 ),
357 u256_to_biguint(result.gas_used),
358 Box::new(new_state),
359 ))
360 }
361
362 fn get_limits(
363 &self,
364 token_in: Bytes,
365 token_out: Bytes,
366 ) -> Result<(BigUint, BigUint), SimulationError> {
367 if !self.ticks.has_initialized_ticks() {
368 return Ok((BigUint::ZERO, BigUint::ZERO));
369 }
370
371 let zero_for_one = token_in < token_out;
372 let mut current_tick = self.tick.value();
373 let mut current_sqrt_price = self.sqrt_price;
374 let mut current_liquidity = self.liquidity;
375 let mut total_amount_in = U256::ZERO;
376 let mut total_amount_out = U256::ZERO;
377 let mut ticks_crossed: u64 = 0;
378
379 while let Ok((tick, next_tick_info)) = self
382 .ticks
383 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
384 {
385 if ticks_crossed == MAX_TICKS_CROSSED {
387 break;
388 }
389 ticks_crossed += 1;
390
391 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
393
394 let sqrt_price_next = match next_tick_info {
395 Some(info) => info.sqrt_price,
396 None => get_sqrt_ratio_at_tick_cached(next_tick)?,
397 };
398
399 let (amount_in, amount_out) = if zero_for_one {
402 let amount0 = get_amount0_delta(
403 sqrt_price_next,
404 current_sqrt_price,
405 current_liquidity,
406 true,
407 )?;
408 let amount1 = get_amount1_delta(
409 sqrt_price_next,
410 current_sqrt_price,
411 current_liquidity,
412 false,
413 )?;
414 (amount0, amount1)
415 } else {
416 let amount0 = get_amount0_delta(
417 sqrt_price_next,
418 current_sqrt_price,
419 current_liquidity,
420 false,
421 )?;
422 let amount1 = get_amount1_delta(
423 sqrt_price_next,
424 current_sqrt_price,
425 current_liquidity,
426 true,
427 )?;
428 (amount1, amount0)
429 };
430
431 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
433 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
434
435 if let Some(info) = next_tick_info {
440 let liquidity_raw = info.net_liquidity;
441 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
442
443 match liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta) {
446 Ok(new_liquidity) => {
447 current_liquidity = new_liquidity;
448 }
449 Err(_) => {
450 break;
453 }
454 }
455 }
456
457 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
459 current_sqrt_price = sqrt_price_next;
460 }
461
462 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
463 }
464
465 fn delta_transition(
466 &mut self,
467 mut delta: ProtocolStateDelta,
468 _tokens: &HashMap<Bytes, Token>,
469 _balances: &Balances,
470 ) -> Result<(), TransitionError> {
471 self.step_cache = SwapStepCache::default();
473 if let Some(liquidity) = delta
475 .updated_attributes
476 .remove("liquidity")
477 {
478 self.liquidity = liquidity.into();
479 }
480 if let Some(sqrt_price) = delta
481 .updated_attributes
482 .get("sqrt_price_x96")
483 {
484 self.sqrt_price = U256::from_be_slice(sqrt_price);
485 }
486 if let Some(tick) = delta.updated_attributes.remove("tick") {
487 self.tick = i32::from(tick).into();
488 }
489 if let Some(fee) = delta.updated_attributes.remove("fee") {
491 self.fee = fee.into();
492 }
493
494 for (key, value) in delta.updated_attributes {
496 let Some(tick) = key.strip_prefix("ticks/") else {
497 continue;
498 };
499
500 Arc::make_mut(&mut self.ticks)
501 .set_tick_liquidity(
502 tick.parse::<i32>()
503 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
504 i128::from(value),
505 )
506 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
507 }
508 for key in delta.deleted_attributes {
510 let Some(tick) = key.strip_prefix("ticks/") else {
511 continue;
512 };
513
514 Arc::make_mut(&mut self.ticks)
515 .set_tick_liquidity(
516 tick.parse::<i32>()
517 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
518 0,
519 )
520 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
521 }
522 Ok(())
523 }
524
525 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
530 if !self.ticks.has_initialized_ticks() {
531 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
532 }
533
534 match params.swap_constraint() {
535 SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
536 "Ramses V3 does not support TradeLimitPrice constraint in query_pool_swap"
537 .to_string(),
538 None,
539 )),
540 SwapConstraint::PoolTargetPrice {
541 target,
542 tolerance: _,
543 min_amount_in: _,
544 max_amount_in: _,
545 } => {
546 let (amount_in, amount_out, swap_result) = clmm_swap_to_price(
547 self.sqrt_price,
548 ¶ms.token_in().address,
549 ¶ms.token_out().address,
550 target,
551 self.fee,
552 Sign::Positive,
553 |zero_for_one, amount_specified, sqrt_price_limit| {
554 self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit))
555 },
556 )?;
557
558 let new_state = self.clone_post_swap(
559 swap_result.sqrt_price,
560 swap_result.tick,
561 swap_result.liquidity,
562 );
563
564 Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
565 }
566 }
567 }
568
569 fn clone_box(&self) -> Box<dyn ProtocolSim> {
570 Box::new(self.clone())
571 }
572
573 fn as_any(&self) -> &dyn Any {
574 self
575 }
576
577 fn as_any_mut(&mut self) -> &mut dyn Any {
578 self
579 }
580
581 fn eq(&self, other: &dyn ProtocolSim) -> bool {
582 let Some(RamsesV3State {
583 liquidity,
584 sqrt_price,
585 fee,
586 tick,
587 tick_spacing,
588 ticks,
589 step_cache: _,
590 }) = other
591 .as_any()
592 .downcast_ref::<RamsesV3State>()
593 else {
594 return false;
595 };
596
597 &self.liquidity == liquidity &&
598 &self.sqrt_price == sqrt_price &&
599 &self.fee == fee &&
600 &self.tick == tick &&
601 &self.tick_spacing == tick_spacing &&
602 &self.ticks == ticks
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use std::{
609 collections::{HashMap, HashSet},
610 str::FromStr,
611 };
612
613 use num_bigint::ToBigUint;
614 use tycho_common::models::Chain;
615
616 use super::*;
617
618 #[test]
622 fn test_get_amount_out_math_parity() {
623 let wbtc = Token::new(
624 &Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(),
625 "WBTC",
626 8,
627 0,
628 &[Some(10_000)],
629 Chain::Ethereum,
630 100,
631 );
632 let weth = Token::new(
633 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
634 "WETH",
635 18,
636 0,
637 &[Some(10_000)],
638 Chain::Ethereum,
639 100,
640 );
641 let pool = RamsesV3State::new(
642 377952820878029838,
643 U256::from_str("28437325270877025820973479874632004").unwrap(),
644 500,
645 10,
646 255830,
647 vec![
648 TickInfo::new(255760, 1759015528199933i128).unwrap(),
649 TickInfo::new(255770, 6393138051835308i128).unwrap(),
650 TickInfo::new(255780, 228206673808681i128).unwrap(),
651 TickInfo::new(255820, 1319490609195820i128).unwrap(),
652 TickInfo::new(255830, 678916926147901i128).unwrap(),
653 TickInfo::new(255840, 12208947683433103i128).unwrap(),
654 TickInfo::new(255850, 1177970713095301i128).unwrap(),
655 TickInfo::new(255860, 8752304680520407i128).unwrap(),
656 TickInfo::new(255880, 1486478248067104i128).unwrap(),
657 TickInfo::new(255890, 1878744276123248i128).unwrap(),
658 TickInfo::new(255900, 77340284046725227i128).unwrap(),
659 ],
660 )
661 .unwrap();
662
663 let res = pool
664 .get_amount_out(500000000.to_biguint().unwrap(), &wbtc, &weth)
665 .unwrap();
666
667 assert_eq!(res.amount, BigUint::from_str("64352395915550406461").unwrap());
668 }
669
670 #[test]
671 fn test_delta_transition_updates_mutable_fee() {
672 let mut pool = RamsesV3State::new(
673 1000,
674 U256::from_str("1000").unwrap(),
675 500,
676 10,
677 100,
678 vec![TickInfo::new(255760, 10000).unwrap(), TickInfo::new(255900, -10000).unwrap()],
679 )
680 .unwrap();
681
682 let attributes: HashMap<String, Bytes> = [
683 ("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
684 ("fee".to_string(), Bytes::from(3000_u32.to_be_bytes().to_vec())),
687 ]
688 .into_iter()
689 .collect();
690 let delta = ProtocolStateDelta {
691 component_id: "State1".to_owned(),
692 updated_attributes: attributes,
693 deleted_attributes: HashSet::new(),
694 };
695
696 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
697 .unwrap();
698
699 assert_eq!(pool.liquidity, 2000);
700 assert_eq!(pool.fee, 3000);
701 assert_eq!(pool.fee(), 0.003);
702 }
703}
704
705#[cfg(test)]
706mod tick_list_sharing_tests {
707 use std::collections::{HashMap, HashSet};
708
709 use tycho_common::{dto::ProtocolStateDelta, hex_bytes::Bytes};
710
711 use super::*;
712
713 #[test]
714 fn test_delta_transition_leaves_clones_ticks_unchanged() {
715 let original = RamsesV3State::new(
716 1000,
717 U256::from(1000u64),
718 500,
719 10,
720 100,
721 vec![TickInfo::new(255760, 10000).unwrap(), TickInfo::new(255900, -10000).unwrap()],
722 )
723 .unwrap();
724 let mut updated = original.clone();
725 let delta = ProtocolStateDelta {
726 component_id: "State1".to_owned(),
727 updated_attributes: HashMap::from([(
728 "ticks/255760".to_string(),
729 Bytes::from(20000_i128.to_be_bytes().to_vec()),
730 )]),
731 deleted_attributes: HashSet::new(),
732 };
733
734 updated
735 .delta_transition(delta, &HashMap::new(), &Balances::default())
736 .unwrap();
737
738 assert_eq!(
739 updated
740 .ticks
741 .get_tick(255760)
742 .unwrap()
743 .net_liquidity,
744 20000
745 );
746 assert_eq!(
747 original
748 .ticks
749 .get_tick(255760)
750 .unwrap()
751 .net_liquidity,
752 10000
753 );
754 }
755}
756
757#[cfg(test)]
758mod step_cache_tests {
759 use std::{
760 collections::{HashMap, HashSet},
761 str::FromStr,
762 };
763
764 use tycho_common::{dto::ProtocolStateDelta, hex_bytes::Bytes};
765
766 use super::*;
767 use crate::evm::protocol::utils::uniswap::swap_step_cache::test_fixtures::{
768 amount_orders, test_amounts, test_ticks, wbtc_weth, LIQUIDITY, SQRT_PRICE, TICK,
769 };
770
771 fn test_pool() -> RamsesV3State {
772 RamsesV3State::new(
773 LIQUIDITY,
774 U256::from_str(SQRT_PRICE).unwrap(),
775 500,
776 10,
777 TICK,
778 test_ticks(),
779 )
780 .unwrap()
781 }
782
783 fn format_quote_result(
785 pool: &RamsesV3State,
786 amount: &BigUint,
787 sell: &Token,
788 buy: &Token,
789 ) -> String {
790 match pool.get_amount_out(amount.clone(), sell, buy) {
791 Ok(result) => {
792 let state = result
793 .new_state
794 .as_any()
795 .downcast_ref::<RamsesV3State>()
796 .unwrap();
797 format!(
798 "{} {} {} {} {}",
799 result.amount,
800 result.gas,
801 state.sqrt_price,
802 state.tick.value(),
803 state.liquidity
804 )
805 }
806 Err(error) => format!("error {error}"),
807 }
808 }
809
810 #[test]
811 fn test_step_cache_quotes_match_a_fresh_state_in_any_order() {
812 let (wbtc, weth) = wbtc_weth();
813 for (sell, buy, smallest) in [(&wbtc, &weth, 1_000u64), (&weth, &wbtc, 1_000_000_000)] {
814 for order in amount_orders(&test_amounts(smallest)) {
815 let shared = test_pool();
816 for amount in &order {
817 assert_eq!(
818 format_quote_result(&shared, amount, sell, buy),
819 format_quote_result(&test_pool(), amount, sell, buy),
820 "selling {amount} {}",
821 sell.symbol
822 );
823 }
824 let steps = shared
825 .step_cache
826 .min_inputs(sell < buy)
827 .len();
828 assert!(steps > 2, "only {steps} steps cached");
829 }
830 }
831 }
832
833 #[test]
834 fn test_step_cache_quotes_match_a_fresh_state_at_each_cached_step() {
835 let (wbtc, weth) = wbtc_weth();
836 for (sell, buy, doublings) in [(&wbtc, &weth, 30u32), (&weth, &wbtc, 70)] {
839 let cached = test_pool();
840 let _ = cached.get_amount_out(BigUint::from(10u32) << doublings, sell, buy);
841 let boundaries = cached.step_cache.min_inputs(sell < buy);
842 assert!(boundaries.len() > 2, "only {} steps cached", boundaries.len());
843 for boundary in boundaries.into_iter().skip(1) {
844 for amount in [boundary - U256::from(1u64), boundary, boundary + U256::from(1u64)] {
845 let amount = u256_to_biguint(amount);
846 assert_eq!(
847 format_quote_result(&cached, &amount, sell, buy),
848 format_quote_result(&test_pool(), &amount, sell, buy),
849 "selling {amount} {}",
850 sell.symbol
851 );
852 }
853 }
854 }
855 }
856
857 #[test]
858 fn test_step_cache_of_a_swapped_state() {
859 let (wbtc, weth) = wbtc_weth();
860 let pool = test_pool();
861 let swapped = pool
862 .get_amount_out(BigUint::from(300_000_000u64), &wbtc, &weth)
863 .unwrap()
864 .new_state;
865 let swapped = swapped
866 .as_any()
867 .downcast_ref::<RamsesV3State>()
868 .unwrap();
869 let fresh: RamsesV3State =
871 serde_json::from_value(serde_json::to_value(swapped).unwrap()).unwrap();
872
873 for amount in test_amounts(1_000).iter().rev() {
874 assert_eq!(
875 format_quote_result(swapped, amount, &wbtc, &weth),
876 format_quote_result(&fresh, amount, &wbtc, &weth),
877 "selling {amount} WBTC"
878 );
879 }
880 }
881
882 #[test]
883 fn test_step_cache_restarts_after_delta_transition() {
884 let (wbtc, weth) = wbtc_weth();
885 let large = BigUint::from(3_000_000_000u64);
886 let delta = || ProtocolStateDelta {
889 component_id: "State1".to_owned(),
890 updated_attributes: HashMap::from([(
891 "ticks/255820".to_string(),
892 Bytes::from(
893 1_000_000_000_000_000i128
894 .to_be_bytes()
895 .to_vec(),
896 ),
897 )]),
898 deleted_attributes: HashSet::new(),
899 };
900 let mut cached = test_pool();
901 cached
902 .get_amount_out(large.clone(), &wbtc, &weth)
903 .unwrap();
904 let mut fresh = test_pool();
905
906 cached
907 .delta_transition(delta(), &HashMap::new(), &Balances::default())
908 .unwrap();
909 fresh
910 .delta_transition(delta(), &HashMap::new(), &Balances::default())
911 .unwrap();
912
913 assert_eq!(
914 format_quote_result(&cached, &large, &wbtc, &weth),
915 format_quote_result(&fresh, &large, &wbtc, &weth)
916 );
917 }
918}