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::{
22 adapter::{build_pool, CurveVariant},
23 math::Pool,
24 vm,
25 },
26 u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
27 },
28};
29
30const FEE_DENOMINATOR: f64 = 1e10;
32const STABLESWAP_GAS: u64 = 150_000;
34const CRYPTOSWAP_GAS: u64 = 350_000;
36
37#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub struct CurveState {
50 pool_address: Bytes,
52 tokens: Vec<Bytes>,
54 decimals: Vec<u8>,
56 variant: CurveVariant,
58 pool: Pool,
60}
61
62impl CurveState {
63 pub fn new(
65 pool_address: Bytes,
66 tokens: Vec<Bytes>,
67 decimals: Vec<u8>,
68 variant: CurveVariant,
69 pool: Pool,
70 ) -> Self {
71 Self { pool_address, tokens, decimals, variant, pool }
72 }
73
74 fn coin_index(&self, token: &Bytes) -> Result<usize, SimulationError> {
75 self.tokens
76 .iter()
77 .position(|t| t == token)
78 .ok_or_else(|| {
79 SimulationError::InvalidInput(
80 format!("token {token} is not a coin of curve pool {}", self.pool_address),
81 None,
82 )
83 })
84 }
85
86 fn is_crypto(&self) -> bool {
87 matches!(
88 self.variant,
89 CurveVariant::TwoCryptoV1 |
90 CurveVariant::TwoCryptoNG |
91 CurveVariant::TwoCryptoStable |
92 CurveVariant::TriCryptoV1 |
93 CurveVariant::TriCryptoNG
94 )
95 }
96
97 fn gas_estimate(&self) -> u64 {
98 if self.is_crypto() {
99 CRYPTOSWAP_GAS
100 } else {
101 STABLESWAP_GAS
102 }
103 }
104}
105
106#[typetag::serde]
107impl ProtocolSim for CurveState {
108 fn fee(&self) -> f64 {
109 let fee = self.pool.fee().or_else(|| {
110 self.pool
111 .crypto_fees()
112 .map(|(mid, _, _)| mid)
113 });
114 fee.and_then(|f| u256_to_f64(f).ok())
115 .map(|f| f / FEE_DENOMINATOR)
116 .unwrap_or(0.0)
117 }
118
119 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
120 let i = self.coin_index(&base.address)?;
121 let j = self.coin_index("e.address)?;
122 let (numerator, denominator) = self
123 .pool
124 .spot_price(i, j)
125 .ok_or_else(|| {
126 SimulationError::RecoverableError(format!(
127 "curve spot price unavailable for {}",
128 self.pool_address
129 ))
130 })?;
131 let ratio = u256_to_f64(numerator)? / u256_to_f64(denominator)?;
134 let decimal_adjustment = 10f64.powi(base.decimals as i32 - quote.decimals as i32);
135 Ok(ratio * decimal_adjustment)
136 }
137
138 fn get_amount_out(
139 &self,
140 amount_in: BigUint,
141 token_in: &Token,
142 token_out: &Token,
143 ) -> Result<GetAmountOutResult, SimulationError> {
144 let i = self.coin_index(&token_in.address)?;
145 let j = self.coin_index(&token_out.address)?;
146 let dx = biguint_to_u256(&amount_in);
147
148 let dy = self
149 .pool
150 .get_amount_out(i, j, dx)
151 .ok_or_else(|| {
152 SimulationError::RecoverableError(format!(
153 "curve get_amount_out failed for {}",
154 self.pool_address
155 ))
156 })?;
157
158 let mut new_pool = self.pool.clone();
159 let (balance_in, balance_out) = {
160 let balances = new_pool.balances();
161 (balances[i], balances[j])
162 };
163 new_pool
167 .set_balance(i, balance_in + dx)
168 .map_err(|e| SimulationError::FatalError(format!("curve set_balance failed: {e}")))?;
169 new_pool
170 .set_balance(j, balance_out.saturating_sub(dy))
171 .map_err(|e| SimulationError::FatalError(format!("curve set_balance failed: {e}")))?;
172
173 let new_state = Self { pool: new_pool, ..self.clone() };
174 Ok(GetAmountOutResult::new(
175 u256_to_biguint(dy),
176 self.gas_estimate()
177 .to_biguint()
178 .expect("u64 fits in BigUint"),
179 Box::new(new_state),
180 ))
181 }
182
183 fn get_limits(
184 &self,
185 sell_token: Bytes,
186 buy_token: Bytes,
187 ) -> Result<(BigUint, BigUint), SimulationError> {
188 let i = self.coin_index(&sell_token)?;
189 let j = self.coin_index(&buy_token)?;
190 let (balance_in, balance_out) = {
191 let balances = self.pool.balances();
192 (balances[i], balances[j])
193 };
194 if balance_in.is_zero() || balance_out.is_zero() {
195 return Ok((BigUint::ZERO, BigUint::ZERO));
196 }
197 let max_out_reserve = balance_out.saturating_sub(U256::from(1));
200 let max_out = self
201 .pool
202 .get_amount_out(i, j, balance_in)
203 .ok_or_else(|| {
204 SimulationError::RecoverableError(format!(
205 "curve get_limits: solver failed at max input for {}",
206 self.pool_address
207 ))
208 })?
209 .min(max_out_reserve);
210 Ok((u256_to_biguint(balance_in), u256_to_biguint(max_out)))
211 }
212
213 fn delta_transition(
220 &mut self,
221 delta: ProtocolStateDelta,
222 _tokens: &std::collections::HashMap<Bytes, Token>,
223 _balances: &Balances,
224 ) -> Result<(), TransitionError> {
225 self.pool = match delta
226 .updated_attributes
227 .get(vm::POOL_STATE_ADJUSTED)
228 {
229 Some(encoded) => {
230 let state = vm::decode_raw_state(encoded)?;
231 if state.variant != self.variant {
232 return Err(SimulationError::FatalError(format!(
233 "Variant mismatch: expected {}, got {}",
234 self.variant, state.variant
235 ))
236 .into())
237 }
238 if state.token_decimals != self.decimals {
239 return Err(SimulationError::FatalError(format!(
240 "Token decimals mismatch: expected {:?}, got {:?}",
241 self.decimals, state.token_decimals
242 ))
243 .into())
244 }
245 build_pool(&state).map_err(|e| {
246 SimulationError::FatalError(format!("curve build_pool failed: {e}"))
247 })?
248 }
249 None => {
250 let engine = create_engine(SHARED_TYCHO_DB.clone(), false).expect("Infallible");
251 let pool_address = AlloyAddress::from_slice(self.pool_address.as_ref());
252 vm::decode_from_vm(&engine, &pool_address, self.variant, &self.decimals)?
253 }
254 };
255 Ok(())
256 }
257
258 fn clone_box(&self) -> Box<dyn ProtocolSim> {
259 Box::new(self.clone())
260 }
261
262 fn as_any(&self) -> &dyn Any {
263 self
264 }
265
266 fn as_any_mut(&mut self) -> &mut dyn Any {
267 self
268 }
269
270 fn eq(&self, other: &dyn ProtocolSim) -> bool {
271 other
272 .as_any()
273 .downcast_ref::<Self>()
274 .is_some_and(|other| self == other)
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use std::collections::HashMap;
281
282 use super::*;
283 use crate::evm::protocol::curve::{adapter::RawPoolState, vm::encode_raw_state};
284
285 const VARIANT: CurveVariant = CurveVariant::TriCryptoNG;
286 const DECIMALS: [u8; 3] = [6, 8, 18];
287
288 fn u(s: &str) -> U256 {
289 s.parse().unwrap()
290 }
291
292 fn raw_pool_state(balances: Vec<U256>) -> RawPoolState {
294 RawPoolState {
295 variant: VARIANT,
296 balances,
297 token_decimals: DECIMALS.to_vec(),
298 amp: u("1707629"),
299 mid_fee: Some(u("3000000")),
300 out_fee: Some(u("30000000")),
301 fee_gamma: Some(u("500000000000000")),
302 d: Some(u("7457948167729606869978625")),
303 gamma: Some(u("11809167828997")),
304 price_scale: Some(vec![u("59372627314351316239076"), u("1565715369034455123313")]),
305 ..Default::default()
306 }
307 }
308
309 fn state(balances: Vec<U256>) -> CurveState {
310 let raw_state = raw_pool_state(balances);
311 let pool = build_pool(&raw_state).expect("build");
312 CurveState::new(
313 Bytes::from([7u8; 20]),
314 vec![Bytes::from([1u8; 20]), Bytes::from([2u8; 20]), Bytes::from([3u8; 20])],
315 DECIMALS.to_vec(),
316 VARIANT,
317 pool,
318 )
319 }
320
321 fn delta(attributes: HashMap<String, Bytes>) -> ProtocolStateDelta {
322 ProtocolStateDelta { updated_attributes: attributes, ..Default::default() }
323 }
324
325 #[test]
326 fn test_delta_transition_rebuilds_from_attribute() {
327 let confirmed = vec![u("2466241139205"), u("4200057336"), u("1595469030050811720465")];
328 let pending = vec![u("2470000000000"), u("4190000000"), u("1600000000000000000000")];
329 let mut curve = state(confirmed.clone());
330 let attribute = encode_raw_state(&raw_pool_state(pending.clone())).expect("encode");
331
332 curve
333 .delta_transition(
334 delta(HashMap::from([(vm::POOL_STATE_ADJUSTED.to_string(), attribute)])),
335 &HashMap::new(),
336 &Balances::default(),
337 )
338 .expect("delta transition from attribute failed");
339
340 assert_eq!(
343 curve.pool.balances()[..3],
344 pending[..],
345 "balances must come from the attribute"
346 );
347 assert_ne!(curve.pool.balances()[..3], confirmed[..]);
348 }
349
350 #[test]
351 fn test_delta_transition_errors_on_variant_mismatch() {
352 let mut curve = state(vec![u("1"), u("2"), u("3")]);
353 let mut pending_state = raw_pool_state(curve.pool.balances().to_vec());
354 pending_state.variant = CurveVariant::StableSwapMeta;
355 let encoded = encode_raw_state(&pending_state).expect("encode");
356
357 let result = curve.delta_transition(
358 delta(HashMap::from([(vm::POOL_STATE_ADJUSTED.to_string(), encoded)])),
359 &HashMap::new(),
360 &Balances::default(),
361 );
362
363 assert!(matches!(result, Err(TransitionError::SimulationError(_))), "got {result:?}");
364 }
365
366 #[test]
367 fn test_delta_transition_errors_on_decimals_mismatch() {
368 let mut curve = state(vec![u("1"), u("2"), u("3")]);
369 let mut pending_state = raw_pool_state(curve.pool.balances().to_vec());
370 pending_state.token_decimals = vec![18, 18, 18];
371 let encoded = encode_raw_state(&pending_state).expect("encode");
372
373 let result = curve.delta_transition(
374 delta(HashMap::from([(vm::POOL_STATE_ADJUSTED.to_string(), encoded)])),
375 &HashMap::new(),
376 &Balances::default(),
377 );
378
379 assert!(matches!(result, Err(TransitionError::SimulationError(_))), "got {result:?}");
380 }
381
382 #[test]
383 fn test_delta_transition_rejects_malformed_attribute() {
384 let mut curve = state(vec![u("1"), u("2"), u("3")]);
385
386 let result = curve.delta_transition(
387 delta(HashMap::from([(
388 vm::POOL_STATE_ADJUSTED.to_string(),
389 Bytes::from(b"not json".to_vec()),
390 )])),
391 &HashMap::new(),
392 &Balances::default(),
393 );
394
395 assert!(matches!(result, Err(TransitionError::SimulationError(_))), "got {result:?}");
398 }
399}