use alloy_primitives::U256;
use crate::evm::protocol::curve::{adapter::CurveVariant, math::Pool};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BuildError {
MissingField { variant: CurveVariant, field: &'static str },
WrongCoinCount { variant: CurveVariant, expected: usize, actual: usize },
DecimalsMismatch { balances_len: usize, decimals_len: usize },
DecimalsTooLarge { index: usize, decimals: u8, max: u8 },
DynamicRatesMismatch { balances_len: usize, rates_len: usize },
PriceScaleWrongLen { expected: usize, actual: usize },
MetaMissingVirtualPrice,
}
impl std::fmt::Display for BuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingField { variant, field } => {
write!(f, "{variant}: missing required field `{field}`")
}
Self::WrongCoinCount { variant, expected, actual } => {
write!(f, "{variant}: expected {expected} coins, got {actual} balances")
}
Self::DecimalsMismatch { balances_len, decimals_len } => write!(
f,
"token_decimals length ({decimals_len}) != balances length ({balances_len})"
),
Self::DecimalsTooLarge { index, decimals, max } => {
write!(f, "token_decimals[{index}] = {decimals} exceeds maximum {max}")
}
Self::DynamicRatesMismatch { balances_len, rates_len } => {
write!(f, "dynamic_rates length ({rates_len}) != balances length ({balances_len})")
}
Self::PriceScaleWrongLen { expected, actual } => {
write!(f, "price_scale: expected {expected} elements, got {actual}")
}
Self::MetaMissingVirtualPrice => write!(
f,
"StableSwapMeta: dynamic_rates must provide an explicit rate for the last coin \
(base pool LP token virtual_price). Without it, swap calculations are incorrect."
),
}
}
}
impl std::error::Error for BuildError {}
#[derive(Debug, Clone)]
pub struct RawPoolState {
pub variant: CurveVariant,
pub balances: Vec<U256>,
pub token_decimals: Vec<u8>,
pub amp: U256,
pub fee: Option<U256>,
pub mid_fee: Option<U256>,
pub out_fee: Option<U256>,
pub fee_gamma: Option<U256>,
pub offpeg_fee_multiplier: Option<U256>,
pub price_scale: Option<Vec<U256>>,
pub d: Option<U256>,
pub gamma: Option<U256>,
pub dynamic_rates: Option<Vec<Option<U256>>>,
pub precisions: Option<Vec<U256>>,
pub eth_variant: Option<bool>,
}
impl Default for RawPoolState {
fn default() -> Self {
Self {
variant: CurveVariant::StableSwapV2,
balances: Vec::new(),
token_decimals: Vec::new(),
amp: U256::ZERO,
fee: None,
mid_fee: None,
out_fee: None,
fee_gamma: None,
offpeg_fee_multiplier: None,
price_scale: None,
d: None,
gamma: None,
dynamic_rates: None,
precisions: None,
eth_variant: None,
}
}
}
pub fn interpolate_a(
initial_a: U256,
future_a: U256,
initial_a_time: u64,
future_a_time: u64,
block_timestamp: u64,
) -> U256 {
if block_timestamp >= future_a_time {
return future_a;
}
let elapsed = U256::from(block_timestamp - initial_a_time);
let duration = U256::from(future_a_time - initial_a_time);
if future_a > initial_a {
initial_a + (future_a - initial_a) * elapsed / duration
} else {
initial_a - (initial_a - future_a) * elapsed / duration
}
}
fn compute_stableswap_rates(
token_decimals: &[u8],
dynamic_rates: &Option<Vec<Option<U256>>>,
) -> Vec<U256> {
token_decimals
.iter()
.enumerate()
.map(|(i, &decimals)| {
if let Some(ref rates) = dynamic_rates {
if let Some(Some(rate)) = rates.get(i) {
return *rate;
}
}
U256::from(10u64).pow(U256::from(36 - decimals as u32))
})
.collect()
}
fn compute_crypto_precisions(token_decimals: &[u8]) -> Vec<U256> {
token_decimals
.iter()
.map(|&decimals| U256::from(10u64).pow(U256::from(18 - decimals as u32)))
.collect()
}
macro_rules! require {
($state:expr, $field:ident) => {
$state
.$field
.ok_or(BuildError::MissingField {
variant: $state.variant,
field: stringify!($field),
})?
};
}
pub fn build_pool(state: &RawPoolState) -> Result<Pool, BuildError> {
if state.balances.len() != state.token_decimals.len() {
return Err(BuildError::DecimalsMismatch {
balances_len: state.balances.len(),
decimals_len: state.token_decimals.len(),
});
}
if let Some(ref dr) = state.dynamic_rates {
if dr.len() != state.balances.len() {
return Err(BuildError::DynamicRatesMismatch {
balances_len: state.balances.len(),
rates_len: dr.len(),
});
}
}
match state.variant {
CurveVariant::StableSwapV0 |
CurveVariant::StableSwapV1 |
CurveVariant::StableSwapV2 |
CurveVariant::StableSwapSTETH |
CurveVariant::StableSwapMeta => build_stableswap_plain(state),
CurveVariant::StableSwapNG => build_stableswap_ng(state),
CurveVariant::StableSwapALend => build_stableswap_alend(state),
CurveVariant::TwoCryptoV1 | CurveVariant::TwoCryptoNG | CurveVariant::TwoCryptoStable => {
build_twocrypto(state)
}
CurveVariant::TriCryptoV1 | CurveVariant::TriCryptoNG => build_tricrypto(state),
}
}
fn build_stableswap_plain(state: &RawPoolState) -> Result<Pool, BuildError> {
let fee = require!(state, fee);
for (i, &d) in state.token_decimals.iter().enumerate() {
if d > 36 {
return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 36 });
}
}
if state.variant == CurveVariant::StableSwapMeta {
let n = state.balances.len();
let has_vp = state
.dynamic_rates
.as_ref()
.and_then(|dr| dr.get(n - 1))
.map(|r| r.is_some())
.unwrap_or(false);
if !has_vp {
return Err(BuildError::MetaMissingVirtualPrice);
}
}
let rates = compute_stableswap_rates(&state.token_decimals, &state.dynamic_rates);
let balances = state.balances.clone();
let amp = state.amp;
Ok(match state.variant {
CurveVariant::StableSwapV0 => Pool::StableSwapV0 { balances, rates, amp, fee },
CurveVariant::StableSwapV1 => Pool::StableSwapV1 { balances, rates, amp, fee },
CurveVariant::StableSwapV2 => Pool::StableSwapV2 { balances, rates, amp, fee },
CurveVariant::StableSwapSTETH => Pool::StableSwapSTETH { balances, rates, amp, fee },
CurveVariant::StableSwapMeta => Pool::StableSwapMeta { balances, rates, amp, fee },
_ => unreachable!(),
})
}
fn build_stableswap_ng(state: &RawPoolState) -> Result<Pool, BuildError> {
let fee = require!(state, fee);
let offpeg = state
.offpeg_fee_multiplier
.unwrap_or(U256::from(10_000_000_000u64));
for (i, &d) in state.token_decimals.iter().enumerate() {
if d > 36 {
return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 36 });
}
}
let rates = compute_stableswap_rates(&state.token_decimals, &state.dynamic_rates);
Ok(Pool::StableSwapNG {
balances: state.balances.clone(),
rates,
amp: state.amp,
fee,
offpeg_fee_multiplier: offpeg,
})
}
fn build_stableswap_alend(state: &RawPoolState) -> Result<Pool, BuildError> {
let fee = require!(state, fee);
let offpeg = require!(state, offpeg_fee_multiplier);
for (i, &d) in state.token_decimals.iter().enumerate() {
if d > 18 {
return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 18 });
}
}
let precision_mul = compute_crypto_precisions(&state.token_decimals);
Ok(Pool::StableSwapALend {
balances: state.balances.clone(),
precision_mul,
amp: state.amp,
fee,
offpeg_fee_multiplier: offpeg,
})
}
fn build_twocrypto(state: &RawPoolState) -> Result<Pool, BuildError> {
if state.balances.len() != 2 {
return Err(BuildError::WrongCoinCount {
variant: state.variant,
expected: 2,
actual: state.balances.len(),
});
}
for (i, &d) in state.token_decimals.iter().enumerate() {
if d > 18 {
return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 18 });
}
}
let mid_fee = require!(state, mid_fee);
let out_fee = require!(state, out_fee);
let fee_gamma = require!(state, fee_gamma);
let d = require!(state, d);
let ann = state.amp;
let price_scale_vec = state
.price_scale
.as_ref()
.ok_or(BuildError::MissingField { variant: state.variant, field: "price_scale" })?;
if price_scale_vec.len() != 1 {
return Err(BuildError::PriceScaleWrongLen { expected: 1, actual: price_scale_vec.len() });
}
let price_scale = price_scale_vec[0];
let default_precs = compute_crypto_precisions(&state.token_decimals);
let precisions = state
.precisions
.as_deref()
.unwrap_or(&default_precs);
let balances: [U256; 2] = [state.balances[0], state.balances[1]];
let prec_arr: [U256; 2] = [precisions[0], precisions[1]];
match state.variant {
CurveVariant::TwoCryptoV1 => {
let gamma = require!(state, gamma);
let eth_variant = require!(state, eth_variant);
Ok(Pool::TwoCryptoV1 {
balances,
precisions: prec_arr,
price_scale,
d,
ann,
gamma,
mid_fee,
out_fee,
fee_gamma,
eth_variant,
})
}
CurveVariant::TwoCryptoNG => {
let gamma = require!(state, gamma);
Ok(Pool::TwoCryptoNG {
balances,
precisions: prec_arr,
price_scale,
d,
ann,
gamma,
mid_fee,
out_fee,
fee_gamma,
})
}
CurveVariant::TwoCryptoStable => Ok(Pool::TwoCryptoStable {
balances,
precisions: prec_arr,
price_scale,
d,
ann,
mid_fee,
out_fee,
fee_gamma,
}),
_ => unreachable!("build_twocrypto called for non-twocrypto variant"),
}
}
fn build_tricrypto(state: &RawPoolState) -> Result<Pool, BuildError> {
if state.balances.len() != 3 {
return Err(BuildError::WrongCoinCount {
variant: state.variant,
expected: 3,
actual: state.balances.len(),
});
}
for (i, &d) in state.token_decimals.iter().enumerate() {
if d > 18 {
return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 18 });
}
}
let mid_fee = require!(state, mid_fee);
let out_fee = require!(state, out_fee);
let fee_gamma = require!(state, fee_gamma);
let d = require!(state, d);
let gamma = require!(state, gamma);
let ann = state.amp;
let price_scale_vec = state
.price_scale
.as_ref()
.ok_or(BuildError::MissingField { variant: state.variant, field: "price_scale" })?;
if price_scale_vec.len() != 2 {
return Err(BuildError::PriceScaleWrongLen { expected: 2, actual: price_scale_vec.len() });
}
let price_scale: [U256; 2] = [price_scale_vec[0], price_scale_vec[1]];
let default_precs = compute_crypto_precisions(&state.token_decimals);
let precisions = state
.precisions
.as_deref()
.unwrap_or(&default_precs);
let balances: [U256; 3] = [state.balances[0], state.balances[1], state.balances[2]];
let prec_arr: [U256; 3] = [precisions[0], precisions[1], precisions[2]];
match state.variant {
CurveVariant::TriCryptoV1 => Ok(Pool::TriCryptoV1 {
balances,
precisions: prec_arr,
price_scale,
d,
ann,
gamma,
mid_fee,
out_fee,
fee_gamma,
}),
CurveVariant::TriCryptoNG => Ok(Pool::TriCryptoNG {
balances,
precisions: prec_arr,
price_scale,
d,
ann,
gamma,
mid_fee,
out_fee,
fee_gamma,
}),
_ => unreachable!("build_tricrypto called for non-tricrypto variant"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interpolate_a_no_ramp() {
let a = U256::from(40_000u64);
let result = interpolate_a(a, a, 1000, 2000, 1500);
assert_eq!(result, a);
}
#[test]
fn interpolate_a_ramp_complete() {
let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 3000);
assert_eq!(result, U256::from(40_000u64));
}
#[test]
fn interpolate_a_ramp_exactly_at_end() {
let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 2000);
assert_eq!(result, U256::from(40_000u64));
}
#[test]
fn interpolate_a_ramp_up_midpoint() {
let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 1500);
assert_eq!(result, U256::from(30_000u64));
}
#[test]
fn interpolate_a_ramp_down_midpoint() {
let result = interpolate_a(U256::from(40_000u64), U256::from(20_000u64), 1000, 2000, 1500);
assert_eq!(result, U256::from(30_000u64));
}
#[test]
fn interpolate_a_ramp_up_quarter() {
let result = interpolate_a(U256::from(10_000u64), U256::from(50_000u64), 0, 1000, 250);
assert_eq!(result, U256::from(20_000u64));
}
#[test]
fn interpolate_a_ramp_at_start() {
let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 1000);
assert_eq!(result, U256::from(20_000u64));
}
#[test]
fn interpolate_a_integer_division_truncation() {
let result = interpolate_a(U256::from(10_000u64), U256::from(10_003u64), 0, 1000, 1);
assert_eq!(result, U256::from(10_000u64));
}
#[test]
fn build_stableswap_v0_basic() {
let state = RawPoolState {
variant: CurveVariant::StableSwapV0,
balances: vec![
U256::from(1_000_000_000_000_000_000_000u128), U256::from(1_000_000_000u128), U256::from(1_000_000_000u128), U256::from(1_000_000_000_000_000_000_000u128), ],
token_decimals: vec![18, 6, 6, 18],
amp: U256::from(200u64), fee: Some(U256::from(4_000_000u64)),
..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool.get_amount_out(0, 1, U256::from(1_000_000_000_000_000_000u128));
assert!(dy.is_some());
}
#[test]
fn build_stableswap_v2_rates_18_6() {
let state = RawPoolState {
variant: CurveVariant::StableSwapV2,
balances: vec![
U256::from(1_000_000_000_000_000_000_000u128),
U256::from(1_000_000_000u128),
],
token_decimals: vec![18, 6],
amp: U256::from(40_000u64), fee: Some(U256::from(4_000_000u64)),
..Default::default()
};
let pool = build_pool(&state).unwrap();
match &pool {
Pool::StableSwapV2 { rates, .. } => {
assert_eq!(rates[0], U256::from(10u64).pow(U256::from(18u64)));
assert_eq!(rates[1], U256::from(10u64).pow(U256::from(30u64)));
}
_ => panic!("wrong variant"),
}
}
#[test]
fn build_stableswap_ng_with_dynamic_rates() {
let oracle_rate = U256::from(1_050_000_000_000_000_000u128); let state = RawPoolState {
variant: CurveVariant::StableSwapNG,
balances: vec![
U256::from(1_000_000_000_000_000_000_000u128),
U256::from(1_000_000_000_000_000_000_000u128),
],
token_decimals: vec![18, 18],
amp: U256::from(150_000u64),
fee: Some(U256::from(4_000_000u64)),
offpeg_fee_multiplier: Some(U256::from(20_000_000_000u128)),
dynamic_rates: Some(vec![
None, Some(oracle_rate), ]),
..Default::default()
};
let pool = build_pool(&state).unwrap();
match &pool {
Pool::StableSwapNG { rates, .. } => {
assert_eq!(rates[0], U256::from(10u64).pow(U256::from(18u64)));
assert_eq!(rates[1], oracle_rate);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn build_stableswap_alend_precision_mul() {
let state = RawPoolState {
variant: CurveVariant::StableSwapALend,
balances: vec![
U256::from(1_000_000_000_000_000_000_000u128),
U256::from(1_000_000_000_000_000_000_000u128),
],
token_decimals: vec![18, 18],
amp: U256::from(10_000u64), fee: Some(U256::from(4_000_000u64)),
offpeg_fee_multiplier: Some(U256::from(20_000_000_000u128)),
..Default::default()
};
let pool = build_pool(&state).unwrap();
match &pool {
Pool::StableSwapALend { precision_mul, .. } => {
assert_eq!(precision_mul[0], U256::from(1u64));
assert_eq!(precision_mul[1], U256::from(1u64));
}
_ => panic!("wrong variant"),
}
}
#[test]
fn build_stableswap_meta_virtual_price_rate() {
let virtual_price = U256::from(1_020_000_000_000_000_000u128); let state = RawPoolState {
variant: CurveVariant::StableSwapMeta,
balances: vec![
U256::from(1_000_000_000u128), U256::from(1_000_000_000_000_000_000_000u128), ],
token_decimals: vec![2, 18],
amp: U256::from(150_000u64),
fee: Some(U256::from(4_000_000u64)),
dynamic_rates: Some(vec![
None, Some(virtual_price), ]),
..Default::default()
};
let pool = build_pool(&state).unwrap();
match &pool {
Pool::StableSwapMeta { rates, .. } => {
assert_eq!(rates[0], U256::from(10u64).pow(U256::from(34u64)));
assert_eq!(rates[1], virtual_price);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn build_twocrypto_ng_basic() {
let state = RawPoolState {
variant: CurveVariant::TwoCryptoNG,
balances: vec![
U256::from(1_000_000_000_000_000_000_000u128),
U256::from(1_000_000_000_000_000_000_000u128),
],
token_decimals: vec![18, 18],
amp: U256::from(540_000u64 * 10_000u64), mid_fee: Some(U256::from(3_000_000u64)),
out_fee: Some(U256::from(30_000_000u64)),
fee_gamma: Some(U256::from(500_000_000_000_000u128)),
d: Some(U256::from(2_000_000_000_000_000_000_000u128)),
gamma: Some(U256::from(10_000_000_000_000u128)),
price_scale: Some(vec![U256::from(1_000_000_000_000_000_000u128)]),
..Default::default()
};
let pool = build_pool(&state).unwrap();
match &pool {
Pool::TwoCryptoNG { precisions, ann, .. } => {
assert_eq!(precisions[0], U256::from(1u64)); assert_eq!(precisions[1], U256::from(1u64));
assert_eq!(*ann, state.amp);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn build_twocrypto_stable_no_gamma() {
let state = RawPoolState {
variant: CurveVariant::TwoCryptoStable,
balances: vec![U256::from(1_000_000_000u128), U256::from(1_000_000_000u128)],
token_decimals: vec![6, 6],
amp: U256::from(540_000u64 * 10_000u64),
mid_fee: Some(U256::from(3_000_000u64)),
out_fee: Some(U256::from(30_000_000u64)),
fee_gamma: Some(U256::from(500_000_000_000_000u128)),
d: Some(U256::from(2_000_000_000u128)),
price_scale: Some(vec![U256::from(1_000_000_000_000_000_000u128)]),
..Default::default()
};
let pool = build_pool(&state).unwrap();
match &pool {
Pool::TwoCryptoStable { precisions, .. } => {
assert_eq!(precisions[0], U256::from(10u64).pow(U256::from(12u64)));
}
_ => panic!("wrong variant"),
}
}
#[test]
fn build_tricrypto_ng_basic() {
let state = RawPoolState {
variant: CurveVariant::TriCryptoNG,
balances: vec![
U256::from(1_000_000_000u128), U256::from(50_000_000u128), U256::from(500_000_000_000_000_000_000u128), ],
token_decimals: vec![6, 8, 18],
amp: U256::from(1_707_629u64 * 10_000u64),
mid_fee: Some(U256::from(3_000_000u64)),
out_fee: Some(U256::from(30_000_000u64)),
fee_gamma: Some(U256::from(500_000_000_000_000u128)),
d: Some(U256::from(3_000_000_000_000_000_000_000u128)),
gamma: Some(U256::from(11_809_167_828_997u128)),
price_scale: Some(vec![
U256::from(60_000_000_000_000_000_000_000u128), U256::from(3_000_000_000_000_000_000_000u128), ]),
..Default::default()
};
let pool = build_pool(&state).unwrap();
match &pool {
Pool::TriCryptoNG { precisions, price_scale, .. } => {
assert_eq!(precisions[0], U256::from(10u64).pow(U256::from(12u64))); assert_eq!(precisions[1], U256::from(10u64).pow(U256::from(10u64))); assert_eq!(precisions[2], U256::from(1u64)); assert_eq!(price_scale.len(), 2);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn build_missing_fee_returns_error() {
let state = RawPoolState {
variant: CurveVariant::StableSwapV2,
balances: vec![U256::from(1u64), U256::from(1u64)],
token_decimals: vec![18, 18],
amp: U256::from(40_000u64),
..Default::default()
};
let err = match build_pool(&state) {
Err(e) => e,
Ok(_) => panic!("expected error"),
};
assert!(matches!(err, BuildError::MissingField { field: "fee", .. }));
}
#[test]
fn build_decimals_mismatch_returns_error() {
let state = RawPoolState {
variant: CurveVariant::StableSwapV2,
balances: vec![U256::from(1u64), U256::from(1u64)],
token_decimals: vec![18], amp: U256::from(40_000u64),
fee: Some(U256::from(4_000_000u64)),
..Default::default()
};
let err = match build_pool(&state) {
Err(e) => e,
Ok(_) => panic!("expected error"),
};
assert!(matches!(err, BuildError::DecimalsMismatch { .. }));
}
#[test]
fn build_twocrypto_wrong_coin_count() {
let state = RawPoolState {
variant: CurveVariant::TwoCryptoNG,
balances: vec![U256::from(1u64), U256::from(1u64), U256::from(1u64)],
token_decimals: vec![18, 18, 18],
amp: U256::from(1u64),
mid_fee: Some(U256::from(1u64)),
out_fee: Some(U256::from(1u64)),
fee_gamma: Some(U256::from(1u64)),
d: Some(U256::from(1u64)),
gamma: Some(U256::from(1u64)),
price_scale: Some(vec![U256::from(1u64)]),
..Default::default()
};
let err = match build_pool(&state) {
Err(e) => e,
Ok(_) => panic!("expected error"),
};
assert!(matches!(err, BuildError::WrongCoinCount { expected: 2, actual: 3, .. }));
}
#[test]
fn build_tricrypto_wrong_price_scale_len() {
let state = RawPoolState {
variant: CurveVariant::TriCryptoNG,
balances: vec![U256::from(1u64), U256::from(1u64), U256::from(1u64)],
token_decimals: vec![6, 8, 18],
amp: U256::from(1u64),
mid_fee: Some(U256::from(1u64)),
out_fee: Some(U256::from(1u64)),
fee_gamma: Some(U256::from(1u64)),
d: Some(U256::from(1u64)),
gamma: Some(U256::from(1u64)),
price_scale: Some(vec![U256::from(1u64)]), ..Default::default()
};
let err = match build_pool(&state) {
Err(e) => e,
Ok(_) => panic!("expected error"),
};
assert!(matches!(err, BuildError::PriceScaleWrongLen { expected: 2, actual: 1 }));
}
#[test]
fn build_ng_without_offpeg_defaults_to_fee_denominator() {
let state = RawPoolState {
variant: CurveVariant::StableSwapNG,
balances: vec![U256::from(1u64), U256::from(1u64)],
token_decimals: vec![18, 18],
amp: U256::from(40_000u64),
fee: Some(U256::from(4_000_000u64)),
..Default::default()
};
let pool = build_pool(&state).expect("should succeed with defaulted offpeg");
assert_eq!(pool.offpeg_fee_multiplier(), Some(U256::from(10_000_000_000u64)));
}
#[test]
fn build_ng_crvusd_sdai_matches_on_chain() {
let state = RawPoolState {
variant: CurveVariant::StableSwapNG,
balances: vec![
"3219009600398261994"
.parse::<U256>()
.expect("balance 0"),
"311156701443769568"
.parse::<U256>()
.expect("balance 1"),
],
token_decimals: vec![18, 18],
amp: U256::from(150_000u64),
fee: Some(U256::from(1_000_000u64)),
dynamic_rates: Some(vec![
Some(
"1000000000000000000"
.parse::<U256>()
.expect("rate 0"),
),
Some(
"1173627645818786870"
.parse::<U256>()
.expect("rate 1"),
),
]),
..Default::default()
};
let pool = build_pool(&state).expect("should build with defaulted offpeg");
let dy = pool
.get_amount_out(0, 1, U256::from(3_219_009_600_398_261u64))
.expect("swap should succeed");
let expected = U256::from(2_720_818_166_217_034u64);
let diff = if dy > expected { dy - expected } else { expected - dy };
assert!(diff <= U256::from(1u64), "mismatch: got {dy}, expected {expected}, diff {diff}");
}
#[test]
fn build_meta_without_virtual_price_fails() {
let state = RawPoolState {
variant: CurveVariant::StableSwapMeta,
balances: vec![
U256::from(1_000_000_000u128),
U256::from(1_000_000_000_000_000_000_000u128),
],
token_decimals: vec![2, 18],
amp: U256::from(150_000u64),
fee: Some(U256::from(4_000_000u64)),
..Default::default()
};
let err = match build_pool(&state) {
Err(e) => e,
Ok(_) => panic!("expected MetaMissingVirtualPrice error"),
};
assert!(matches!(err, BuildError::MetaMissingVirtualPrice));
}
#[test]
fn build_meta_with_partial_dynamic_rates_missing_vp_fails() {
let state = RawPoolState {
variant: CurveVariant::StableSwapMeta,
balances: vec![
U256::from(1_000_000_000u128),
U256::from(1_000_000_000_000_000_000_000u128),
],
token_decimals: vec![2, 18],
amp: U256::from(150_000u64),
fee: Some(U256::from(4_000_000u64)),
dynamic_rates: Some(vec![None, None]), ..Default::default()
};
let err = match build_pool(&state) {
Err(e) => e,
Ok(_) => panic!("expected MetaMissingVirtualPrice error"),
};
assert!(matches!(err, BuildError::MetaMissingVirtualPrice));
}
#[test]
fn rates_match_fuzz_registry_18_dec() {
let rates = super::compute_stableswap_rates(&[18], &None);
assert_eq!(rates[0], U256::from(10u64).pow(U256::from(18u64)));
}
#[test]
fn rates_match_fuzz_registry_6_dec() {
let rates = super::compute_stableswap_rates(&[6], &None);
assert_eq!(rates[0], U256::from(10u64).pow(U256::from(30u64)));
}
#[test]
fn rates_match_fuzz_registry_8_dec() {
let rates = super::compute_stableswap_rates(&[8], &None);
assert_eq!(rates[0], U256::from(10u64).pow(U256::from(28u64)));
}
#[test]
fn rates_match_fuzz_registry_2_dec() {
let rates = super::compute_stableswap_rates(&[2], &None);
assert_eq!(rates[0], U256::from(10u64).pow(U256::from(34u64)));
}
#[test]
fn precisions_match_fuzz_registry() {
let precs = super::compute_crypto_precisions(&[6, 8, 18]);
assert_eq!(precs[0], U256::from(10u64).pow(U256::from(12u64))); assert_eq!(precs[1], U256::from(10u64).pow(U256::from(10u64))); assert_eq!(precs[2], U256::from(1u64)); }
#[test]
fn precision_mul_matches_fuzz_registry() {
let pm = super::compute_crypto_precisions(&[18, 6]);
assert_eq!(pm[0], U256::from(1u64));
assert_eq!(pm[1], U256::from(10u64).pow(U256::from(12u64)));
}
#[test]
fn build_all_11_variants_succeed() {
let stableswap_base = |variant: CurveVariant| -> RawPoolState {
RawPoolState {
variant,
balances: vec![U256::from(1_000_000_000_000_000_000u128); 2],
token_decimals: vec![18, 18],
amp: U256::from(40_000u64),
fee: Some(U256::from(4_000_000u64)),
..Default::default()
}
};
for v in
[CurveVariant::StableSwapV0, CurveVariant::StableSwapV1, CurveVariant::StableSwapV2]
{
assert!(build_pool(&stableswap_base(v)).is_ok(), "failed for {v}");
}
let mut meta = stableswap_base(CurveVariant::StableSwapMeta);
meta.dynamic_rates = Some(vec![None, Some(U256::from(10u64).pow(U256::from(18u64)))]);
assert!(build_pool(&meta).is_ok(), "failed for StableSwapMeta");
let mut ng = stableswap_base(CurveVariant::StableSwapNG);
ng.offpeg_fee_multiplier = Some(U256::from(20_000_000_000u128));
assert!(build_pool(&ng).is_ok(), "failed for StableSwapNG");
let mut alend = stableswap_base(CurveVariant::StableSwapALend);
alend.offpeg_fee_multiplier = Some(U256::from(20_000_000_000u128));
assert!(build_pool(&alend).is_ok(), "failed for StableSwapALend");
let crypto_base = |variant: CurveVariant, n: usize| -> RawPoolState {
RawPoolState {
variant,
balances: vec![U256::from(1_000_000_000_000_000_000u128); n],
token_decimals: vec![18; n],
amp: U256::from(540_000u64 * 10_000u64),
mid_fee: Some(U256::from(3_000_000u64)),
out_fee: Some(U256::from(30_000_000u64)),
fee_gamma: Some(U256::from(500_000_000_000_000u128)),
d: Some(U256::from(2_000_000_000_000_000_000_000u128)),
gamma: Some(U256::from(10_000_000_000_000u128)),
price_scale: Some(if n == 2 {
vec![U256::from(10u64).pow(U256::from(18u64))]
} else {
vec![U256::from(10u64).pow(U256::from(18u64)); n - 1]
}),
eth_variant: Some(true),
..Default::default()
}
};
for v in [CurveVariant::TwoCryptoV1, CurveVariant::TwoCryptoNG] {
assert!(build_pool(&crypto_base(v, 2)).is_ok(), "failed for {v}");
}
let mut tcs = crypto_base(CurveVariant::TwoCryptoStable, 2);
tcs.gamma = None;
assert!(build_pool(&tcs).is_ok(), "failed for TwoCryptoStable");
for v in [CurveVariant::TriCryptoV1, CurveVariant::TriCryptoNG] {
assert!(build_pool(&crypto_base(v, 3)).is_ok(), "failed for {v}");
}
}
fn u(s: &str) -> U256 {
U256::from_str_radix(s, 10).unwrap()
}
#[test]
fn integration_stableswap_v0_susd() {
let state = RawPoolState {
variant: CurveVariant::StableSwapV0,
balances: vec![
u("1919848022082255699479"),
u("1920322445"),
u("1920171938"),
u("21038816168255729764832232005"),
],
token_decimals: vec![18, 6, 6, 18],
amp: U256::from(256u64),
fee: Some(U256::from(2_000_000u64)),
..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool
.get_amount_out(0, 1, u("19198480220822556994"))
.unwrap();
assert_eq!(dy, U256::from(19_009_291u64));
}
#[test]
fn integration_stableswap_v1_3pool() {
let state = RawPoolState {
variant: CurveVariant::StableSwapV1,
balances: vec![
u("45102835177280382580138407"),
u("45853975278310"),
u("72989152672276"),
],
token_decimals: vec![18, 6, 6],
amp: U256::from(4000u64),
fee: Some(U256::from(1_500_000u64)),
..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool
.get_amount_out(0, 1, u("451028351772803825801384"))
.unwrap();
assert_eq!(dy, u("450961663745"));
}
#[test]
fn integration_stableswap_v2_frax_usdc() {
let state = RawPoolState {
variant: CurveVariant::StableSwapV2,
balances: vec![u("6722234569994793202271485"), u("714493991383")],
token_decimals: vec![18, 6],
amp: U256::from(150_000u64),
fee: Some(U256::from(1_000_000u64)),
..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool
.get_amount_out(0, 1, u("67222345699947932022714"))
.unwrap();
assert_eq!(dy, u("66561674655"));
}
#[test]
fn integration_stableswap_alend_aave() {
let state = RawPoolState {
variant: CurveVariant::StableSwapALend,
balances: vec![u("968991099162993551077367"), u("1012448901351"), u("414282246850")],
token_decimals: vec![18, 6, 6],
amp: U256::from(200_000u64),
fee: Some(U256::from(4_000_000u64)),
offpeg_fee_multiplier: Some(u("20000000000")),
..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool
.get_amount_out(0, 1, u("9689910991629935510773"))
.unwrap();
assert_eq!(dy, u("9686201099"));
}
#[test]
fn integration_stableswap_ng_usde_dai() {
let state = RawPoolState {
variant: CurveVariant::StableSwapNG,
balances: vec![u("124403796536542495997070"), u("95031311223261676260348")],
token_decimals: vec![18, 18],
amp: U256::from(40_000u64),
fee: Some(U256::from(4_000_000u64)),
offpeg_fee_multiplier: Some(u("20000000000")),
dynamic_rates: Some(vec![
Some(u("1000000000000000000")),
Some(u("1000000000000000000")),
]),
..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool
.get_amount_out(0, 1, u("1244037965365424959970"))
.unwrap();
assert_eq!(dy, u("1242635841481792448583"));
}
#[test]
fn integration_stableswap_meta_gusd_3crv() {
let state = RawPoolState {
variant: CurveVariant::StableSwapMeta,
balances: vec![u("59814423"), u("1210422553896217308280639")],
token_decimals: vec![2, 18],
amp: U256::from(100_000u64),
fee: Some(U256::from(4_000_000u64)),
dynamic_rates: Some(vec![
None, Some(u("1039823717145796146")), ]),
..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool
.get_amount_out(0, 1, u("598144"))
.unwrap();
assert_eq!(dy, u("5755338887370979902172"));
}
#[test]
fn integration_twocrypto_v1_crv_eth() {
let state = RawPoolState {
variant: CurveVariant::TwoCryptoV1,
balances: vec![u("33389428640766852909"), u("1538654846121127403001612563")],
token_decimals: vec![18, 18],
amp: U256::from(400_000u64),
d: Some(u("3338917956478824050009")),
gamma: Some(u("145000000000000")),
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(true), ..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool
.get_amount_out(0, 1, u("333894286407668529"))
.unwrap();
assert_eq!(dy, u("15024547954512515366680912"));
}
#[test]
fn integration_twocrypto_ng_crvusd_fxn() {
let state = RawPoolState {
variant: CurveVariant::TwoCryptoNG,
balances: vec![u("575304877931995002539"), u("1286854862507061937737")],
token_decimals: vec![18, 18],
amp: U256::from(400_000u64),
d: Some(u("1309807915207365083258")),
gamma: Some(u("145000000000000")),
price_scale: Some(vec![u("578321621819309618")]),
mid_fee: Some(U256::from(26_000_000u64)),
out_fee: Some(U256::from(45_000_000u64)),
fee_gamma: Some(u("230000000000000")),
..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool
.get_amount_out(0, 1, u("5753048779319950025"))
.unwrap();
assert_eq!(dy, u("12553693226638615366"));
}
#[test]
fn integration_twocrypto_stable_crvusd_weth() {
let state = RawPoolState {
variant: CurveVariant::TwoCryptoStable,
balances: vec![u("17087755783041929282185464"), u("13675635632110845893058")],
token_decimals: vec![18, 18],
amp: U256::from(25_000u64),
d: Some(u("53892663239303863640675237")),
price_scale: Some(vec![u("2783064941591876143844")]),
mid_fee: Some(U256::from(60_000_000u64)),
out_fee: Some(U256::from(220_000_000u64)),
fee_gamma: Some(u("1395000000000000")),
..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool
.get_amount_out(0, 1, u("170877557830419292821854"))
.unwrap();
assert_eq!(dy, u("77522288630419592645"));
}
#[test]
fn integration_tricrypto_v1_usdt_wbtc_weth() {
let state = RawPoolState {
variant: CurveVariant::TriCryptoV1,
balances: vec![u("3687737692530"), u("5185841754"), u("1696614171366863858308")],
token_decimals: vec![6, 8, 18],
amp: U256::from(1_707_629u64),
d: Some(u("11006845200255249518958282")),
gamma: Some(u("11809167828997")),
price_scale: Some(vec![u("70578404679338064954709"), u("2156666095129214805267")]),
mid_fee: Some(U256::from(3_000_000u64)),
out_fee: Some(U256::from(30_000_000u64)),
fee_gamma: Some(u("500000000000000")),
..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool
.get_amount_out(0, 1, u("36877376925"))
.unwrap();
assert_eq!(dy, U256::from(51_646_866u64));
}
#[test]
fn integration_tricrypto_ng_usdc_wbtc_weth() {
let state = RawPoolState {
variant: CurveVariant::TriCryptoNG,
balances: vec![u("3323859056394"), u("4735137544"), u("1544027711277257449902")],
token_decimals: vec![6, 8, 18],
amp: U256::from(1_707_629u64),
d: Some(u("10010654847128420517547506")),
gamma: Some(u("11809167828997")),
price_scale: Some(vec![u("70750968814053384159761"), u("2161000205852311064272")]),
mid_fee: Some(U256::from(3_000_000u64)),
out_fee: Some(U256::from(30_000_000u64)),
fee_gamma: Some(u("500000000000000")),
..Default::default()
};
let pool = build_pool(&state).unwrap();
let dy = pool
.get_amount_out(0, 1, u("33238590563"))
.unwrap();
assert_eq!(dy, U256::from(46_932_317u64));
}
}