1use std::{any::Any, collections::HashMap};
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 sqrt_price_math::{get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64},
29 swap_math,
30 tick_list::{TickInfo, TickList, TickListErrorKind},
31 tick_math::{
32 get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio, MAX_SQRT_RATIO, MAX_TICK,
33 MIN_SQRT_RATIO, MIN_TICK,
34 },
35 StepComputation, SwapResults, SwapState,
36 },
37 },
38};
39
40const SWAP_BASE_GAS: u64 = 78_000;
55const GAS_PER_BITMAP_WORD: u64 = 2_100;
57const GAS_PER_SWAP_MATH_STEP: u64 = 5_400;
59const GAS_PER_INITIALIZED_TICK_CROSS: u64 = 35_000;
63const V3_CALLBACK_SETTLEMENT_GAS: u64 = 70_000;
65const MAX_SWAP_GAS: u64 = 16_700_000;
67const MAX_TICKS_CROSSED: u64 = (MAX_SWAP_GAS - SWAP_BASE_GAS) / GAS_PER_INITIALIZED_TICK_CROSS;
68
69#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
75pub struct RamsesV3State {
76 liquidity: u128,
77 sqrt_price: U256,
78 fee: u32,
79 tick: i32,
80 tick_spacing: u16,
81 ticks: TickList,
82}
83
84impl RamsesV3State {
85 pub fn new(
95 liquidity: u128,
96 sqrt_price: U256,
97 fee: u32,
98 tick_spacing: u16,
99 tick: i32,
100 ticks: Vec<TickInfo>,
101 ) -> Result<Self, SimulationError> {
102 let tick_list = TickList::from(tick_spacing, ticks)?;
103 Ok(RamsesV3State { liquidity, sqrt_price, fee, tick, tick_spacing, ticks: tick_list })
104 }
105
106 fn swap(
107 &self,
108 zero_for_one: bool,
109 amount_specified: I256,
110 sqrt_price_limit: Option<U256>,
111 ) -> Result<SwapResults, SimulationError> {
112 if !self.ticks.has_initialized_ticks() {
113 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
114 }
115 let price_limit = if let Some(limit) = sqrt_price_limit {
116 limit
117 } else if zero_for_one {
118 safe_add_u256(MIN_SQRT_RATIO, U256::ONE)?
119 } else {
120 safe_sub_u256(MAX_SQRT_RATIO, U256::ONE)?
121 };
122
123 let price_limit_valid = if zero_for_one {
124 price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
125 } else {
126 price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
127 };
128 if !price_limit_valid {
129 return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
130 }
131
132 let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
133
134 let mut state = SwapState {
135 amount_remaining: amount_specified,
136 amount_calculated: I256::from_raw(U256::from(0u64)),
137 sqrt_price: self.sqrt_price,
138 tick: self.tick,
139 liquidity: self.liquidity,
140 };
141 let mut gas_used = U256::from(SWAP_BASE_GAS);
142
143 while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
144 state.sqrt_price != price_limit
145 {
146 let (mut next_tick, initialized) = match self
147 .ticks
148 .next_initialized_tick_within_one_word(state.tick, zero_for_one)
149 {
150 Ok((tick, init)) => {
151 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_BITMAP_WORD))?;
152 (tick, init)
153 }
154 Err(tick_err) => match tick_err.kind {
155 TickListErrorKind::TicksExeeded => {
156 let mut new_state = self.clone();
157 new_state.liquidity = state.liquidity;
158 new_state.tick = state.tick;
159 new_state.sqrt_price = state.sqrt_price;
160 return Err(SimulationError::InvalidInput(
161 "Ticks exceeded".into(),
162 Some(GetAmountOutResult::new(
163 u256_to_biguint(state.amount_calculated.abs().into_raw()),
164 u256_to_biguint(gas_used),
165 Box::new(new_state),
166 )),
167 ));
168 }
169 _ => return Err(SimulationError::FatalError("Unknown error".to_string())),
170 },
171 };
172
173 next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
174
175 let sqrt_price_start = state.sqrt_price;
176 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
177 let (sqrt_price, amount_in, amount_out, fee_amount) = swap_math::compute_swap_step(
178 state.sqrt_price,
179 RamsesV3State::get_sqrt_ratio_target(sqrt_price_next, price_limit, zero_for_one),
180 state.liquidity,
181 state.amount_remaining,
182 self.fee,
183 )?;
184 state.sqrt_price = sqrt_price;
185
186 let step = StepComputation {
187 sqrt_price_start,
188 tick_next: next_tick,
189 initialized,
190 sqrt_price_next,
191 amount_in,
192 amount_out,
193 fee_amount,
194 };
195
196 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_SWAP_MATH_STEP))?;
197
198 if exact_input {
199 state.amount_remaining -= I256::checked_from_sign_and_abs(
200 Sign::Positive,
201 safe_add_u256(step.amount_in, step.fee_amount)?,
202 )
203 .unwrap();
204 state.amount_calculated -=
205 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
206 } else {
207 state.amount_remaining +=
208 I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
209 state.amount_calculated += I256::checked_from_sign_and_abs(
210 Sign::Positive,
211 safe_add_u256(step.amount_in, step.fee_amount)?,
212 )
213 .unwrap();
214 }
215 if state.sqrt_price == step.sqrt_price_next {
216 if step.initialized {
217 let liquidity_raw = self
218 .ticks
219 .get_tick(step.tick_next)
220 .unwrap()
221 .net_liquidity;
222 let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
223 state.liquidity =
224 liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
225 gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_INITIALIZED_TICK_CROSS))?;
226 }
227 state.tick = if zero_for_one { step.tick_next - 1 } else { step.tick_next };
228 } else if state.sqrt_price != step.sqrt_price_start {
229 state.tick = get_tick_at_sqrt_ratio(state.sqrt_price)?;
230 }
231 }
232 Ok(SwapResults {
233 amount_calculated: state.amount_calculated,
234 amount_specified,
235 amount_remaining: state.amount_remaining,
236 sqrt_price: state.sqrt_price,
237 liquidity: state.liquidity,
238 tick: state.tick,
239 gas_used: safe_add_u256(gas_used, U256::from(V3_CALLBACK_SETTLEMENT_GAS))?,
240 })
241 }
242
243 fn get_sqrt_ratio_target(
244 sqrt_price_next: U256,
245 sqrt_price_limit: U256,
246 zero_for_one: bool,
247 ) -> U256 {
248 let cond1 = if zero_for_one {
249 sqrt_price_next < sqrt_price_limit
250 } else {
251 sqrt_price_next > sqrt_price_limit
252 };
253
254 if cond1 {
255 sqrt_price_limit
256 } else {
257 sqrt_price_next
258 }
259 }
260}
261
262#[typetag::serde]
263impl ProtocolSim for RamsesV3State {
264 fn fee(&self) -> f64 {
265 self.fee as f64 / 1_000_000.0
266 }
267
268 fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
269 let price = if a < b {
270 sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)?
271 } else {
272 1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)?
273 };
274 Ok(add_fee_markup(price, self.fee()))
275 }
276
277 fn get_amount_out(
278 &self,
279 amount_in: BigUint,
280 token_a: &Token,
281 token_b: &Token,
282 ) -> Result<GetAmountOutResult, SimulationError> {
283 let zero_for_one = token_a < token_b;
284 let amount_specified = U256::try_from_be_slice(&amount_in.to_bytes_be())
285 .and_then(|unsigned_amount_in| {
286 I256::checked_from_sign_and_abs(Sign::Positive, unsigned_amount_in)
287 })
288 .ok_or_else(|| {
289 SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
290 })?;
291
292 let result = self.swap(zero_for_one, amount_specified, None)?;
293
294 trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "RAMSES V3 SWAP");
295 let mut new_state = self.clone();
296 new_state.liquidity = result.liquidity;
297 new_state.tick = result.tick;
298 new_state.sqrt_price = result.sqrt_price;
299
300 Ok(GetAmountOutResult::new(
301 u256_to_biguint(
302 result
303 .amount_calculated
304 .saturating_abs()
305 .into_raw(),
306 ),
307 u256_to_biguint(result.gas_used),
308 Box::new(new_state),
309 ))
310 }
311
312 fn get_limits(
313 &self,
314 token_in: Bytes,
315 token_out: Bytes,
316 ) -> Result<(BigUint, BigUint), SimulationError> {
317 if !self.ticks.has_initialized_ticks() {
318 return Ok((BigUint::ZERO, BigUint::ZERO));
319 }
320
321 let zero_for_one = token_in < token_out;
322 let mut current_tick = self.tick;
323 let mut current_sqrt_price = self.sqrt_price;
324 let mut current_liquidity = self.liquidity;
325 let mut total_amount_in = U256::ZERO;
326 let mut total_amount_out = U256::ZERO;
327 let mut ticks_crossed: u64 = 0;
328
329 while let Ok((tick, initialized)) = self
332 .ticks
333 .next_initialized_tick_within_one_word(current_tick, zero_for_one)
334 {
335 if ticks_crossed == MAX_TICKS_CROSSED {
337 break;
338 }
339 ticks_crossed += 1;
340
341 let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
343
344 let sqrt_price_next = get_sqrt_ratio_at_tick(next_tick)?;
346
347 let (amount_in, amount_out) = if zero_for_one {
350 let amount0 = get_amount0_delta(
351 sqrt_price_next,
352 current_sqrt_price,
353 current_liquidity,
354 true,
355 )?;
356 let amount1 = get_amount1_delta(
357 sqrt_price_next,
358 current_sqrt_price,
359 current_liquidity,
360 false,
361 )?;
362 (amount0, amount1)
363 } else {
364 let amount0 = get_amount0_delta(
365 sqrt_price_next,
366 current_sqrt_price,
367 current_liquidity,
368 false,
369 )?;
370 let amount1 = get_amount1_delta(
371 sqrt_price_next,
372 current_sqrt_price,
373 current_liquidity,
374 true,
375 )?;
376 (amount1, amount0)
377 };
378
379 total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
381 total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
382
383 if initialized {
388 let liquidity_raw = self
389 .ticks
390 .get_tick(next_tick)
391 .unwrap()
392 .net_liquidity;
393 let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
394
395 match liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta) {
398 Ok(new_liquidity) => {
399 current_liquidity = new_liquidity;
400 }
401 Err(_) => {
402 break;
405 }
406 }
407 }
408
409 current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
411 current_sqrt_price = sqrt_price_next;
412 }
413
414 Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
415 }
416
417 fn delta_transition(
418 &mut self,
419 mut delta: ProtocolStateDelta,
420 _tokens: &HashMap<Bytes, Token>,
421 _balances: &Balances,
422 ) -> Result<(), TransitionError> {
423 if let Some(liquidity) = delta
425 .updated_attributes
426 .remove("liquidity")
427 {
428 self.liquidity = liquidity.into();
429 }
430 if let Some(sqrt_price) = delta
431 .updated_attributes
432 .get("sqrt_price_x96")
433 {
434 self.sqrt_price = U256::from_be_slice(sqrt_price);
435 }
436 if let Some(tick) = delta.updated_attributes.remove("tick") {
437 self.tick = tick.into();
438 }
439 if let Some(fee) = delta.updated_attributes.remove("fee") {
441 self.fee = fee.into();
442 }
443
444 for (key, value) in delta.updated_attributes {
446 let Some(tick) = key.strip_prefix("ticks/") else {
447 continue;
448 };
449
450 self.ticks
451 .set_tick_liquidity(
452 tick.parse::<i32>()
453 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
454 i128::from(value),
455 )
456 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
457 }
458 for key in delta.deleted_attributes {
460 let Some(tick) = key.strip_prefix("ticks/") else {
461 continue;
462 };
463
464 self.ticks
465 .set_tick_liquidity(
466 tick.parse::<i32>()
467 .map_err(|err| TransitionError::DecodeError(err.to_string()))?,
468 0,
469 )
470 .map_err(|err| TransitionError::DecodeError(err.to_string()))?;
471 }
472 Ok(())
473 }
474
475 fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
480 if !self.ticks.has_initialized_ticks() {
481 return Err(SimulationError::RecoverableError("No liquidity".to_string()));
482 }
483
484 match params.swap_constraint() {
485 SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
486 "Ramses V3 does not support TradeLimitPrice constraint in query_pool_swap"
487 .to_string(),
488 None,
489 )),
490 SwapConstraint::PoolTargetPrice {
491 target,
492 tolerance: _,
493 min_amount_in: _,
494 max_amount_in: _,
495 } => {
496 let (amount_in, amount_out, swap_result) = clmm_swap_to_price(
497 self.sqrt_price,
498 ¶ms.token_in().address,
499 ¶ms.token_out().address,
500 target,
501 self.fee,
502 Sign::Positive,
503 |zero_for_one, amount_specified, sqrt_price_limit| {
504 self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit))
505 },
506 )?;
507
508 let mut new_state = self.clone();
509 new_state.liquidity = swap_result.liquidity;
510 new_state.tick = swap_result.tick;
511 new_state.sqrt_price = swap_result.sqrt_price;
512
513 Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
514 }
515 }
516 }
517
518 fn clone_box(&self) -> Box<dyn ProtocolSim> {
519 Box::new(self.clone())
520 }
521
522 fn as_any(&self) -> &dyn Any {
523 self
524 }
525
526 fn as_any_mut(&mut self) -> &mut dyn Any {
527 self
528 }
529
530 fn eq(&self, other: &dyn ProtocolSim) -> bool {
531 let Some(RamsesV3State { liquidity, sqrt_price, fee, tick, tick_spacing, ticks }) = other
532 .as_any()
533 .downcast_ref::<RamsesV3State>()
534 else {
535 return false;
536 };
537
538 &self.liquidity == liquidity &&
539 &self.sqrt_price == sqrt_price &&
540 &self.fee == fee &&
541 &self.tick == tick &&
542 &self.tick_spacing == tick_spacing &&
543 &self.ticks == ticks
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use std::{
550 collections::{HashMap, HashSet},
551 str::FromStr,
552 };
553
554 use num_bigint::ToBigUint;
555 use tycho_common::models::Chain;
556
557 use super::*;
558
559 #[test]
563 fn test_get_amount_out_math_parity() {
564 let wbtc = Token::new(
565 &Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(),
566 "WBTC",
567 8,
568 0,
569 &[Some(10_000)],
570 Chain::Ethereum,
571 100,
572 );
573 let weth = Token::new(
574 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
575 "WETH",
576 18,
577 0,
578 &[Some(10_000)],
579 Chain::Ethereum,
580 100,
581 );
582 let pool = RamsesV3State::new(
583 377952820878029838,
584 U256::from_str("28437325270877025820973479874632004").unwrap(),
585 500,
586 10,
587 255830,
588 vec![
589 TickInfo::new(255760, 1759015528199933i128).unwrap(),
590 TickInfo::new(255770, 6393138051835308i128).unwrap(),
591 TickInfo::new(255780, 228206673808681i128).unwrap(),
592 TickInfo::new(255820, 1319490609195820i128).unwrap(),
593 TickInfo::new(255830, 678916926147901i128).unwrap(),
594 TickInfo::new(255840, 12208947683433103i128).unwrap(),
595 TickInfo::new(255850, 1177970713095301i128).unwrap(),
596 TickInfo::new(255860, 8752304680520407i128).unwrap(),
597 TickInfo::new(255880, 1486478248067104i128).unwrap(),
598 TickInfo::new(255890, 1878744276123248i128).unwrap(),
599 TickInfo::new(255900, 77340284046725227i128).unwrap(),
600 ],
601 )
602 .unwrap();
603
604 let res = pool
605 .get_amount_out(500000000.to_biguint().unwrap(), &wbtc, &weth)
606 .unwrap();
607
608 assert_eq!(res.amount, BigUint::from_str("64352395915550406461").unwrap());
609 }
610
611 #[test]
612 fn test_delta_transition_updates_mutable_fee() {
613 let mut pool = RamsesV3State::new(
614 1000,
615 U256::from_str("1000").unwrap(),
616 500,
617 10,
618 100,
619 vec![TickInfo::new(255760, 10000).unwrap(), TickInfo::new(255900, -10000).unwrap()],
620 )
621 .unwrap();
622
623 let attributes: HashMap<String, Bytes> = [
624 ("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
625 ("fee".to_string(), Bytes::from(3000_u32.to_be_bytes().to_vec())),
628 ]
629 .into_iter()
630 .collect();
631 let delta = ProtocolStateDelta {
632 component_id: "State1".to_owned(),
633 updated_attributes: attributes,
634 deleted_attributes: HashSet::new(),
635 };
636
637 pool.delta_transition(delta, &HashMap::new(), &Balances::default())
638 .unwrap();
639
640 assert_eq!(pool.liquidity, 2000);
641 assert_eq!(pool.fee, 3000);
642 assert_eq!(pool.fee(), 0.003);
643 }
644}