use std::{
collections::{HashMap, HashSet},
convert::identity,
};
use evm_ekubo_sdk::{
math::{tick::to_sqrt_ratio, uint::U256},
quoting::{
self,
base_pool::{BasePoolError, BasePoolResources, BasePoolState},
types::{NodeKey, Pool, QuoteParams, Tick, TokenAmount},
util::find_nearest_initialized_tick_index,
},
};
use num_traits::Zero;
use serde::{Deserialize, Serialize};
use tycho_common::{
simulation::errors::{SimulationError, TransitionError},
Bytes,
};
use super::{EkuboPool, EkuboPoolQuote};
use crate::{
evm::protocol::ekubo::attributes::ticks_from_attributes, protocol::errors::InvalidSnapshotError,
};
#[derive(Debug, Clone, Eq, Serialize, Deserialize)]
pub struct BasePool {
imp: quoting::base_pool::BasePool,
state: BasePoolState,
active_tick: Option<i32>,
}
impl PartialEq for BasePool {
fn eq(&self, other: &Self) -> bool {
self.key() == other.key() &&
self.imp.get_sorted_ticks() == other.imp.get_sorted_ticks() &&
self.state == other.state
}
}
pub(super) fn impl_from_state(
key: NodeKey,
state: BasePoolState,
ticks: Vec<Tick>,
) -> Result<quoting::base_pool::BasePool, BasePoolError> {
quoting::base_pool::BasePool::new(key, state, ticks)
}
impl BasePool {
const BASE_GAS_COST: u64 = 24_000;
const GAS_COST_OF_ONE_TICK_SPACING_CROSSED: u64 = 4_000;
const GAS_COST_OF_ONE_INITIALIZED_TICK_CROSSED: u64 = 20_000;
pub fn new(
key: NodeKey,
ticks: Vec<Tick>,
sqrt_ratio: U256,
liquidity: u128,
tick: i32,
) -> Result<Self, InvalidSnapshotError> {
let state = BasePoolState {
sqrt_ratio,
liquidity,
active_tick_index: find_nearest_initialized_tick_index(&ticks, tick),
};
Ok(Self {
imp: impl_from_state(key, state, ticks).map_err(|err| {
InvalidSnapshotError::ValueError(format!("creating base pool: {err:?}"))
})?,
state,
active_tick: Some(tick),
})
}
pub(super) fn gas_costs(resources: BasePoolResources) -> u64 {
u64::from(resources.tick_spacings_crossed) * Self::GAS_COST_OF_ONE_TICK_SPACING_CROSSED +
u64::from(resources.initialized_ticks_crossed) *
Self::GAS_COST_OF_ONE_INITIALIZED_TICK_CROSSED
}
}
impl EkuboPool for BasePool {
fn key(&self) -> &NodeKey {
self.imp.get_key()
}
fn sqrt_ratio(&self) -> U256 {
self.state.sqrt_ratio
}
fn set_sqrt_ratio(&mut self, sqrt_ratio: U256) {
self.state.sqrt_ratio = sqrt_ratio;
}
fn set_liquidity(&mut self, liquidity: u128) {
self.state.liquidity = liquidity;
}
fn quote(&self, token_amount: TokenAmount) -> Result<EkuboPoolQuote, SimulationError> {
let quote = self
.imp
.quote(QuoteParams {
token_amount,
sqrt_ratio_limit: None,
override_state: Some(self.state),
meta: (),
})
.map_err(|err| SimulationError::RecoverableError(format!("{err:?}")))?;
let state_after = quote.state_after;
let new_state =
Self { imp: self.imp.clone(), state: state_after, active_tick: None }.into();
Ok(EkuboPoolQuote {
consumed_amount: quote.consumed_amount,
calculated_amount: quote.calculated_amount,
gas: Self::BASE_GAS_COST + Self::gas_costs(quote.execution_resources),
new_state,
})
}
fn get_limit(&self, token_in: U256) -> Result<i128, SimulationError> {
get_limit(token_in, self.sqrt_ratio(), &self.imp, self.state, (), identity)
}
fn finish_transition(
&mut self,
updated_attributes: HashMap<String, Bytes>,
deleted_attributes: HashSet<String>,
) -> Result<(), TransitionError> {
let active_tick_update = updated_attributes
.get("tick")
.and_then(|updated_tick| {
let updated_tick = updated_tick.clone().into();
(self.active_tick != Some(updated_tick)).then_some(updated_tick)
});
let changed_ticks = ticks_from_attributes(
updated_attributes.into_iter().chain(
deleted_attributes
.into_iter()
.map(|key| (key, Bytes::new())),
),
)
.map_err(TransitionError::DecodeError)?;
let new_initialized_ticks = (!changed_ticks.is_empty()).then(|| {
let mut ticks = self.imp.get_sorted_ticks().clone();
for tick in changed_ticks {
let res = ticks.binary_search_by_key(&tick.index, |t| t.index);
match res {
Ok(idx) => {
if tick.liquidity_delta.is_zero() {
ticks.remove(idx);
} else {
ticks[idx] = tick;
}
}
Err(idx) => {
ticks.insert(idx, tick);
}
}
}
ticks
});
if let Some(new_active_tick) = active_tick_update {
self.active_tick = Some(new_active_tick);
}
if active_tick_update.is_some() || new_initialized_ticks.is_some() {
self.state.active_tick_index = find_nearest_initialized_tick_index(
new_initialized_ticks
.as_ref()
.unwrap_or(self.imp.get_sorted_ticks()),
self.active_tick.ok_or_else(|| {
TransitionError::MissingAttribute(
"base state should always have an active tick during transitions"
.to_string(),
)
})?,
);
}
if let Some(ticks) = new_initialized_ticks {
self.imp = impl_from_state(*self.key(), self.state, ticks).map_err(|err| {
TransitionError::SimulationError(SimulationError::RecoverableError(format!(
"reinstantiate base pool: {err:?}"
)))
})?;
}
Ok(())
}
}
const WEI_UNDERESTIMATION_FACTOR: i128 = 2;
pub(super) fn get_limit<P, S, M, R>(
token_in: U256,
sqrt_ratio: U256,
imp: &P,
state: S,
meta: M,
resources_fn: impl FnOnce(R) -> BasePoolResources,
) -> Result<i128, SimulationError>
where
P: Pool<State = S, Meta = M, Resources = R>,
{
let sqrt_ratio_limit = if token_in == imp.get_key().token0 {
imp.min_tick_with_liquidity()
.map_or(Ok(sqrt_ratio), |tick| {
to_sqrt_ratio(tick)
.ok_or_else(|| {
SimulationError::FatalError(
"sqrt_ratio should be computable from tick index".to_string(),
)
})
.map(|r| Ord::min(r, sqrt_ratio))
})
} else {
imp.max_tick_with_liquidity()
.map_or(Ok(sqrt_ratio), |tick| {
to_sqrt_ratio(tick)
.ok_or_else(|| {
SimulationError::FatalError(
"sqrt_ratio should be computable from tick index".to_string(),
)
})
.map(|r| Ord::max(r, sqrt_ratio))
})
}?;
let quote = imp
.quote(QuoteParams {
token_amount: TokenAmount { amount: i128::MAX, token: token_in },
sqrt_ratio_limit: Some(sqrt_ratio_limit),
override_state: Some(state),
meta,
})
.map_err(|err| SimulationError::RecoverableError(format!("quoting error: {err:?}")))?;
let resources = resources_fn(quote.execution_resources);
Ok(quote
.consumed_amount
.saturating_sub(
WEI_UNDERESTIMATION_FACTOR *
(i128::from(resources.initialized_ticks_crossed) +
i128::from(resources.tick_spacings_crossed) / 256 +
1),
)
.max(0))
}