use std::{collections::HashMap, fmt::Debug};
use alloy::{
core::sol,
primitives::{Address as AlloyAddress, Keccak256, U256},
sol_types::{SolCall, SolValue},
};
use revm::{
state::{AccountInfo, Bytecode},
DatabaseRef,
};
use tycho_common::{simulation::errors::SimulationError, Bytes};
use crate::evm::{
engine_db::engine_db_interface::EngineDatabaseInterface,
protocol::{
curve::{
adapter::{build_pool, detect_eth_variant, CurveVariant, ProbingResults, RawPoolState},
math::Pool,
},
vm::utils::get_code_for_contract,
},
simulation::{SimulationEngine, SimulationParameters},
};
sol! {
#[allow(missing_docs)]
interface ICurve {
function balances(uint256 i) external view returns (uint256);
function A() external view returns (uint256);
function fee() external view returns (uint256);
function initial_A() external view returns (uint256);
function future_A() external view returns (uint256);
function offpeg_fee_multiplier() external view returns (uint256);
function gamma() external view returns (uint256);
function D() external view returns (uint256);
function mid_fee() external view returns (uint256);
function out_fee() external view returns (uint256);
function fee_gamma() external view returns (uint256);
function price_scale() external view returns (uint256);
function MATH() external view returns (address);
function base_pool() external view returns (address);
function version() external view returns (string);
}
#[allow(missing_docs)]
interface ICurveOld {
function balances(int128 i) external view returns (uint256);
}
#[allow(missing_docs)]
interface ICurveTri {
function price_scale(uint256 i) external view returns (uint256);
function precisions() external view returns (uint256[3]);
}
#[allow(missing_docs)]
interface ICurveTwo {
function precisions() external view returns (uint256[2]);
}
#[allow(missing_docs)]
interface IBasePool {
function get_virtual_price() external view returns (uint256);
}
}
const A_PRECISION: u64 = 100;
const STORED_RATES_SELECTOR: [u8; 4] = [0xfd, 0x06, 0x84, 0xb1];
pub fn decode_from_vm<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
variant: CurveVariant,
token_decimals: &[u8],
) -> Result<Pool, SimulationError>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
let n_coins = token_decimals.len();
let state = match variant {
CurveVariant::StableSwapV0 => RawPoolState {
variant,
balances: read_balances_int128(engine, pool, n_coins)?,
token_decimals: token_decimals.to_vec(),
amp: call(engine, pool, ICurve::ACall {})?,
fee: Some(call(engine, pool, ICurve::feeCall {})?),
..Default::default()
},
CurveVariant::StableSwapV1 => RawPoolState {
variant,
balances: read_balances(engine, pool, n_coins)?,
token_decimals: token_decimals.to_vec(),
amp: call(engine, pool, ICurve::ACall {})?,
fee: Some(call(engine, pool, ICurve::feeCall {})?),
..Default::default()
},
CurveVariant::StableSwapV2 | CurveVariant::StableSwapSTETH => RawPoolState {
variant,
balances: read_balances(engine, pool, n_coins)?,
token_decimals: token_decimals.to_vec(),
amp: read_ramped_amp(engine, pool)?,
fee: Some(call(engine, pool, ICurve::feeCall {})?),
..Default::default()
},
CurveVariant::StableSwapALend => RawPoolState {
variant,
balances: read_balances(engine, pool, n_coins)?,
token_decimals: token_decimals.to_vec(),
amp: read_ramped_amp(engine, pool)?,
fee: Some(call(engine, pool, ICurve::feeCall {})?),
offpeg_fee_multiplier: Some(call(engine, pool, ICurve::offpeg_fee_multiplierCall {})?),
..Default::default()
},
CurveVariant::StableSwapNG => RawPoolState {
variant,
balances: read_balances(engine, pool, n_coins)?,
token_decimals: token_decimals.to_vec(),
amp: read_ramped_amp(engine, pool)?,
fee: Some(call(engine, pool, ICurve::feeCall {})?),
offpeg_fee_multiplier: call_opt(engine, pool, ICurve::offpeg_fee_multiplierCall {}),
dynamic_rates: read_stored_rates(engine, pool, n_coins),
..Default::default()
},
CurveVariant::StableSwapMeta => {
let mut dynamic_rates = vec![None; n_coins];
if let Some(last) = dynamic_rates.last_mut() {
*last = Some(read_base_virtual_price(engine, pool)?);
}
RawPoolState {
variant,
balances: read_balances(engine, pool, n_coins)?,
token_decimals: token_decimals.to_vec(),
amp: read_ramped_amp(engine, pool)?,
fee: Some(call(engine, pool, ICurve::feeCall {})?),
dynamic_rates: Some(dynamic_rates),
..Default::default()
}
}
CurveVariant::TwoCryptoV1 | CurveVariant::TwoCryptoNG | CurveVariant::TwoCryptoStable => {
read_twocrypto(engine, pool, variant, token_decimals)?
}
CurveVariant::TriCryptoV1 | CurveVariant::TriCryptoNG => {
read_tricrypto(engine, pool, variant, token_decimals)?
}
};
build_pool(&state)
.map_err(|e| SimulationError::FatalError(format!("curve build_pool failed: {e}")))
}
fn read_twocrypto<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
variant: CurveVariant,
token_decimals: &[u8],
) -> Result<RawPoolState, SimulationError>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
let balances = read_balances(engine, pool, 2)?;
let price_scale = call(engine, pool, ICurve::price_scaleCall {})?;
let precisions = call_opt(engine, pool, ICurveTwo::precisionsCall {}).map(|p| p.to_vec());
let gamma = if variant == CurveVariant::TwoCryptoStable {
None
} else {
Some(call(engine, pool, ICurve::gammaCall {})?)
};
let eth_variant =
if variant == CurveVariant::TwoCryptoV1 { Some(detect_eth_variant(*pool)) } else { None };
Ok(RawPoolState {
variant,
balances,
token_decimals: token_decimals.to_vec(),
amp: call(engine, pool, ICurve::ACall {})?,
mid_fee: Some(call(engine, pool, ICurve::mid_feeCall {})?),
out_fee: Some(call(engine, pool, ICurve::out_feeCall {})?),
fee_gamma: Some(call(engine, pool, ICurve::fee_gammaCall {})?),
d: Some(call(engine, pool, ICurve::DCall {})?),
gamma,
price_scale: Some(vec![price_scale]),
precisions,
eth_variant,
..Default::default()
})
}
fn read_tricrypto<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
variant: CurveVariant,
token_decimals: &[u8],
) -> Result<RawPoolState, SimulationError>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
let balances = read_balances(engine, pool, 3)?;
let ps0 = call(engine, pool, ICurveTri::price_scaleCall { i: U256::from(0) })?;
let ps1 = call(engine, pool, ICurveTri::price_scaleCall { i: U256::from(1) })?;
let precisions = call_opt(engine, pool, ICurveTri::precisionsCall {}).map(|p| p.to_vec());
Ok(RawPoolState {
variant,
balances,
token_decimals: token_decimals.to_vec(),
amp: call(engine, pool, ICurve::ACall {})?,
mid_fee: Some(call(engine, pool, ICurve::mid_feeCall {})?),
out_fee: Some(call(engine, pool, ICurve::out_feeCall {})?),
fee_gamma: Some(call(engine, pool, ICurve::fee_gammaCall {})?),
d: Some(call(engine, pool, ICurve::DCall {})?),
gamma: Some(call(engine, pool, ICurve::gammaCall {})?),
price_scale: Some(vec![ps0, ps1]),
precisions,
..Default::default()
})
}
fn read_ramped_amp<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
) -> Result<U256, SimulationError>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
let initial_a = call_opt(engine, pool, ICurve::initial_ACall {});
let future_a = call_opt(engine, pool, ICurve::future_ACall {});
match (initial_a, future_a) {
(Some(ia), Some(fa)) if ia == fa => Ok(ia),
_ => Ok(call(engine, pool, ICurve::ACall {})? * U256::from(A_PRECISION)),
}
}
fn read_balances<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
n_coins: usize,
) -> Result<Vec<U256>, SimulationError>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
let mut balances = Vec::with_capacity(n_coins);
for i in 0..n_coins {
balances.push(call(engine, pool, ICurve::balancesCall { i: U256::from(i) })?);
}
Ok(balances)
}
fn read_balances_int128<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
n_coins: usize,
) -> Result<Vec<U256>, SimulationError>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
let mut balances = Vec::with_capacity(n_coins);
for i in 0..n_coins {
balances.push(call(engine, pool, ICurveOld::balancesCall { i: i as i128 })?);
}
Ok(balances)
}
fn read_base_virtual_price<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
) -> Result<U256, SimulationError>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
let base_pool = call(engine, pool, ICurve::base_poolCall {})?;
call(engine, &base_pool, IBasePool::get_virtual_priceCall {})
}
fn read_stored_rates<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
n_coins: usize,
) -> Option<Vec<Option<U256>>>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
let res = engine
.simulate(¶ms(*pool, STORED_RATES_SELECTOR.to_vec()))
.ok()?;
let out = res.result.as_ref();
if out.len() < n_coins * 32 {
return None;
}
let first_word = U256::from_be_slice(&out[..32]);
let data_offset =
if first_word <= U256::from(256u64) && out.len() >= (n_coins + 2) * 32 { 64 } else { 0 };
let rates = (0..n_coins)
.map(|i| {
let start = data_offset + i * 32;
Some(U256::from_be_slice(&out[start..start + 32]))
})
.collect();
Some(rates)
}
pub fn read_math_address<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
) -> Option<AlloyAddress>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
call_opt(engine, pool, ICurve::MATHCall {})
}
pub async fn load_math_contract<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
) -> Result<(), SimulationError>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
let Some(math) = read_math_address(engine, pool) else {
return Ok(());
};
let code = get_code_for_contract(&math.to_string(), None)
.await
.map_err(|e| {
SimulationError::RecoverableError(format!(
"curve: failed to fetch MATH() code for {math}: {e}"
))
})?;
engine
.state
.init_account(
math,
AccountInfo {
balance: U256::ZERO,
nonce: 0,
code_hash: code.hash_slow(),
code: Some(code),
},
None,
false,
)
.map_err(|e| {
SimulationError::FatalError(format!(
"curve: failed to load MATH() code for {math}: {e:?}"
))
})?;
Ok(())
}
pub fn read_version<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
address: &AlloyAddress,
) -> Option<String>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
call_opt(engine, address, ICurve::versionCall {})
}
pub fn probe<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
n_coins: usize,
) -> ProbingResults
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
ProbingResults {
has_gamma: call_opt(engine, pool, ICurve::gammaCall {}).is_some(),
n_coins,
has_math: read_math_address(engine, pool).is_some(),
math_version: read_math_address(engine, pool).and_then(|math| read_version(engine, &math)),
has_offpeg_fee_multiplier: call_opt(engine, pool, ICurve::offpeg_fee_multiplierCall {})
.is_some(),
has_stored_rates: read_stored_rates(engine, pool, n_coins).is_some(),
has_version: call_opt(engine, pool, ICurve::versionCall {}).is_some(),
has_base_pool: call_opt(engine, pool, ICurve::base_poolCall {}).is_some(),
has_int128_balances: call_opt(engine, pool, ICurveOld::balancesCall { i: 0 }).is_some(),
pool_address: *pool,
}
}
pub async fn load_stateless_contracts<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
attributes: &HashMap<String, Bytes>,
) -> Result<(), SimulationError>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
let mut index = 0;
while let Some(encoded) = attributes.get(&format!("stateless_contract_addr_{index}")) {
let address = String::from_utf8(encoded.to_vec()).map_err(|e| {
SimulationError::FatalError(format!("curve stateless address not UTF-8: {e}"))
})?;
let inline_code = attributes
.get(&format!("stateless_contract_code_{index}"))
.map(|value| value.to_vec());
index += 1;
let (account, code) = match inline_code {
Some(bytecode) => (address, Bytecode::new_raw(bytecode.into())),
None => {
let resolved = if address.starts_with("call") {
resolve_call_address(engine, &address)?
} else {
address
};
let code = get_code_for_contract(&resolved, None).await?;
(resolved, code)
}
};
let account: AlloyAddress = account.parse().map_err(|_| {
SimulationError::FatalError(format!("curve stateless: invalid address {account}"))
})?;
engine
.state
.init_account(
account,
AccountInfo {
balance: U256::ZERO,
nonce: 0,
code_hash: code.hash_slow(),
code: Some(code),
},
None,
false,
)
.map_err(|e| {
SimulationError::FatalError(format!("curve stateless init_account failed: {e:?}"))
})?;
}
Ok(())
}
fn resolve_call_address<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
directive: &str,
) -> Result<String, SimulationError>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
let method = directive
.split(':')
.next_back()
.ok_or_else(|| {
SimulationError::FatalError(format!(
"curve stateless: malformed call directive {directive}"
))
})?;
let to: AlloyAddress = directive
.split(':')
.nth(1)
.ok_or_else(|| {
SimulationError::FatalError(format!(
"curve stateless: missing target in call directive {directive}"
))
})?
.parse()
.map_err(|_| {
SimulationError::FatalError(format!(
"curve stateless: invalid target in call directive {directive}"
))
})?;
let mut hasher = Keccak256::new();
hasher.update(method.as_bytes());
let selector = hasher.finalize()[..4].to_vec();
let res = engine
.simulate(¶ms(to, selector))
.map_err(|e| SimulationError::FatalError(format!("curve stateless call failed: {e}")))?;
let address = AlloyAddress::abi_decode(res.result.as_ref()).map_err(|e| {
SimulationError::FatalError(format!("curve stateless call decode failed: {e}"))
})?;
Ok(address.to_string())
}
fn params(to: AlloyAddress, data: Vec<u8>) -> SimulationParameters {
SimulationParameters {
caller: AlloyAddress::ZERO,
to,
data,
value: U256::ZERO,
overrides: None,
gas_limit: None,
transient_storage: None,
block_overrides: None,
}
}
fn call<D, C, R>(
engine: &SimulationEngine<D>,
to: &AlloyAddress,
sol_call: C,
) -> Result<R, SimulationError>
where
D: EngineDatabaseInterface + Clone + Debug,
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
C: SolCall<Return = R>,
{
let res = engine
.simulate(¶ms(*to, sol_call.abi_encode()))
.map_err(|e| SimulationError::RecoverableError(format!("curve getter call failed: {e}")))?;
C::abi_decode_returns(res.result.as_ref())
.map_err(|e| SimulationError::FatalError(format!("curve getter decode failed: {e}")))
}
fn call_opt<D, C, R>(engine: &SimulationEngine<D>, to: &AlloyAddress, sol_call: C) -> Option<R>
where
D: EngineDatabaseInterface + Clone + Debug,
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
C: SolCall<Return = R>,
{
call::<D, C, R>(engine, to, sol_call).ok()
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use alloy::sol;
use tycho_client::feed::BlockHeader;
use super::*;
use crate::evm::{
engine_db::{
simulation_db::SimulationDB,
utils::{get_client, get_runtime},
},
simulation::SimulationEngine,
};
sol! {
function get_dy_stable(int128 i, int128 j, uint256 dx) external view returns (uint256);
function coins(uint256 i) external view returns (address);
function decimals() external view returns (uint8);
}
const ETH_PLACEHOLDER: AlloyAddress =
alloy::primitives::address!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee");
fn read_decimals<D: EngineDatabaseInterface + Clone + Debug>(
engine: &SimulationEngine<D>,
pool: &AlloyAddress,
n: usize,
) -> Vec<u8>
where
<D as DatabaseRef>::Error: Debug,
<D as EngineDatabaseInterface>::Error: Debug,
{
(0..n)
.map(|i| {
let coin: AlloyAddress =
call(engine, pool, coinsCall { i: U256::from(i) }).unwrap();
if coin == ETH_PLACEHOLDER {
18
} else {
call(engine, &coin, decimalsCall {}).unwrap()
}
})
.collect()
}
fn assert_stable_matches_onchain(
pool: &str,
variant: CurveVariant,
n_coins: usize,
swap: (usize, usize, U256),
block: (u64, u64),
) {
let (i, j, dx) = swap;
let (block_number, timestamp) = block;
let header = BlockHeader { number: block_number, timestamp, ..Default::default() };
let mut db = SimulationDB::new(get_client(None).unwrap(), get_runtime().unwrap(), None);
db.set_block(Some(header));
let engine = SimulationEngine::new(db, false);
let pool = AlloyAddress::from_str(pool).unwrap();
let decimals = read_decimals(&engine, &pool, n_coins);
let decoded = decode_from_vm(&engine, &pool, variant, &decimals).expect("decode failed");
let ours = decoded
.get_amount_out(i, j, dx)
.expect("get_amount_out returned None");
let onchain: U256 =
call(&engine, &pool, get_dy_stableCall { i: i as i128, j: j as i128, dx })
.expect("on-chain get_dy failed");
assert_eq!(ours, onchain, "curve quote diverged from on-chain get_dy");
}
#[test]
fn tricrypto_ng_build_pool_matches_onchain_get_dy() {
let u = |s: &str| s.parse::<U256>().unwrap();
let state = RawPoolState {
variant: CurveVariant::TriCryptoNG,
balances: vec![u("2466241139205"), u("4200057336"), u("1595469030050811720465")],
token_decimals: vec![6, 8, 18],
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()
};
let pool = build_pool(&state).expect("build_pool failed");
let dx = U256::from(1_000_000_000u64); assert_eq!(pool.get_amount_out(0, 1, dx), Some(u("1690920")), "USDC->WBTC");
assert_eq!(pool.get_amount_out(0, 2, dx), Some(u("641654961086650131")), "USDC->WETH");
}
#[test]
fn twocrypto_v1_eth_variant_build_pool_matches_onchain_get_dy() {
let u = |s: &str| s.parse::<U256>().unwrap();
let make = |eth_variant: bool| {
build_pool(&RawPoolState {
variant: CurveVariant::TwoCryptoV1,
balances: vec![u("33389428640940997105"), u("1538654846140514380725767708")],
token_decimals: vec![18, 18], amp: u("400000"),
gamma: Some(u("145000000000000")),
d: Some(u("3338917956508624293928")),
price_scale: Some(vec![u("52805053500476")]),
mid_fee: Some(U256::from(26_000_000u64)),
out_fee: Some(U256::from(45_000_000u64)),
fee_gamma: Some(u("230000000000000")),
eth_variant: Some(eth_variant),
..Default::default()
})
.expect("build_pool")
};
let pool = make(true);
assert_eq!(
pool.get_amount_out(0, 1, u("1000000000000000000")),
Some(u("44131555012248406155621024")),
"1 WETH -> CRV",
);
assert_eq!(
pool.get_amount_out(0, 1, u("500000000000000000")),
Some(u("22389351263618692591189738")),
"0.5 WETH -> CRV",
);
assert_eq!(
pool.get_amount_out(1, 0, u("1000000000000000000000")),
Some(u("21806963109202")),
"1000 CRV -> WETH",
);
assert_eq!(
pool.get_amount_out(1, 0, u("10000000000000000000000")),
Some(u("218068351371449")),
"10000 CRV -> WETH",
);
}
#[test]
fn twocrypto_v011_is_stable_not_ng() {
let u = |s: &str| s.parse::<U256>().unwrap();
let base = |variant: CurveVariant, gamma: Option<U256>| {
build_pool(&RawPoolState {
variant,
balances: vec![u("250289528581622891700521"), u("179139571297")],
token_decimals: vec![18, 6],
amp: u("350000"),
mid_fee: Some(u("1000000")),
out_fee: Some(u("20000000")),
fee_gamma: Some(u("63100000000000000")),
d: Some(u("500000118136487176847089")),
gamma,
price_scale: Some(vec![u("1393944381226980604")]),
precisions: Some(vec![u("1"), u("1000000000000")]),
..Default::default()
})
.expect("build_pool")
};
let dx = u("1000000000000000000"); let stable = base(CurveVariant::TwoCryptoStable, None).get_amount_out(0, 1, dx);
let ng =
base(CurveVariant::TwoCryptoNG, Some(u("100000000000000"))).get_amount_out(0, 1, dx);
eprintln!("on-chain get_dy=717271 stable={stable:?} ng={ng:?}");
assert_eq!(stable, Some(u("717271")), "TwoCryptoStable matches on-chain get_dy");
assert_ne!(ng, Some(u("717271")), "TwoCryptoNG does NOT match (current misclassification)");
}
#[test]
fn tricrypto_ng_wrong_coin2_decimals_breaks_quotes() {
let u = |s: &str| s.parse::<U256>().unwrap();
let make = |decimals: Vec<u8>| {
build_pool(&RawPoolState {
variant: CurveVariant::TriCryptoNG,
balances: vec![u("2466241139205"), u("4200057336"), u("1595469030050811720465")],
token_decimals: decimals,
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()
})
.expect("build_pool")
};
let dx_usdc = U256::from(1_000_000_000u64); let dx_wbtc = U256::from(13_656_795u64); for dec2 in [18u8, 0, 6] {
let p = make(vec![6, 8, dec2]);
eprintln!(
"decimals=[6,8,{dec2}] USDC->WETH(0,2)={:?} WBTC->USDC(1,0)={:?}",
p.get_amount_out(0, 2, dx_usdc),
p.get_amount_out(1, 0, dx_wbtc),
);
}
let correct_out = make(vec![6, 8, 18])
.get_amount_out(0, 2, dx_usdc)
.unwrap();
let wrong_out = make(vec![6, 8, 0])
.get_amount_out(0, 2, dx_usdc)
.unwrap();
assert!(correct_out > U256::from(10).pow(U256::from(17)), "correct ~0.6 ETH");
assert!(wrong_out > correct_out * U256::from(100u64), "wrong decimals corrupt the quote");
}
#[test]
#[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
fn differential_3pool_stableswap_v1() {
assert_stable_matches_onchain(
"0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7",
CurveVariant::StableSwapV1,
3,
(0, 1, U256::from(1_000_000_000_000_000_000u128)),
(21_500_000, 1_736_000_000),
);
}
#[test]
#[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
fn differential_stableswap_ng_plain() {
assert_stable_matches_onchain(
"0xf55b0f6f2da5ffddb104b58a60f2862745960442",
CurveVariant::StableSwapNG,
2,
(0, 1, U256::from(1_000_000_000_000_000_000u128)),
(21_500_000, 1_736_000_000),
);
}
}