use std::any::Any;
use alloy::primitives::{Address as AlloyAddress, U256};
use num_bigint::{BigUint, ToBigUint};
use serde::{Deserialize, Serialize};
use tycho_common::{
dto::ProtocolStateDelta,
models::token::Token,
simulation::{
errors::{SimulationError, TransitionError},
protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
},
Bytes,
};
use crate::evm::{
engine_db::{create_engine, SHARED_TYCHO_DB},
protocol::{
curve::{
adapter::{build_pool, CurveVariant},
math::Pool,
vm,
},
u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
},
};
const FEE_DENOMINATOR: f64 = 1e10;
const STABLESWAP_GAS: u64 = 150_000;
const CRYPTOSWAP_GAS: u64 = 350_000;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CurveState {
pool_address: Bytes,
tokens: Vec<Bytes>,
decimals: Vec<u8>,
variant: CurveVariant,
pool: Pool,
}
impl CurveState {
pub fn new(
pool_address: Bytes,
tokens: Vec<Bytes>,
decimals: Vec<u8>,
variant: CurveVariant,
pool: Pool,
) -> Self {
Self { pool_address, tokens, decimals, variant, pool }
}
fn coin_index(&self, token: &Bytes) -> Result<usize, SimulationError> {
self.tokens
.iter()
.position(|t| t == token)
.ok_or_else(|| {
SimulationError::InvalidInput(
format!("token {token} is not a coin of curve pool {}", self.pool_address),
None,
)
})
}
fn is_crypto(&self) -> bool {
matches!(
self.variant,
CurveVariant::TwoCryptoV1 |
CurveVariant::TwoCryptoNG |
CurveVariant::TwoCryptoStable |
CurveVariant::TriCryptoV1 |
CurveVariant::TriCryptoNG
)
}
fn gas_estimate(&self) -> u64 {
if self.is_crypto() {
CRYPTOSWAP_GAS
} else {
STABLESWAP_GAS
}
}
}
#[typetag::serde]
impl ProtocolSim for CurveState {
fn fee(&self) -> f64 {
let fee = self.pool.fee().or_else(|| {
self.pool
.crypto_fees()
.map(|(mid, _, _)| mid)
});
fee.and_then(|f| u256_to_f64(f).ok())
.map(|f| f / FEE_DENOMINATOR)
.unwrap_or(0.0)
}
fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
let i = self.coin_index(&base.address)?;
let j = self.coin_index("e.address)?;
let (numerator, denominator) = self
.pool
.spot_price(i, j)
.ok_or_else(|| {
SimulationError::RecoverableError(format!(
"curve spot price unavailable for {}",
self.pool_address
))
})?;
let ratio = u256_to_f64(numerator)? / u256_to_f64(denominator)?;
let decimal_adjustment = 10f64.powi(base.decimals as i32 - quote.decimals as i32);
Ok(ratio * decimal_adjustment)
}
fn get_amount_out(
&self,
amount_in: BigUint,
token_in: &Token,
token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
let i = self.coin_index(&token_in.address)?;
let j = self.coin_index(&token_out.address)?;
let dx = biguint_to_u256(&amount_in);
let dy = self
.pool
.get_amount_out(i, j, dx)
.ok_or_else(|| {
SimulationError::RecoverableError(format!(
"curve get_amount_out failed for {}",
self.pool_address
))
})?;
let mut new_pool = self.pool.clone();
let (balance_in, balance_out) = {
let balances = new_pool.balances();
(balances[i], balances[j])
};
new_pool
.set_balance(i, balance_in + dx)
.map_err(|e| SimulationError::FatalError(format!("curve set_balance failed: {e}")))?;
new_pool
.set_balance(j, balance_out.saturating_sub(dy))
.map_err(|e| SimulationError::FatalError(format!("curve set_balance failed: {e}")))?;
let new_state = Self { pool: new_pool, ..self.clone() };
Ok(GetAmountOutResult::new(
u256_to_biguint(dy),
self.gas_estimate()
.to_biguint()
.expect("u64 fits in BigUint"),
Box::new(new_state),
))
}
fn get_limits(
&self,
sell_token: Bytes,
buy_token: Bytes,
) -> Result<(BigUint, BigUint), SimulationError> {
let i = self.coin_index(&sell_token)?;
let j = self.coin_index(&buy_token)?;
let (balance_in, balance_out) = {
let balances = self.pool.balances();
(balances[i], balances[j])
};
if balance_in.is_zero() || balance_out.is_zero() {
return Ok((BigUint::ZERO, BigUint::ZERO));
}
let max_out_reserve = balance_out.saturating_sub(U256::from(1));
let max_out = self
.pool
.get_amount_out(i, j, balance_in)
.ok_or_else(|| {
SimulationError::RecoverableError(format!(
"curve get_limits: solver failed at max input for {}",
self.pool_address
))
})?
.min(max_out_reserve);
Ok((u256_to_biguint(balance_in), u256_to_biguint(max_out)))
}
fn delta_transition(
&mut self,
delta: ProtocolStateDelta,
_tokens: &std::collections::HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError> {
self.pool = match delta
.updated_attributes
.get(vm::POOL_STATE_ADJUSTED)
{
Some(encoded) => {
let state = vm::decode_raw_state(encoded)?;
if state.variant != self.variant {
return Err(SimulationError::FatalError(format!(
"Variant mismatch: expected {}, got {}",
self.variant, state.variant
))
.into())
}
if state.token_decimals != self.decimals {
return Err(SimulationError::FatalError(format!(
"Token decimals mismatch: expected {:?}, got {:?}",
self.decimals, state.token_decimals
))
.into())
}
build_pool(&state).map_err(|e| {
SimulationError::FatalError(format!("curve build_pool failed: {e}"))
})?
}
None => {
let engine = create_engine(SHARED_TYCHO_DB.clone(), false).expect("Infallible");
let pool_address = AlloyAddress::from_slice(self.pool_address.as_ref());
vm::decode_from_vm(&engine, &pool_address, self.variant, &self.decimals)?
}
};
Ok(())
}
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 {
other
.as_any()
.downcast_ref::<Self>()
.is_some_and(|other| self == other)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::evm::protocol::curve::{adapter::RawPoolState, vm::encode_raw_state};
const VARIANT: CurveVariant = CurveVariant::TriCryptoNG;
const DECIMALS: [u8; 3] = [6, 8, 18];
fn u(s: &str) -> U256 {
s.parse().unwrap()
}
fn raw_pool_state(balances: Vec<U256>) -> RawPoolState {
RawPoolState {
variant: VARIANT,
balances,
token_decimals: DECIMALS.to_vec(),
amp: u("1707629"),
mid_fee: Some(u("3000000")),
out_fee: Some(u("30000000")),
fee_gamma: Some(u("500000000000000")),
d: Some(u("7457948167729606869978625")),
gamma: Some(u("11809167828997")),
price_scale: Some(vec![u("59372627314351316239076"), u("1565715369034455123313")]),
..Default::default()
}
}
fn state(balances: Vec<U256>) -> CurveState {
let raw_state = raw_pool_state(balances);
let pool = build_pool(&raw_state).expect("build");
CurveState::new(
Bytes::from([7u8; 20]),
vec![Bytes::from([1u8; 20]), Bytes::from([2u8; 20]), Bytes::from([3u8; 20])],
DECIMALS.to_vec(),
VARIANT,
pool,
)
}
fn delta(attributes: HashMap<String, Bytes>) -> ProtocolStateDelta {
ProtocolStateDelta { updated_attributes: attributes, ..Default::default() }
}
#[test]
fn test_delta_transition_rebuilds_from_attribute() {
let confirmed = vec![u("2466241139205"), u("4200057336"), u("1595469030050811720465")];
let pending = vec![u("2470000000000"), u("4190000000"), u("1600000000000000000000")];
let mut curve = state(confirmed.clone());
let attribute = encode_raw_state(&raw_pool_state(pending.clone())).expect("encode");
curve
.delta_transition(
delta(HashMap::from([(vm::POOL_STATE_ADJUSTED.to_string(), attribute)])),
&HashMap::new(),
&Balances::default(),
)
.expect("delta transition from attribute failed");
assert_eq!(
curve.pool.balances()[..3],
pending[..],
"balances must come from the attribute"
);
assert_ne!(curve.pool.balances()[..3], confirmed[..]);
}
#[test]
fn test_delta_transition_errors_on_variant_mismatch() {
let mut curve = state(vec![u("1"), u("2"), u("3")]);
let mut pending_state = raw_pool_state(curve.pool.balances().to_vec());
pending_state.variant = CurveVariant::StableSwapMeta;
let encoded = encode_raw_state(&pending_state).expect("encode");
let result = curve.delta_transition(
delta(HashMap::from([(vm::POOL_STATE_ADJUSTED.to_string(), encoded)])),
&HashMap::new(),
&Balances::default(),
);
assert!(matches!(result, Err(TransitionError::SimulationError(_))), "got {result:?}");
}
#[test]
fn test_delta_transition_errors_on_decimals_mismatch() {
let mut curve = state(vec![u("1"), u("2"), u("3")]);
let mut pending_state = raw_pool_state(curve.pool.balances().to_vec());
pending_state.token_decimals = vec![18, 18, 18];
let encoded = encode_raw_state(&pending_state).expect("encode");
let result = curve.delta_transition(
delta(HashMap::from([(vm::POOL_STATE_ADJUSTED.to_string(), encoded)])),
&HashMap::new(),
&Balances::default(),
);
assert!(matches!(result, Err(TransitionError::SimulationError(_))), "got {result:?}");
}
#[test]
fn test_delta_transition_rejects_malformed_attribute() {
let mut curve = state(vec![u("1"), u("2"), u("3")]);
let result = curve.delta_transition(
delta(HashMap::from([(
vm::POOL_STATE_ADJUSTED.to_string(),
Bytes::from(b"not json".to_vec()),
)])),
&HashMap::new(),
&Balances::default(),
);
assert!(matches!(result, Err(TransitionError::SimulationError(_))), "got {result:?}");
}
}