tycho_simulation/evm/protocol/curve/
state.rs1use std::any::Any;
4
5use alloy::primitives::{Address as AlloyAddress, U256};
6use num_bigint::{BigUint, ToBigUint};
7use serde::{Deserialize, Serialize};
8use tycho_common::{
9 dto::ProtocolStateDelta,
10 models::token::Token,
11 simulation::{
12 errors::{SimulationError, TransitionError},
13 protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
14 },
15 Bytes,
16};
17
18use crate::evm::{
19 engine_db::{create_engine, SHARED_TYCHO_DB},
20 protocol::{
21 curve::{adapter::CurveVariant, math::Pool, vm},
22 u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
23 },
24};
25
26const FEE_DENOMINATOR: f64 = 1e10;
28const STABLESWAP_GAS: u64 = 150_000;
30const CRYPTOSWAP_GAS: u64 = 350_000;
32
33#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
45pub struct CurveState {
46 pool_address: Bytes,
48 tokens: Vec<Bytes>,
50 decimals: Vec<u8>,
52 variant: CurveVariant,
54 pool: Pool,
56}
57
58impl CurveState {
59 pub(super) fn new(
61 pool_address: Bytes,
62 tokens: Vec<Bytes>,
63 decimals: Vec<u8>,
64 variant: CurveVariant,
65 pool: Pool,
66 ) -> Self {
67 Self { pool_address, tokens, decimals, variant, pool }
68 }
69
70 fn coin_index(&self, token: &Bytes) -> Result<usize, SimulationError> {
71 self.tokens
72 .iter()
73 .position(|t| t == token)
74 .ok_or_else(|| {
75 SimulationError::InvalidInput(
76 format!("token {token} is not a coin of curve pool {}", self.pool_address),
77 None,
78 )
79 })
80 }
81
82 fn is_crypto(&self) -> bool {
83 matches!(
84 self.variant,
85 CurveVariant::TwoCryptoV1 |
86 CurveVariant::TwoCryptoNG |
87 CurveVariant::TwoCryptoStable |
88 CurveVariant::TriCryptoV1 |
89 CurveVariant::TriCryptoNG
90 )
91 }
92
93 fn gas_estimate(&self) -> u64 {
94 if self.is_crypto() {
95 CRYPTOSWAP_GAS
96 } else {
97 STABLESWAP_GAS
98 }
99 }
100}
101
102#[typetag::serde]
103impl ProtocolSim for CurveState {
104 fn fee(&self) -> f64 {
105 let fee = self.pool.fee().or_else(|| {
106 self.pool
107 .crypto_fees()
108 .map(|(mid, _, _)| mid)
109 });
110 fee.and_then(|f| u256_to_f64(f).ok())
111 .map(|f| f / FEE_DENOMINATOR)
112 .unwrap_or(0.0)
113 }
114
115 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
116 let i = self.coin_index(&base.address)?;
117 let j = self.coin_index("e.address)?;
118 let (numerator, denominator) = self
119 .pool
120 .spot_price(i, j)
121 .ok_or_else(|| {
122 SimulationError::RecoverableError(format!(
123 "curve spot price unavailable for {}",
124 self.pool_address
125 ))
126 })?;
127 let ratio = u256_to_f64(numerator)? / u256_to_f64(denominator)?;
130 let decimal_adjustment = 10f64.powi(base.decimals as i32 - quote.decimals as i32);
131 Ok(ratio * decimal_adjustment)
132 }
133
134 fn get_amount_out(
135 &self,
136 amount_in: BigUint,
137 token_in: &Token,
138 token_out: &Token,
139 ) -> Result<GetAmountOutResult, SimulationError> {
140 let i = self.coin_index(&token_in.address)?;
141 let j = self.coin_index(&token_out.address)?;
142 let dx = biguint_to_u256(&amount_in);
143
144 let dy = self
145 .pool
146 .get_amount_out(i, j, dx)
147 .ok_or_else(|| {
148 SimulationError::RecoverableError(format!(
149 "curve get_amount_out failed for {}",
150 self.pool_address
151 ))
152 })?;
153
154 let mut new_pool = self.pool.clone();
155 let (balance_in, balance_out) = {
156 let balances = new_pool.balances();
157 (balances[i], balances[j])
158 };
159 new_pool
163 .set_balance(i, balance_in + dx)
164 .map_err(|e| SimulationError::FatalError(format!("curve set_balance failed: {e}")))?;
165 new_pool
166 .set_balance(j, balance_out.saturating_sub(dy))
167 .map_err(|e| SimulationError::FatalError(format!("curve set_balance failed: {e}")))?;
168
169 let new_state = Self { pool: new_pool, ..self.clone() };
170 Ok(GetAmountOutResult::new(
171 u256_to_biguint(dy),
172 self.gas_estimate()
173 .to_biguint()
174 .expect("u64 fits in BigUint"),
175 Box::new(new_state),
176 ))
177 }
178
179 fn get_limits(
180 &self,
181 sell_token: Bytes,
182 buy_token: Bytes,
183 ) -> Result<(BigUint, BigUint), SimulationError> {
184 let i = self.coin_index(&sell_token)?;
185 let j = self.coin_index(&buy_token)?;
186 let (balance_in, balance_out) = {
187 let balances = self.pool.balances();
188 (balances[i], balances[j])
189 };
190 if balance_in.is_zero() || balance_out.is_zero() {
191 return Ok((BigUint::ZERO, BigUint::ZERO));
192 }
193 let max_out_reserve = balance_out.saturating_sub(U256::from(1));
196 let max_out = self
197 .pool
198 .get_amount_out(i, j, balance_in)
199 .ok_or_else(|| {
200 SimulationError::RecoverableError(format!(
201 "curve get_limits: solver failed at max input for {}",
202 self.pool_address
203 ))
204 })?
205 .min(max_out_reserve);
206 Ok((u256_to_biguint(balance_in), u256_to_biguint(max_out)))
207 }
208
209 fn delta_transition(
210 &mut self,
211 _delta: ProtocolStateDelta,
212 _tokens: &std::collections::HashMap<Bytes, Token>,
213 _balances: &Balances,
214 ) -> Result<(), TransitionError> {
215 let engine = create_engine(SHARED_TYCHO_DB.clone(), false).expect("Infallible");
216 let pool_address = AlloyAddress::from_slice(self.pool_address.as_ref());
217 self.pool = vm::decode_from_vm(&engine, &pool_address, self.variant, &self.decimals)
218 .map_err(TransitionError::SimulationError)?;
219 Ok(())
220 }
221
222 fn clone_box(&self) -> Box<dyn ProtocolSim> {
223 Box::new(self.clone())
224 }
225
226 fn as_any(&self) -> &dyn Any {
227 self
228 }
229
230 fn as_any_mut(&mut self) -> &mut dyn Any {
231 self
232 }
233
234 fn eq(&self, other: &dyn ProtocolSim) -> bool {
235 other
236 .as_any()
237 .downcast_ref::<Self>()
238 .is_some_and(|other| self == other)
239 }
240}