1use std::fmt::Debug;
8
9use alloy::{
10 core::sol,
11 primitives::{Address as AlloyAddress, U256},
12 sol_types::SolCall,
13};
14use revm::{state::AccountInfo, DatabaseRef};
15use tycho_common::{simulation::errors::SimulationError, Bytes};
16
17use crate::evm::{
18 engine_db::engine_db_interface::EngineDatabaseInterface,
19 protocol::{
20 curve::{
21 adapter::{build_pool, detect_eth_variant, CurveVariant, ProbingResults, RawPoolState},
22 math::Pool,
23 },
24 vm::utils::get_code_for_contract,
25 },
26 simulation::{PendingOverrides, SimulationEngine},
27};
28
29sol! {
30 #[allow(missing_docs)]
31 interface ICurve {
32 function balances(uint256 i) external view returns (uint256);
33 function A() external view returns (uint256);
34 function fee() external view returns (uint256);
35 function initial_A() external view returns (uint256);
36 function future_A() external view returns (uint256);
37 function offpeg_fee_multiplier() external view returns (uint256);
38 function gamma() external view returns (uint256);
39 function D() external view returns (uint256);
40 function mid_fee() external view returns (uint256);
41 function out_fee() external view returns (uint256);
42 function fee_gamma() external view returns (uint256);
43 function price_scale() external view returns (uint256);
44 function MATH() external view returns (address);
45 function base_pool() external view returns (address);
46 function version() external view returns (string);
47 }
48
49 #[allow(missing_docs)]
50 interface ICurveOld {
51 function balances(int128 i) external view returns (uint256);
52 }
53
54 #[allow(missing_docs)]
55 interface ICurveTri {
56 function price_scale(uint256 i) external view returns (uint256);
57 function precisions() external view returns (uint256[3]);
58 }
59
60 #[allow(missing_docs)]
61 interface ICurveTwo {
62 function precisions() external view returns (uint256[2]);
63 }
64
65 #[allow(missing_docs)]
66 interface IBasePool {
67 function get_virtual_price() external view returns (uint256);
68 }
69}
70
71const A_PRECISION: u64 = 100;
73const STORED_RATES_SELECTOR: [u8; 4] = [0xfd, 0x06, 0x84, 0xb1];
76
77pub const POOL_STATE_ADJUSTED: &str = "pool_state_adjusted";
82
83pub fn encode_raw_state(state: &RawPoolState) -> Result<Bytes, SimulationError> {
85 serde_json::to_vec(state)
86 .map(Bytes::from)
87 .map_err(|e| SimulationError::FatalError(format!("curve state encode failed: {e}")))
88}
89
90pub fn decode_raw_state(bytes: &[u8]) -> Result<RawPoolState, SimulationError> {
92 serde_json::from_slice(bytes)
93 .map_err(|e| SimulationError::FatalError(format!("curve state decode failed: {e}")))
94}
95
96pub fn decode_from_vm<D: EngineDatabaseInterface + Clone + Debug>(
102 engine: &SimulationEngine<D>,
103 pool: &AlloyAddress,
104 variant: CurveVariant,
105 token_decimals: &[u8],
106) -> Result<Pool, SimulationError>
107where
108 <D as DatabaseRef>::Error: Debug,
109 <D as EngineDatabaseInterface>::Error: Debug,
110{
111 let state =
112 read_raw_pool_state(engine, pool, variant, token_decimals, &PendingOverrides::default())?;
113 build_pool(&state)
114 .map_err(|e| SimulationError::FatalError(format!("curve build_pool failed: {e}")))
115}
116
117pub fn read_raw_pool_state<D: EngineDatabaseInterface + Clone + Debug>(
127 engine: &SimulationEngine<D>,
128 pool: &AlloyAddress,
129 variant: CurveVariant,
130 decimals: &[u8],
131 overrides: &PendingOverrides,
132) -> Result<RawPoolState, SimulationError>
133where
134 <D as DatabaseRef>::Error: Debug,
135 <D as EngineDatabaseInterface>::Error: Debug,
136{
137 PoolReader::new(engine, overrides).read_pool(pool, variant, decimals)
138}
139
140struct PoolReader<'a, D: EngineDatabaseInterface + Clone + Debug>
145where
146 <D as DatabaseRef>::Error: Debug,
147 <D as EngineDatabaseInterface>::Error: Debug,
148{
149 engine: &'a SimulationEngine<D>,
150 overrides: &'a PendingOverrides,
151}
152
153impl<'a, D: EngineDatabaseInterface + Clone + Debug> PoolReader<'a, D>
154where
155 <D as DatabaseRef>::Error: Debug,
156 <D as EngineDatabaseInterface>::Error: Debug,
157{
158 fn new(engine: &'a SimulationEngine<D>, overrides: &'a PendingOverrides) -> Self {
159 Self { engine, overrides }
160 }
161
162 fn read_pool(
163 &self,
164 pool: &AlloyAddress,
165 variant: CurveVariant,
166 decimals: &[u8],
167 ) -> Result<RawPoolState, SimulationError> {
168 let n_coins = decimals.len();
169 match variant {
170 CurveVariant::StableSwapV0 => Ok(RawPoolState {
171 variant,
172 balances: self.read_balances_int128(pool, n_coins)?,
173 amp: self.call(pool, ICurve::ACall {})?,
174 fee: Some(self.call(pool, ICurve::feeCall {})?),
175 token_decimals: decimals.into(),
176 ..Default::default()
177 }),
178 CurveVariant::StableSwapV1 => Ok(RawPoolState {
179 variant,
180 balances: self.read_balances(pool, n_coins)?,
181 amp: self.call(pool, ICurve::ACall {})?,
182 fee: Some(self.call(pool, ICurve::feeCall {})?),
183 token_decimals: decimals.into(),
184 ..Default::default()
185 }),
186 CurveVariant::StableSwapV2 | CurveVariant::StableSwapSTETH => Ok(RawPoolState {
187 variant,
188 balances: self.read_balances(pool, n_coins)?,
189 amp: self.read_ramped_amp(pool)?,
190 fee: Some(self.call(pool, ICurve::feeCall {})?),
191 token_decimals: decimals.into(),
192 ..Default::default()
193 }),
194 CurveVariant::StableSwapALend => Ok(RawPoolState {
195 variant,
196 balances: self.read_balances(pool, n_coins)?,
197 amp: self.read_ramped_amp(pool)?,
198 fee: Some(self.call(pool, ICurve::feeCall {})?),
199 offpeg_fee_multiplier: Some(self.call(pool, ICurve::offpeg_fee_multiplierCall {})?),
200 token_decimals: decimals.into(),
201 ..Default::default()
202 }),
203 CurveVariant::StableSwapNG => Ok(RawPoolState {
204 variant,
205 balances: self.read_balances(pool, n_coins)?,
206 amp: self.read_ramped_amp(pool)?,
207 fee: Some(self.call(pool, ICurve::feeCall {})?),
208 offpeg_fee_multiplier: self.call_opt(pool, ICurve::offpeg_fee_multiplierCall {}),
210 dynamic_rates: self.read_stored_rates(pool, n_coins),
211 token_decimals: decimals.into(),
212 ..Default::default()
213 }),
214 CurveVariant::StableSwapMeta => {
215 let mut dynamic_rates = vec![None; n_coins];
216 if let Some(last) = dynamic_rates.last_mut() {
217 *last = Some(self.read_base_virtual_price(pool)?);
218 }
219 Ok(RawPoolState {
220 variant,
221 balances: self.read_balances(pool, n_coins)?,
222 amp: self.read_ramped_amp(pool)?,
223 fee: Some(self.call(pool, ICurve::feeCall {})?),
224 dynamic_rates: Some(dynamic_rates),
225 token_decimals: decimals.into(),
226 ..Default::default()
227 })
228 }
229 CurveVariant::TwoCryptoV1 |
230 CurveVariant::TwoCryptoNG |
231 CurveVariant::TwoCryptoStable => self.read_twocrypto(pool, variant, decimals),
232 CurveVariant::TriCryptoV1 | CurveVariant::TriCryptoNG => {
233 self.read_tricrypto(pool, variant, decimals)
234 }
235 }
236 }
237
238 fn read_twocrypto(
239 &self,
240 pool: &AlloyAddress,
241 variant: CurveVariant,
242 decimals: &[u8],
243 ) -> Result<RawPoolState, SimulationError> {
244 let balances = self.read_balances(pool, 2)?;
245 let price_scale = self.call(pool, ICurve::price_scaleCall {})?;
246 let precisions = self
247 .call_opt(pool, ICurveTwo::precisionsCall {})
248 .map(|p| p.to_vec());
249 let gamma = if variant == CurveVariant::TwoCryptoStable {
250 None
251 } else {
252 Some(self.call(pool, ICurve::gammaCall {})?)
253 };
254 let eth_variant = if variant == CurveVariant::TwoCryptoV1 {
255 Some(detect_eth_variant(*pool))
256 } else {
257 None
258 };
259 Ok(RawPoolState {
260 variant,
261 balances,
262 amp: self.call(pool, ICurve::ACall {})?,
263 mid_fee: Some(self.call(pool, ICurve::mid_feeCall {})?),
264 out_fee: Some(self.call(pool, ICurve::out_feeCall {})?),
265 fee_gamma: Some(self.call(pool, ICurve::fee_gammaCall {})?),
266 d: Some(self.call(pool, ICurve::DCall {})?),
267 gamma,
268 price_scale: Some(vec![price_scale]),
269 precisions,
270 eth_variant,
271 token_decimals: decimals.into(),
272 ..Default::default()
273 })
274 }
275
276 fn read_tricrypto(
277 &self,
278 pool: &AlloyAddress,
279 variant: CurveVariant,
280 decimals: &[u8],
281 ) -> Result<RawPoolState, SimulationError> {
282 let balances = self.read_balances(pool, 3)?;
283 let ps0 = self.call(pool, ICurveTri::price_scaleCall { i: U256::from(0) })?;
284 let ps1 = self.call(pool, ICurveTri::price_scaleCall { i: U256::from(1) })?;
285 let precisions = self
286 .call_opt(pool, ICurveTri::precisionsCall {})
287 .map(|p| p.to_vec());
288 Ok(RawPoolState {
289 variant,
290 balances,
291 amp: self.call(pool, ICurve::ACall {})?,
292 mid_fee: Some(self.call(pool, ICurve::mid_feeCall {})?),
293 out_fee: Some(self.call(pool, ICurve::out_feeCall {})?),
294 fee_gamma: Some(self.call(pool, ICurve::fee_gammaCall {})?),
295 d: Some(self.call(pool, ICurve::DCall {})?),
296 gamma: Some(self.call(pool, ICurve::gammaCall {})?),
297 price_scale: Some(vec![ps0, ps1]),
298 precisions,
299 token_decimals: decimals.into(),
300 ..Default::default()
301 })
302 }
303
304 fn read_ramped_amp(&self, pool: &AlloyAddress) -> Result<U256, SimulationError> {
305 let initial_a = self.call_opt(pool, ICurve::initial_ACall {});
306 let future_a = self.call_opt(pool, ICurve::future_ACall {});
307 match (initial_a, future_a) {
308 (Some(ia), Some(fa)) if ia == fa => Ok(ia),
309 _ => Ok(self.call(pool, ICurve::ACall {})? * U256::from(A_PRECISION)),
317 }
318 }
319
320 fn read_balances(
321 &self,
322 pool: &AlloyAddress,
323 n_coins: usize,
324 ) -> Result<Vec<U256>, SimulationError> {
325 let mut balances = Vec::with_capacity(n_coins);
326 for i in 0..n_coins {
327 balances.push(self.call(pool, ICurve::balancesCall { i: U256::from(i) })?);
328 }
329 Ok(balances)
330 }
331
332 fn read_balances_int128(
333 &self,
334 pool: &AlloyAddress,
335 n_coins: usize,
336 ) -> Result<Vec<U256>, SimulationError> {
337 let mut balances = Vec::with_capacity(n_coins);
338 for i in 0..n_coins {
339 balances.push(self.call(pool, ICurveOld::balancesCall { i: i as i128 })?);
340 }
341 Ok(balances)
342 }
343
344 fn read_base_virtual_price(&self, pool: &AlloyAddress) -> Result<U256, SimulationError> {
347 let base_pool = self.call(pool, ICurve::base_poolCall {})?;
348 self.call(&base_pool, IBasePool::get_virtual_priceCall {})
349 }
350
351 fn read_stored_rates(&self, pool: &AlloyAddress, n_coins: usize) -> Option<Vec<Option<U256>>> {
354 let res = self
355 .engine
356 .simulate(
357 &self
358 .overrides
359 .view_call(*pool, STORED_RATES_SELECTOR.to_vec()),
360 )
361 .ok()?;
362 let out = res.result.as_ref();
363 if out.len() < n_coins * 32 {
364 return None;
365 }
366 let first_word = U256::from_be_slice(&out[..32]);
369 let data_offset = if first_word <= U256::from(256u64) && out.len() >= (n_coins + 2) * 32 {
370 64
371 } else {
372 0
373 };
374 let rates = (0..n_coins)
375 .map(|i| {
376 let start = data_offset + i * 32;
377 Some(U256::from_be_slice(&out[start..start + 32]))
378 })
379 .collect();
380 Some(rates)
381 }
382
383 fn call<C, R>(&self, to: &AlloyAddress, sol_call: C) -> Result<R, SimulationError>
384 where
385 C: SolCall<Return = R>,
386 {
387 let res = self
388 .engine
389 .simulate(
390 &self
391 .overrides
392 .view_call(*to, sol_call.abi_encode()),
393 )
394 .map_err(|e| {
395 SimulationError::RecoverableError(format!("curve getter call failed: {e}"))
396 })?;
397 C::abi_decode_returns(res.result.as_ref())
398 .map_err(|e| SimulationError::FatalError(format!("curve getter decode failed: {e}")))
399 }
400
401 fn call_opt<C, R>(&self, to: &AlloyAddress, sol_call: C) -> Option<R>
402 where
403 C: SolCall<Return = R>,
404 {
405 self.call(to, sol_call).ok()
406 }
407}
408
409pub fn read_math_address<D: EngineDatabaseInterface + Clone + Debug>(
411 engine: &SimulationEngine<D>,
412 pool: &AlloyAddress,
413) -> Option<AlloyAddress>
414where
415 <D as DatabaseRef>::Error: Debug,
416 <D as EngineDatabaseInterface>::Error: Debug,
417{
418 PoolReader::new(engine, &PendingOverrides::default()).call_opt(pool, ICurve::MATHCall {})
419}
420
421pub async fn load_math_contract<D: EngineDatabaseInterface + Clone + Debug>(
429 engine: &SimulationEngine<D>,
430 pool: &AlloyAddress,
431) -> Result<(), SimulationError>
432where
433 <D as DatabaseRef>::Error: Debug,
434 <D as EngineDatabaseInterface>::Error: Debug,
435{
436 let Some(math) = read_math_address(engine, pool) else {
437 return Ok(());
438 };
439 let code = get_code_for_contract(&math.to_string(), None)
440 .await
441 .map_err(|e| {
442 SimulationError::RecoverableError(format!(
443 "curve: failed to fetch MATH() code for {math}: {e}"
444 ))
445 })?;
446 engine
447 .state
448 .init_account(
449 math,
450 AccountInfo {
451 balance: U256::ZERO,
452 nonce: 0,
453 code_hash: code.hash_slow(),
454 code: Some(code),
455 },
456 None,
457 false,
458 )
459 .map_err(|e| {
460 SimulationError::FatalError(format!(
461 "curve: failed to load MATH() code for {math}: {e:?}"
462 ))
463 })?;
464 Ok(())
465}
466
467pub fn read_version<D: EngineDatabaseInterface + Clone + Debug>(
469 engine: &SimulationEngine<D>,
470 address: &AlloyAddress,
471) -> Option<String>
472where
473 <D as DatabaseRef>::Error: Debug,
474 <D as EngineDatabaseInterface>::Error: Debug,
475{
476 PoolReader::new(engine, &PendingOverrides::default()).call_opt(address, ICurve::versionCall {})
477}
478
479pub fn probe<D: EngineDatabaseInterface + Clone + Debug>(
484 engine: &SimulationEngine<D>,
485 pool: &AlloyAddress,
486 n_coins: usize,
487) -> ProbingResults
488where
489 <D as DatabaseRef>::Error: Debug,
490 <D as EngineDatabaseInterface>::Error: Debug,
491{
492 let overrides = PendingOverrides::default();
495 let reader = PoolReader::new(engine, &overrides);
496 let math: Option<AlloyAddress> = reader.call_opt(pool, ICurve::MATHCall {});
497 ProbingResults {
498 has_gamma: reader
499 .call_opt(pool, ICurve::gammaCall {})
500 .is_some(),
501 n_coins,
502 has_math: math.is_some(),
503 math_version: math.and_then(|math| reader.call_opt(&math, ICurve::versionCall {})),
506 has_offpeg_fee_multiplier: reader
507 .call_opt(pool, ICurve::offpeg_fee_multiplierCall {})
508 .is_some(),
509 has_stored_rates: reader
510 .read_stored_rates(pool, n_coins)
511 .is_some(),
512 has_version: reader
513 .call_opt(pool, ICurve::versionCall {})
514 .is_some(),
515 has_base_pool: reader
516 .call_opt(pool, ICurve::base_poolCall {})
517 .is_some(),
518 has_int128_balances: reader
519 .call_opt(pool, ICurveOld::balancesCall { i: 0 })
520 .is_some(),
521 pool_address: *pool,
522 }
523}
524
525#[cfg(test)]
526mod test {
527 use std::str::FromStr;
528
529 use alloy::sol;
530 use tycho_client::feed::BlockHeader;
531
532 use super::*;
533 use crate::evm::{
534 engine_db::{
535 simulation_db::SimulationDB,
536 utils::{get_client, get_runtime},
537 },
538 simulation::SimulationEngine,
539 };
540
541 sol! {
542 function get_dy_stable(int128 i, int128 j, uint256 dx) external view returns (uint256);
543 function coins(uint256 i) external view returns (address);
544 function decimals() external view returns (uint8);
545 }
546
547 const ETH_PLACEHOLDER: AlloyAddress =
548 alloy::primitives::address!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee");
549
550 fn read_decimals<D: EngineDatabaseInterface + Clone + Debug>(
552 engine: &SimulationEngine<D>,
553 pool: &AlloyAddress,
554 n: usize,
555 ) -> Vec<u8>
556 where
557 <D as DatabaseRef>::Error: Debug,
558 <D as EngineDatabaseInterface>::Error: Debug,
559 {
560 let overrides = PendingOverrides::default();
561 let reader = PoolReader::new(engine, &overrides);
562 (0..n)
563 .map(|i| {
564 let coin: AlloyAddress = reader
565 .call(pool, coinsCall { i: U256::from(i) })
566 .unwrap();
567 if coin == ETH_PLACEHOLDER {
568 18
569 } else {
570 reader
571 .call(&coin, decimalsCall {})
572 .unwrap()
573 }
574 })
575 .collect()
576 }
577
578 fn assert_stable_matches_onchain(
581 pool: &str,
582 variant: CurveVariant,
583 n_coins: usize,
584 swap: (usize, usize, U256),
585 block: (u64, u64),
586 ) {
587 let (i, j, dx) = swap;
588 let (block_number, timestamp) = block;
589 let header = BlockHeader { number: block_number, timestamp, ..Default::default() };
590 let mut db = SimulationDB::new(get_client(None).unwrap(), get_runtime().unwrap(), None);
591 db.set_block(Some(header));
592 let engine = SimulationEngine::new(db, false);
593 let pool = AlloyAddress::from_str(pool).unwrap();
594 let decimals = read_decimals(&engine, &pool, n_coins);
595
596 let decoded = decode_from_vm(&engine, &pool, variant, &decimals).expect("decode failed");
597 let ours = decoded
598 .get_amount_out(i, j, dx)
599 .expect("get_amount_out returned None");
600
601 let onchain: U256 = PoolReader::new(&engine, &PendingOverrides::default())
602 .call(&pool, get_dy_stableCall { i: i as i128, j: j as i128, dx })
603 .expect("on-chain get_dy failed");
604
605 assert_eq!(ours, onchain, "curve quote diverged from on-chain get_dy");
606 }
607
608 #[test]
611 fn raw_pool_state_roundtrips_through_the_attribute_encoding() {
612 let u = |s: &str| s.parse::<U256>().unwrap();
613 let state = RawPoolState {
614 variant: CurveVariant::StableSwapV2,
615 balances: vec![u("2466241139205"), u("4200057336")],
616 token_decimals: vec![18, 6],
617 amp: u("1707629"),
618 fee: Some(u("4000000")),
619 mid_fee: Some(u("3000000")),
620 out_fee: Some(u("30000000")),
621 fee_gamma: Some(u("500000000000000")),
622 offpeg_fee_multiplier: Some(u("20000000000")),
623 price_scale: Some(vec![u("59372627314351316239076")]),
624 d: Some(u("7457948167729606869978625")),
625 gamma: Some(u("11809167828997")),
626 dynamic_rates: Some(vec![Some(u("1000000000000000000")), None]),
627 precisions: Some(vec![u("1"), u("1000000000000")]),
628 eth_variant: Some(true),
629 };
630
631 let encoded = encode_raw_state(&state).expect("encode failed");
632
633 assert_eq!(decode_raw_state(&encoded).expect("decode failed"), state);
634 }
635
636 #[test]
637 fn decoding_malformed_readings_fails() {
638 assert!(decode_raw_state(b"not json").is_err());
639 }
640
641 #[test]
645 fn tricrypto_ng_build_pool_matches_onchain_get_dy() {
646 let u = |s: &str| s.parse::<U256>().unwrap();
647 let state = RawPoolState {
648 variant: CurveVariant::TriCryptoNG,
649 balances: vec![u("2466241139205"), u("4200057336"), u("1595469030050811720465")],
650 token_decimals: vec![6, 8, 18],
651 amp: u("1707629"),
652 mid_fee: Some(u("3000000")),
653 out_fee: Some(u("30000000")),
654 fee_gamma: Some(u("500000000000000")),
655 d: Some(u("7457948167729606869978625")),
656 gamma: Some(u("11809167828997")),
657 price_scale: Some(vec![u("59372627314351316239076"), u("1565715369034455123313")]),
658 ..Default::default()
659 };
660 let pool = build_pool(&state).expect("build_pool failed");
661 let dx = U256::from(1_000_000_000u64); assert_eq!(pool.get_amount_out(0, 1, dx), Some(u("1690920")), "USDC->WBTC");
663 assert_eq!(pool.get_amount_out(0, 2, dx), Some(u("641654961086650131")), "USDC->WETH");
664 }
665
666 #[test]
673 fn twocrypto_v1_eth_variant_build_pool_matches_onchain_get_dy() {
674 let u = |s: &str| s.parse::<U256>().unwrap();
675 let make = |eth_variant: bool| {
676 build_pool(&RawPoolState {
677 variant: CurveVariant::TwoCryptoV1,
678 balances: vec![u("33389428640940997105"), u("1538654846140514380725767708")],
679 token_decimals: vec![18, 18], amp: u("400000"),
681 gamma: Some(u("145000000000000")),
682 d: Some(u("3338917956508624293928")),
683 price_scale: Some(vec![u("52805053500476")]),
684 mid_fee: Some(U256::from(26_000_000u64)),
685 out_fee: Some(U256::from(45_000_000u64)),
686 fee_gamma: Some(u("230000000000000")),
687 eth_variant: Some(eth_variant),
688 ..Default::default()
689 })
690 .expect("build_pool")
691 };
692 let pool = make(true);
693 assert_eq!(
695 pool.get_amount_out(0, 1, u("1000000000000000000")),
696 Some(u("44131555012248406155621024")),
697 "1 WETH -> CRV",
698 );
699 assert_eq!(
700 pool.get_amount_out(0, 1, u("500000000000000000")),
701 Some(u("22389351263618692591189738")),
702 "0.5 WETH -> CRV",
703 );
704 assert_eq!(
706 pool.get_amount_out(1, 0, u("1000000000000000000000")),
707 Some(u("21806963109202")),
708 "1000 CRV -> WETH",
709 );
710 assert_eq!(
711 pool.get_amount_out(1, 0, u("10000000000000000000000")),
712 Some(u("218068351371449")),
713 "10000 CRV -> WETH",
714 );
715 }
716
717 #[test]
720 fn twocrypto_v011_is_stable_not_ng() {
721 let u = |s: &str| s.parse::<U256>().unwrap();
722 let base = |variant: CurveVariant, gamma: Option<U256>| {
723 build_pool(&RawPoolState {
724 variant,
725 balances: vec![u("250289528581622891700521"), u("179139571297")],
726 token_decimals: vec![18, 6],
727 amp: u("350000"),
728 mid_fee: Some(u("1000000")),
729 out_fee: Some(u("20000000")),
730 fee_gamma: Some(u("63100000000000000")),
731 d: Some(u("500000118136487176847089")),
732 gamma,
733 price_scale: Some(vec![u("1393944381226980604")]),
734 precisions: Some(vec![u("1"), u("1000000000000")]),
735 ..Default::default()
736 })
737 .expect("build_pool")
738 };
739 let dx = u("1000000000000000000"); let stable = base(CurveVariant::TwoCryptoStable, None).get_amount_out(0, 1, dx);
741 let ng =
742 base(CurveVariant::TwoCryptoNG, Some(u("100000000000000"))).get_amount_out(0, 1, dx);
743 eprintln!("on-chain get_dy=717271 stable={stable:?} ng={ng:?}");
744 assert_eq!(stable, Some(u("717271")), "TwoCryptoStable matches on-chain get_dy");
745 assert_ne!(ng, Some(u("717271")), "TwoCryptoNG does NOT match (current misclassification)");
746 }
747
748 #[test]
752 fn tricrypto_ng_wrong_coin2_decimals_breaks_quotes() {
753 let u = |s: &str| s.parse::<U256>().unwrap();
754 let make = |decimals: Vec<u8>| {
755 build_pool(&RawPoolState {
756 variant: CurveVariant::TriCryptoNG,
757 balances: vec![u("2466241139205"), u("4200057336"), u("1595469030050811720465")],
758 token_decimals: decimals,
759 amp: u("1707629"),
760 mid_fee: Some(u("3000000")),
761 out_fee: Some(u("30000000")),
762 fee_gamma: Some(u("500000000000000")),
763 d: Some(u("7457948167729606869978625")),
764 gamma: Some(u("11809167828997")),
765 price_scale: Some(vec![u("59372627314351316239076"), u("1565715369034455123313")]),
766 ..Default::default()
767 })
768 .expect("build_pool")
769 };
770 let dx_usdc = U256::from(1_000_000_000u64); let dx_wbtc = U256::from(13_656_795u64); for dec2 in [18u8, 0, 6] {
773 let p = make(vec![6, 8, dec2]);
774 eprintln!(
775 "decimals=[6,8,{dec2}] USDC->WETH(0,2)={:?} WBTC->USDC(1,0)={:?}",
776 p.get_amount_out(0, 2, dx_usdc),
777 p.get_amount_out(1, 0, dx_wbtc),
778 );
779 }
780 let correct_out = make(vec![6, 8, 18])
783 .get_amount_out(0, 2, dx_usdc)
784 .unwrap();
785 let wrong_out = make(vec![6, 8, 0])
786 .get_amount_out(0, 2, dx_usdc)
787 .unwrap();
788 assert!(correct_out > U256::from(10).pow(U256::from(17)), "correct ~0.6 ETH");
789 assert!(wrong_out > correct_out * U256::from(100u64), "wrong decimals corrupt the quote");
790 }
791
792 #[test]
793 #[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
794 fn differential_3pool_stableswap_v1() {
795 assert_stable_matches_onchain(
797 "0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7",
798 CurveVariant::StableSwapV1,
799 3,
800 (0, 1, U256::from(1_000_000_000_000_000_000u128)),
801 (21_500_000, 1_736_000_000),
802 );
803 }
804
805 #[test]
806 #[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
807 fn differential_stableswap_ng_plain() {
808 assert_stable_matches_onchain(
810 "0xf55b0f6f2da5ffddb104b58a60f2862745960442",
811 CurveVariant::StableSwapNG,
812 2,
813 (0, 1, U256::from(1_000_000_000_000_000_000u128)),
814 (21_500_000, 1_736_000_000),
815 );
816 }
817}