use std::{any::Any, collections::HashMap, sync::Arc};
use alloy::primitives::{Sign, I256, U256};
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use tracing::trace;
use tycho_common::{
dto::ProtocolStateDelta,
models::token::Token,
simulation::{
errors::{SimulationError, TransitionError},
protocol_sim::{
Balances, GetAmountOutResult, PoolSwap, ProtocolSim, QueryPoolSwapParams,
SwapConstraint,
},
},
Bytes,
};
use crate::evm::protocol::{
clmm::clmm_swap_to_price,
safe_math::{safe_add_u256, safe_sub_u256},
u256_num::u256_to_biguint,
utils::{
add_fee_markup,
uniswap::{
liquidity_math,
pool_tick::PoolTick,
sqrt_price_math::{get_amount0_delta, get_amount1_delta, sqrt_price_q96_to_f64},
swap_math::{self, SwapStepResult},
swap_step_cache::{StepCacheKey, StepCacheRun, SwapStepCache},
tick_list::{TickInfo, TickList, TickListErrorKind},
tick_math::{
get_sqrt_ratio_at_tick_cached, MAX_SQRT_RATIO, MAX_TICK, MIN_SQRT_RATIO, MIN_TICK,
},
StepComputation, SwapResults, SwapState,
},
},
};
const SWAP_BASE_GAS: u64 = 78_000;
const GAS_PER_BITMAP_WORD: u64 = 2_100;
const GAS_PER_SWAP_MATH_STEP: u64 = 5_400;
const GAS_PER_INITIALIZED_TICK_CROSS: u64 = 35_000;
const V3_CALLBACK_SETTLEMENT_GAS: u64 = 70_000;
const MAX_SWAP_GAS: u64 = 16_700_000;
const MAX_TICKS_CROSSED: u64 = (MAX_SWAP_GAS - SWAP_BASE_GAS) / GAS_PER_INITIALIZED_TICK_CROSS;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RamsesV3State {
liquidity: u128,
sqrt_price: U256,
fee: u32,
tick: PoolTick,
tick_spacing: u16,
ticks: Arc<TickList>,
#[serde(skip)]
step_cache: SwapStepCache,
}
impl RamsesV3State {
pub fn new(
liquidity: u128,
sqrt_price: U256,
fee: u32,
tick_spacing: u16,
tick: i32,
ticks: Vec<TickInfo>,
) -> Result<Self, SimulationError> {
let tick_list = TickList::from(tick_spacing, ticks)?;
Ok(RamsesV3State {
liquidity,
sqrt_price,
fee,
tick: tick.into(),
tick_spacing,
ticks: Arc::new(tick_list),
step_cache: SwapStepCache::default(),
})
}
fn clone_post_swap(&self, sqrt_price: U256, tick: PoolTick, liquidity: u128) -> Self {
let mut state = self.clone();
state.sqrt_price = sqrt_price;
state.tick = tick;
state.liquidity = liquidity;
state.step_cache = SwapStepCache::default();
state
}
fn swap(
&self,
zero_for_one: bool,
amount_specified: I256,
sqrt_price_limit: Option<U256>,
) -> Result<SwapResults, SimulationError> {
if !self.ticks.has_initialized_ticks() {
return Err(SimulationError::RecoverableError("No liquidity".to_string()));
}
let price_limit = if let Some(limit) = sqrt_price_limit {
limit
} else if zero_for_one {
safe_add_u256(MIN_SQRT_RATIO, U256::ONE)?
} else {
safe_sub_u256(MAX_SQRT_RATIO, U256::ONE)?
};
let price_limit_valid = if zero_for_one {
price_limit > MIN_SQRT_RATIO && price_limit < self.sqrt_price
} else {
price_limit < MAX_SQRT_RATIO && price_limit > self.sqrt_price
};
if !price_limit_valid {
return Err(SimulationError::InvalidInput("Price limit out of range".into(), None));
}
let exact_input = amount_specified > I256::from_raw(U256::from(0u64));
let mut state = SwapState {
amount_remaining: amount_specified,
amount_calculated: I256::from_raw(U256::from(0u64)),
sqrt_price: self.sqrt_price,
tick: self.tick.value().into(),
liquidity: self.liquidity,
};
let mut gas_used = U256::from(SWAP_BASE_GAS);
let cache_key = if exact_input && sqrt_price_limit.is_none() {
StepCacheKey::new(
self.sqrt_price,
state.tick.value(),
self.liquidity,
self.fee,
gas_used,
)
} else {
None
};
let mut cache_run = StepCacheRun::new(
&self.step_cache,
zero_for_one,
cache_key,
amount_specified.unsigned_abs(),
);
if let Some(progress) = cache_run.apply_cached_progress(&mut state) {
gas_used = progress.gas;
}
while state.amount_remaining != I256::from_raw(U256::from(0u64)) &&
state.sqrt_price != price_limit
{
let (mut next_tick, next_tick_info) = match self
.ticks
.next_initialized_tick_within_one_word(state.tick.value(), zero_for_one)
{
Ok((tick, info)) => {
gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_BITMAP_WORD))?;
(tick, info)
}
Err(tick_err) => match tick_err.kind {
TickListErrorKind::TicksExeeded => {
let new_state = self.clone_post_swap(
state.sqrt_price,
state.tick.clone(),
state.liquidity,
);
return Err(SimulationError::InvalidInput(
"Ticks exceeded".into(),
Some(GetAmountOutResult::new(
u256_to_biguint(state.amount_calculated.abs().into_raw()),
u256_to_biguint(gas_used),
Box::new(new_state),
)),
));
}
_ => return Err(SimulationError::FatalError("Unknown error".to_string())),
},
};
next_tick = next_tick.clamp(MIN_TICK, MAX_TICK);
let sqrt_price_start = state.sqrt_price;
let sqrt_price_next = match next_tick_info {
Some(info) => info.sqrt_price,
None => get_sqrt_ratio_at_tick_cached(next_tick)?,
};
let SwapStepResult { sqrt_price, amount_in_with_fee, amount_out } =
swap_math::compute_swap_step(
state.sqrt_price,
RamsesV3State::get_sqrt_ratio_target(
sqrt_price_next,
price_limit,
zero_for_one,
),
state.liquidity,
state.amount_remaining,
self.fee,
)?;
state.sqrt_price = sqrt_price;
let step = StepComputation {
sqrt_price_start,
tick_next: next_tick,
sqrt_price_next,
amount_in_with_fee,
amount_out,
};
gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_SWAP_MATH_STEP))?;
if exact_input {
state.amount_remaining -=
I256::checked_from_sign_and_abs(Sign::Positive, step.amount_in_with_fee)
.unwrap();
state.amount_calculated -=
I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
} else {
state.amount_remaining +=
I256::checked_from_sign_and_abs(Sign::Positive, step.amount_out).unwrap();
state.amount_calculated +=
I256::checked_from_sign_and_abs(Sign::Positive, step.amount_in_with_fee)
.unwrap();
}
if state.sqrt_price == step.sqrt_price_next {
if let Some(info) = next_tick_info {
let liquidity_raw = info.net_liquidity;
let liquidity_net = if zero_for_one { -liquidity_raw } else { liquidity_raw };
state.liquidity =
liquidity_math::add_liquidity_delta(state.liquidity, liquidity_net)?;
gas_used = safe_add_u256(gas_used, U256::from(GAS_PER_INITIALIZED_TICK_CROSS))?;
}
state.tick =
PoolTick::from(if zero_for_one { step.tick_next - 1 } else { step.tick_next });
} else if state.sqrt_price != step.sqrt_price_start {
state.tick = PoolTick::at_sqrt_price(state.sqrt_price)?;
}
cache_run.cache_progress(&step, &state, gas_used);
}
Ok(SwapResults {
amount_calculated: state.amount_calculated,
amount_specified,
amount_remaining: state.amount_remaining,
sqrt_price: state.sqrt_price,
liquidity: state.liquidity,
tick: state.tick,
gas_used: safe_add_u256(gas_used, U256::from(V3_CALLBACK_SETTLEMENT_GAS))?,
})
}
fn get_sqrt_ratio_target(
sqrt_price_next: U256,
sqrt_price_limit: U256,
zero_for_one: bool,
) -> U256 {
let cond1 = if zero_for_one {
sqrt_price_next < sqrt_price_limit
} else {
sqrt_price_next > sqrt_price_limit
};
if cond1 {
sqrt_price_limit
} else {
sqrt_price_next
}
}
}
#[typetag::serde]
impl ProtocolSim for RamsesV3State {
fn fee(&self) -> f64 {
self.fee as f64 / 1_000_000.0
}
fn spot_price(&self, a: &Token, b: &Token) -> Result<f64, SimulationError> {
let price = if a < b {
sqrt_price_q96_to_f64(self.sqrt_price, a.decimals, b.decimals)?
} else {
1.0f64 / sqrt_price_q96_to_f64(self.sqrt_price, b.decimals, a.decimals)?
};
Ok(add_fee_markup(price, self.fee()))
}
fn get_amount_out(
&self,
amount_in: BigUint,
token_a: &Token,
token_b: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
let zero_for_one = token_a < token_b;
let amount_specified = U256::try_from_be_slice(&amount_in.to_bytes_be())
.and_then(|unsigned_amount_in| {
I256::checked_from_sign_and_abs(Sign::Positive, unsigned_amount_in)
})
.ok_or_else(|| {
SimulationError::InvalidInput("I256 overflow: amount_in".to_string(), None)
})?;
let result = self.swap(zero_for_one, amount_specified, None)?;
trace!(?amount_in, ?token_a, ?token_b, ?zero_for_one, ?result, "RAMSES V3 SWAP");
let new_state = self.clone_post_swap(result.sqrt_price, result.tick, result.liquidity);
Ok(GetAmountOutResult::new(
u256_to_biguint(
result
.amount_calculated
.saturating_abs()
.into_raw(),
),
u256_to_biguint(result.gas_used),
Box::new(new_state),
))
}
fn get_limits(
&self,
token_in: Bytes,
token_out: Bytes,
) -> Result<(BigUint, BigUint), SimulationError> {
if !self.ticks.has_initialized_ticks() {
return Ok((BigUint::ZERO, BigUint::ZERO));
}
let zero_for_one = token_in < token_out;
let mut current_tick = self.tick.value();
let mut current_sqrt_price = self.sqrt_price;
let mut current_liquidity = self.liquidity;
let mut total_amount_in = U256::ZERO;
let mut total_amount_out = U256::ZERO;
let mut ticks_crossed: u64 = 0;
while let Ok((tick, next_tick_info)) = self
.ticks
.next_initialized_tick_within_one_word(current_tick, zero_for_one)
{
if ticks_crossed == MAX_TICKS_CROSSED {
break;
}
ticks_crossed += 1;
let next_tick = tick.clamp(MIN_TICK, MAX_TICK);
let sqrt_price_next = match next_tick_info {
Some(info) => info.sqrt_price,
None => get_sqrt_ratio_at_tick_cached(next_tick)?,
};
let (amount_in, amount_out) = if zero_for_one {
let amount0 = get_amount0_delta(
sqrt_price_next,
current_sqrt_price,
current_liquidity,
true,
)?;
let amount1 = get_amount1_delta(
sqrt_price_next,
current_sqrt_price,
current_liquidity,
false,
)?;
(amount0, amount1)
} else {
let amount0 = get_amount0_delta(
sqrt_price_next,
current_sqrt_price,
current_liquidity,
false,
)?;
let amount1 = get_amount1_delta(
sqrt_price_next,
current_sqrt_price,
current_liquidity,
true,
)?;
(amount1, amount0)
};
total_amount_in = safe_add_u256(total_amount_in, amount_in)?;
total_amount_out = safe_add_u256(total_amount_out, amount_out)?;
if let Some(info) = next_tick_info {
let liquidity_raw = info.net_liquidity;
let liquidity_delta = if zero_for_one { -liquidity_raw } else { liquidity_raw };
match liquidity_math::add_liquidity_delta(current_liquidity, liquidity_delta) {
Ok(new_liquidity) => {
current_liquidity = new_liquidity;
}
Err(_) => {
break;
}
}
}
current_tick = if zero_for_one { next_tick - 1 } else { next_tick };
current_sqrt_price = sqrt_price_next;
}
Ok((u256_to_biguint(total_amount_in), u256_to_biguint(total_amount_out)))
}
fn delta_transition(
&mut self,
mut delta: ProtocolStateDelta,
_tokens: &HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError> {
self.step_cache = SwapStepCache::default();
if let Some(liquidity) = delta
.updated_attributes
.remove("liquidity")
{
self.liquidity = liquidity.into();
}
if let Some(sqrt_price) = delta
.updated_attributes
.get("sqrt_price_x96")
{
self.sqrt_price = U256::from_be_slice(sqrt_price);
}
if let Some(tick) = delta.updated_attributes.remove("tick") {
self.tick = i32::from(tick).into();
}
if let Some(fee) = delta.updated_attributes.remove("fee") {
self.fee = fee.into();
}
for (key, value) in delta.updated_attributes {
let Some(tick) = key.strip_prefix("ticks/") else {
continue;
};
Arc::make_mut(&mut self.ticks)
.set_tick_liquidity(
tick.parse::<i32>()
.map_err(|err| TransitionError::DecodeError(err.to_string()))?,
i128::from(value),
)
.map_err(|err| TransitionError::DecodeError(err.to_string()))?;
}
for key in delta.deleted_attributes {
let Some(tick) = key.strip_prefix("ticks/") else {
continue;
};
Arc::make_mut(&mut self.ticks)
.set_tick_liquidity(
tick.parse::<i32>()
.map_err(|err| TransitionError::DecodeError(err.to_string()))?,
0,
)
.map_err(|err| TransitionError::DecodeError(err.to_string()))?;
}
Ok(())
}
fn query_pool_swap(&self, params: &QueryPoolSwapParams) -> Result<PoolSwap, SimulationError> {
if !self.ticks.has_initialized_ticks() {
return Err(SimulationError::RecoverableError("No liquidity".to_string()));
}
match params.swap_constraint() {
SwapConstraint::TradeLimitPrice { .. } => Err(SimulationError::InvalidInput(
"Ramses V3 does not support TradeLimitPrice constraint in query_pool_swap"
.to_string(),
None,
)),
SwapConstraint::PoolTargetPrice {
target,
tolerance: _,
min_amount_in: _,
max_amount_in: _,
} => {
let (amount_in, amount_out, swap_result) = clmm_swap_to_price(
self.sqrt_price,
¶ms.token_in().address,
¶ms.token_out().address,
target,
self.fee,
Sign::Positive,
|zero_for_one, amount_specified, sqrt_price_limit| {
self.swap(zero_for_one, amount_specified, Some(sqrt_price_limit))
},
)?;
let new_state = self.clone_post_swap(
swap_result.sqrt_price,
swap_result.tick,
swap_result.liquidity,
);
Ok(PoolSwap::new(amount_in, amount_out, Box::new(new_state), None))
}
}
}
fn clone_box(&self) -> Box<dyn ProtocolSim> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn eq(&self, other: &dyn ProtocolSim) -> bool {
let Some(RamsesV3State {
liquidity,
sqrt_price,
fee,
tick,
tick_spacing,
ticks,
step_cache: _,
}) = other
.as_any()
.downcast_ref::<RamsesV3State>()
else {
return false;
};
&self.liquidity == liquidity &&
&self.sqrt_price == sqrt_price &&
&self.fee == fee &&
&self.tick == tick &&
&self.tick_spacing == tick_spacing &&
&self.ticks == ticks
}
}
#[cfg(test)]
mod tests {
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
use num_bigint::ToBigUint;
use tycho_common::models::Chain;
use super::*;
#[test]
fn test_get_amount_out_math_parity() {
let wbtc = Token::new(
&Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(),
"WBTC",
8,
0,
&[Some(10_000)],
Chain::Ethereum,
100,
);
let weth = Token::new(
&Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
"WETH",
18,
0,
&[Some(10_000)],
Chain::Ethereum,
100,
);
let pool = RamsesV3State::new(
377952820878029838,
U256::from_str("28437325270877025820973479874632004").unwrap(),
500,
10,
255830,
vec![
TickInfo::new(255760, 1759015528199933i128).unwrap(),
TickInfo::new(255770, 6393138051835308i128).unwrap(),
TickInfo::new(255780, 228206673808681i128).unwrap(),
TickInfo::new(255820, 1319490609195820i128).unwrap(),
TickInfo::new(255830, 678916926147901i128).unwrap(),
TickInfo::new(255840, 12208947683433103i128).unwrap(),
TickInfo::new(255850, 1177970713095301i128).unwrap(),
TickInfo::new(255860, 8752304680520407i128).unwrap(),
TickInfo::new(255880, 1486478248067104i128).unwrap(),
TickInfo::new(255890, 1878744276123248i128).unwrap(),
TickInfo::new(255900, 77340284046725227i128).unwrap(),
],
)
.unwrap();
let res = pool
.get_amount_out(500000000.to_biguint().unwrap(), &wbtc, &weth)
.unwrap();
assert_eq!(res.amount, BigUint::from_str("64352395915550406461").unwrap());
}
#[test]
fn test_delta_transition_updates_mutable_fee() {
let mut pool = RamsesV3State::new(
1000,
U256::from_str("1000").unwrap(),
500,
10,
100,
vec![TickInfo::new(255760, 10000).unwrap(), TickInfo::new(255900, -10000).unwrap()],
)
.unwrap();
let attributes: HashMap<String, Bytes> = [
("liquidity".to_string(), Bytes::from(2000_u64.to_be_bytes().to_vec())),
("fee".to_string(), Bytes::from(3000_u32.to_be_bytes().to_vec())),
]
.into_iter()
.collect();
let delta = ProtocolStateDelta {
component_id: "State1".to_owned(),
updated_attributes: attributes,
deleted_attributes: HashSet::new(),
};
pool.delta_transition(delta, &HashMap::new(), &Balances::default())
.unwrap();
assert_eq!(pool.liquidity, 2000);
assert_eq!(pool.fee, 3000);
assert_eq!(pool.fee(), 0.003);
}
}
#[cfg(test)]
mod tick_list_sharing_tests {
use std::collections::{HashMap, HashSet};
use tycho_common::{dto::ProtocolStateDelta, hex_bytes::Bytes};
use super::*;
#[test]
fn test_delta_transition_leaves_clones_ticks_unchanged() {
let original = RamsesV3State::new(
1000,
U256::from(1000u64),
500,
10,
100,
vec![TickInfo::new(255760, 10000).unwrap(), TickInfo::new(255900, -10000).unwrap()],
)
.unwrap();
let mut updated = original.clone();
let delta = ProtocolStateDelta {
component_id: "State1".to_owned(),
updated_attributes: HashMap::from([(
"ticks/255760".to_string(),
Bytes::from(20000_i128.to_be_bytes().to_vec()),
)]),
deleted_attributes: HashSet::new(),
};
updated
.delta_transition(delta, &HashMap::new(), &Balances::default())
.unwrap();
assert_eq!(
updated
.ticks
.get_tick(255760)
.unwrap()
.net_liquidity,
20000
);
assert_eq!(
original
.ticks
.get_tick(255760)
.unwrap()
.net_liquidity,
10000
);
}
}
#[cfg(test)]
mod step_cache_tests {
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
use tycho_common::{dto::ProtocolStateDelta, hex_bytes::Bytes};
use super::*;
use crate::evm::protocol::utils::uniswap::swap_step_cache::test_fixtures::{
amount_orders, test_amounts, test_ticks, wbtc_weth, LIQUIDITY, SQRT_PRICE, TICK,
};
fn test_pool() -> RamsesV3State {
RamsesV3State::new(
LIQUIDITY,
U256::from_str(SQRT_PRICE).unwrap(),
500,
10,
TICK,
test_ticks(),
)
.unwrap()
}
fn format_quote_result(
pool: &RamsesV3State,
amount: &BigUint,
sell: &Token,
buy: &Token,
) -> String {
match pool.get_amount_out(amount.clone(), sell, buy) {
Ok(result) => {
let state = result
.new_state
.as_any()
.downcast_ref::<RamsesV3State>()
.unwrap();
format!(
"{} {} {} {} {}",
result.amount,
result.gas,
state.sqrt_price,
state.tick.value(),
state.liquidity
)
}
Err(error) => format!("error {error}"),
}
}
#[test]
fn test_step_cache_quotes_match_a_fresh_state_in_any_order() {
let (wbtc, weth) = wbtc_weth();
for (sell, buy, smallest) in [(&wbtc, &weth, 1_000u64), (&weth, &wbtc, 1_000_000_000)] {
for order in amount_orders(&test_amounts(smallest)) {
let shared = test_pool();
for amount in &order {
assert_eq!(
format_quote_result(&shared, amount, sell, buy),
format_quote_result(&test_pool(), amount, sell, buy),
"selling {amount} {}",
sell.symbol
);
}
let steps = shared
.step_cache
.min_inputs(sell < buy)
.len();
assert!(steps > 2, "only {steps} steps cached");
}
}
}
#[test]
fn test_step_cache_quotes_match_a_fresh_state_at_each_cached_step() {
let (wbtc, weth) = wbtc_weth();
for (sell, buy, doublings) in [(&wbtc, &weth, 30u32), (&weth, &wbtc, 70)] {
let cached = test_pool();
let _ = cached.get_amount_out(BigUint::from(10u32) << doublings, sell, buy);
let boundaries = cached.step_cache.min_inputs(sell < buy);
assert!(boundaries.len() > 2, "only {} steps cached", boundaries.len());
for boundary in boundaries.into_iter().skip(1) {
for amount in [boundary - U256::from(1u64), boundary, boundary + U256::from(1u64)] {
let amount = u256_to_biguint(amount);
assert_eq!(
format_quote_result(&cached, &amount, sell, buy),
format_quote_result(&test_pool(), &amount, sell, buy),
"selling {amount} {}",
sell.symbol
);
}
}
}
}
#[test]
fn test_step_cache_of_a_swapped_state() {
let (wbtc, weth) = wbtc_weth();
let pool = test_pool();
let swapped = pool
.get_amount_out(BigUint::from(300_000_000u64), &wbtc, &weth)
.unwrap()
.new_state;
let swapped = swapped
.as_any()
.downcast_ref::<RamsesV3State>()
.unwrap();
let fresh: RamsesV3State =
serde_json::from_value(serde_json::to_value(swapped).unwrap()).unwrap();
for amount in test_amounts(1_000).iter().rev() {
assert_eq!(
format_quote_result(swapped, amount, &wbtc, &weth),
format_quote_result(&fresh, amount, &wbtc, &weth),
"selling {amount} WBTC"
);
}
}
#[test]
fn test_step_cache_restarts_after_delta_transition() {
let (wbtc, weth) = wbtc_weth();
let large = BigUint::from(3_000_000_000u64);
let delta = || ProtocolStateDelta {
component_id: "State1".to_owned(),
updated_attributes: HashMap::from([(
"ticks/255820".to_string(),
Bytes::from(
1_000_000_000_000_000i128
.to_be_bytes()
.to_vec(),
),
)]),
deleted_attributes: HashSet::new(),
};
let mut cached = test_pool();
cached
.get_amount_out(large.clone(), &wbtc, &weth)
.unwrap();
let mut fresh = test_pool();
cached
.delta_transition(delta(), &HashMap::new(), &Balances::default())
.unwrap();
fresh
.delta_transition(delta(), &HashMap::new(), &Balances::default())
.unwrap();
assert_eq!(
format_quote_result(&cached, &large, &wbtc, &weth),
format_quote_result(&fresh, &large, &wbtc, &weth)
);
}
}