tycho-simulation 0.394.0

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

use ekubo_sdk::{
    chain::evm::{Evm, EvmPoolKey, EvmTokenAmount},
    math::swap::{amount_before_fee, compute_fee},
    U256,
};
use revm::primitives::Address;
use serde::{Deserialize, Serialize};
use tycho_common::{
    simulation::errors::{SimulationError, TransitionError},
    Bytes,
};

use super::{
    concentrated::ConcentratedPool, full_range::FullRangePool, stableswap::StableswapPool,
    EkuboPool, EkuboPoolQuote,
};
use crate::{evm::protocol::ekubo_v3::state::EkuboV3State, protocol::errors::InvalidSnapshotError};

const GAS_COST_OF_FEE_ACCUMULATION: u64 = 20_000;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[enum_delegate::implement(EkuboPool)]
pub enum Ve33UnderlyingPool {
    Concentrated(ConcentratedPool),
    FullRange(FullRangePool),
    Stableswap(StableswapPool),
}

impl Ve33UnderlyingPool {
    fn from_state(state: EkuboV3State) -> Result<Self, SimulationError> {
        match state {
            EkuboV3State::Concentrated(pool) => Ok(Self::Concentrated(pool)),
            EkuboV3State::FullRange(pool) => Ok(Self::FullRange(pool)),
            EkuboV3State::Stableswap(pool) => Ok(Self::Stableswap(pool)),
            _ => Err(SimulationError::FatalError(
                "Ve33 underlying quote returned an unexpected pool type".to_string(),
            )),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ve33Pool {
    underlying_pool: Ve33UnderlyingPool,
    swap_fee: u64,
}

impl Ve33Pool {
    pub fn new(
        underlying_pool: Ve33UnderlyingPool,
        swap_fee: u64,
    ) -> Result<Self, InvalidSnapshotError> {
        let key = underlying_pool.key();
        if key.config.fee != 0 {
            return Err(InvalidSnapshotError::ValueError(
                "Ve33 pool must have a zero Core fee".to_string(),
            ));
        }
        if key.config.extension == Address::ZERO {
            return Err(InvalidSnapshotError::ValueError(
                "Ve33 pool must have an extension".to_string(),
            ));
        }

        Ok(Self { underlying_pool, swap_fee })
    }

    pub fn swap_fee(&self) -> u64 {
        self.swap_fee
    }
}

impl EkuboPool for Ve33Pool {
    fn key(&self) -> EvmPoolKey {
        self.underlying_pool.key()
    }

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

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

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

    fn finish_transition(
        &mut self,
        updated_attributes: HashMap<String, Bytes>,
        deleted_attributes: HashSet<String>,
    ) -> Result<(), TransitionError> {
        if let Some(swap_fee) = updated_attributes.get("swap_fee") {
            self.swap_fee = u64::from_be_bytes(
                swap_fee
                    .as_ref()
                    .try_into()
                    .map_err(|err| {
                        TransitionError::DecodeError(format!("swap_fee length mismatch: {err:?}"))
                    })?,
            );
        }

        self.underlying_pool
            .finish_transition(updated_attributes, deleted_attributes)
    }

    fn quote(&self, token_amount: EvmTokenAmount) -> Result<EkuboPoolQuote, SimulationError> {
        let quote = self
            .underlying_pool
            .quote(token_amount)?;
        let calculated_amount = if self.swap_fee == 0 {
            quote.calculated_amount
        } else if token_amount.amount >= 0 {
            quote.calculated_amount - compute_fee::<Evm>(quote.calculated_amount, self.swap_fee)
        } else {
            amount_before_fee::<Evm>(quote.calculated_amount, self.swap_fee).ok_or_else(|| {
                SimulationError::RecoverableError(
                    "Ve33 exact-output fee computation overflowed".to_string(),
                )
            })?
        };

        Ok(EkuboPoolQuote {
            consumed_amount: quote.consumed_amount,
            calculated_amount,
            gas: quote.gas + u64::from(self.swap_fee != 0) * GAS_COST_OF_FEE_ACCUMULATION,
            new_state: Self {
                underlying_pool: Ve33UnderlyingPool::from_state(quote.new_state)?,
                swap_fee: self.swap_fee,
            }
            .into(),
        })
    }

    fn get_limit(&self, token_in: Address) -> Result<i128, SimulationError> {
        self.underlying_pool.get_limit(token_in)
    }
}

#[cfg(test)]
mod tests {
    use ekubo_sdk::quoting::{
        pools::full_range::{FullRangePoolKey, FullRangePoolState, FullRangePoolTypeConfig},
        types::PoolConfig,
    };
    use revm::primitives::address;

    use super::*;

    const TOKEN0: Address = address!("0x0000000000000000000000000000000000000001");
    const TOKEN1: Address = address!("0x0000000000000000000000000000000000000002");
    const SWAP_FEE: u64 = u64::MAX / 100;

    fn pool() -> Ve33Pool {
        Ve33Pool::new(
            Ve33UnderlyingPool::FullRange(
                FullRangePool::new(
                    FullRangePoolKey {
                        token0: TOKEN0,
                        token1: TOKEN1,
                        config: PoolConfig {
                            fee: 0,
                            extension: address!("0xD18685a514E59b06d59824e16Db07e73345d9953"),
                            pool_type_config: FullRangePoolTypeConfig,
                        },
                    },
                    FullRangePoolState {
                        sqrt_ratio: U256::from_limbs([0, 0, 1, 0]),
                        liquidity: 1_000_000,
                    },
                )
                .unwrap(),
            ),
            SWAP_FEE,
        )
        .unwrap()
    }

    #[test]
    fn exact_output_adds_fee_to_input() {
        let pool = pool();
        let token_amount = EvmTokenAmount { token: TOKEN1, amount: -100 };
        let underlying_quote = pool
            .underlying_pool
            .quote(token_amount)
            .unwrap();
        let quote = pool.quote(token_amount).unwrap();

        assert_eq!(quote.consumed_amount, underlying_quote.consumed_amount);
        assert_eq!(
            quote.calculated_amount,
            amount_before_fee::<Evm>(underlying_quote.calculated_amount, SWAP_FEE).unwrap()
        );
    }
}