tycho-simulation 0.310.0

Provides tools for interacting with protocol states, calculating spot prices, and quoting token swaps.
Documentation
use std::collections::{HashMap, HashSet};

use evm_ekubo_sdk::{
    math::uint::U256,
    quoting::{
        self,
        base_pool::BasePoolState,
        mev_resist_pool::MEVResistPoolState,
        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,
        pool::base::{self, BasePool},
    },
    protocol::errors::InvalidSnapshotError,
};

#[derive(Debug, Eq, Clone, Serialize, Deserialize)]
pub struct MevResistPool {
    imp: quoting::mev_resist_pool::MEVResistPool,

    ticks: Vec<Tick>,
    base_pool_state: BasePoolState,
    last_tick: i32,

    active_tick: Option<i32>,
}

impl PartialEq for MevResistPool {
    fn eq(&self, other: &Self) -> bool {
        self.key() == other.key() &&
            self.base_pool_state == other.base_pool_state &&
            self.ticks == other.ticks &&
            self.last_tick == other.last_tick
    }
}

fn impl_from_state(
    key: NodeKey,
    base_pool_state: BasePoolState,
    ticks: Vec<Tick>,
    tick: i32,
) -> Result<quoting::mev_resist_pool::MEVResistPool, String> {
    quoting::mev_resist_pool::MEVResistPool::new(
        base::impl_from_state(key, base_pool_state, ticks)
            .map_err(|err| format!("creating base pool: {err:?}"))?,
        0,
        tick,
    )
    .map_err(|err| format!("creating MEV-resist pool: {err:?}"))
}

impl MevResistPool {
    const BASE_GAS_COST: u64 = 41_600;
    const GAS_COST_OF_ONE_STATE_UPDATE: u64 = 16_400;

    pub fn new(
        key: NodeKey,
        ticks: Vec<Tick>,
        sqrt_ratio: U256,
        liquidity: u128,
        tick: i32,
    ) -> Result<Self, InvalidSnapshotError> {
        let base_pool_state = BasePoolState {
            sqrt_ratio,
            liquidity,
            active_tick_index: find_nearest_initialized_tick_index(&ticks, tick),
        };

        Ok(Self {
            imp: impl_from_state(key, base_pool_state, ticks.clone(), tick).map_err(|err| {
                InvalidSnapshotError::ValueError(format!("creating MEV-resist pool: {err:?}"))
            })?,
            ticks,
            base_pool_state,
            last_tick: tick,
            active_tick: Some(tick),
        })
    }
}

impl EkuboPool for MevResistPool {
    fn key(&self) -> &NodeKey {
        self.imp.get_key()
    }

    fn sqrt_ratio(&self) -> U256 {
        self.base_pool_state.sqrt_ratio
    }

    fn set_sqrt_ratio(&mut self, sqrt_ratio: U256) {
        self.base_pool_state.sqrt_ratio = sqrt_ratio;
    }

    fn set_liquidity(&mut self, liquidity: u128) {
        self.base_pool_state.liquidity = liquidity;
    }

    fn quote(&self, token_amount: TokenAmount) -> Result<EkuboPoolQuote, SimulationError> {
        let first_swap_this_block = self.active_tick.is_some();

        let quote = self
            .imp
            .quote(QuoteParams {
                token_amount,
                sqrt_ratio_limit: None,
                override_state: Some(MEVResistPoolState {
                    last_update_time: 0,
                    base_pool_state: self.base_pool_state,
                }),
                meta: u64::from(first_swap_this_block),
            })
            .map_err(|err| SimulationError::RecoverableError(format!("quoting error: {err:?}")))?;

        Ok(EkuboPoolQuote {
            consumed_amount: quote.consumed_amount,
            calculated_amount: quote.calculated_amount,
            gas: Self::BASE_GAS_COST +
                u64::from(
                    quote
                        .execution_resources
                        .state_update_count,
                ) * Self::GAS_COST_OF_ONE_STATE_UPDATE +
                BasePool::gas_costs(
                    quote
                        .execution_resources
                        .base_pool_resources,
                ),
            new_state: Self {
                imp: self.imp.clone(),
                ticks: self.ticks.clone(),
                base_pool_state: quote.state_after.base_pool_state,
                last_tick: self.last_tick,
                active_tick: None,
            }
            .into(),
        })
    }

    fn get_limit(&self, token_in: U256) -> Result<i128, SimulationError> {
        base::get_limit(
            token_in,
            self.sqrt_ratio(),
            &self.imp,
            MEVResistPoolState { last_update_time: 0, base_pool_state: self.base_pool_state },
            0,
            |r| r.base_pool_resources,
        )
    }

    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();

        for tick in changed_ticks {
            let res = self
                .ticks
                .binary_search_by_key(&tick.index, |t| t.index);

            match res {
                Ok(idx) => {
                    if tick.liquidity_delta.is_zero() {
                        self.ticks.remove(idx);
                    } else {
                        self.ticks[idx] = tick;
                    }
                }
                Err(idx) => {
                    self.ticks.insert(idx, tick);
                }
            }
        }

        if let Some(new_active_tick) = active_tick_update {
            self.last_tick = new_active_tick;
            self.active_tick = Some(new_active_tick);
        }

        if active_tick_update.is_some() || new_initialized_ticks {
            self.base_pool_state.active_tick_index = find_nearest_initialized_tick_index(
                &self.ticks,
                self.active_tick.ok_or_else(|| {
                    TransitionError::MissingAttribute(
                        "base state should always have an active tick during transitions"
                            .to_string(),
                    )
                })?,
            );
        }

        if new_initialized_ticks {
            self.imp = impl_from_state(
                *self.key(),
                self.base_pool_state,
                self.ticks.clone(),
                self.last_tick,
            )
            .map_err(|err| {
                TransitionError::SimulationError(SimulationError::RecoverableError(format!(
                    "reinstantiate base pool: {err:?}"
                )))
            })?;
        }

        Ok(())
    }
}