use alloy_primitives::U256;
use crate::evm::protocol::curve::math::core::stableswap_v1::{
get_d, get_y, get_y_d, A_PRECISION, FEE_DENOMINATOR, PRECISION,
};
pub fn get_amount_out(
balances: &[U256],
rates: &[U256],
amp: U256,
fee: U256,
i: usize,
j: usize,
dx: U256,
) -> Option<U256> {
if dx.is_zero() {
return None;
}
let precision = U256::from(PRECISION);
let xp: Vec<U256> = balances
.iter()
.zip(rates.iter())
.map(|(b, r)| *b * *r / precision)
.collect();
let d = get_d(&xp, amp)?;
let x_new = xp[i] + dx * rates[i] / precision;
let y_new = get_y(i, j, x_new, &xp, d, amp)?;
if xp[j] <= y_new {
return None;
}
let dy = (xp[j] - y_new - U256::from(1)) * precision / rates[j];
let fee_amount = fee * dy / U256::from(FEE_DENOMINATOR);
let result = dy - fee_amount;
if result.is_zero() {
return None;
}
Some(result)
}
pub fn get_amount_in(
balances: &[U256],
rates: &[U256],
amp: U256,
fee: U256,
i: usize,
j: usize,
desired_output: U256,
) -> Option<U256> {
if desired_output.is_zero() {
return None;
}
let precision = U256::from(PRECISION);
let fee_denom = U256::from(FEE_DENOMINATOR);
let xp: Vec<U256> = balances
.iter()
.zip(rates.iter())
.map(|(b, r)| *b * *r / precision)
.collect();
let d = get_d(&xp, amp)?;
let fee_complement = fee_denom - fee;
let dy = (desired_output * fee_denom + fee_complement - U256::from(1)) / fee_complement;
let dy_internal = dy * rates[j] / precision;
if xp[j] <= dy_internal + U256::from(1) {
return None;
}
let y_new = xp[j] - dy_internal - U256::from(1);
let x_new = get_y(j, i, y_new, &xp, d, amp)?;
if x_new <= xp[i] {
return None;
}
let dx = (x_new - xp[i]) * precision / rates[i] + U256::from(1);
Some(dx)
}
pub fn spot_price(
balances: &[U256],
rates: &[U256],
amp: U256,
fee: U256,
i: usize,
j: usize,
) -> Option<(U256, U256)> {
let precision = U256::from(PRECISION);
let fee_denom = U256::from(FEE_DENOMINATOR);
let n = U256::from(balances.len());
let ann_eff = amp.checked_mul(n)? / U256::from(A_PRECISION);
let xp: Vec<U256> = balances
.iter()
.zip(rates.iter())
.map(|(b, r)| *b * *r / precision)
.collect();
let d = get_d(&xp, amp)?;
let mut d_p = d;
for x_k in &xp {
d_p = d_p
.checked_mul(d)?
.checked_div(x_k.checked_mul(n)?)?;
}
let num_xp = ann_eff
.checked_mul(xp[i])?
.checked_add(d_p)?;
let den_xp = ann_eff
.checked_mul(xp[j])?
.checked_add(d_p)?;
if den_xp.is_zero() {
return None;
}
let numerator = num_xp
.checked_mul(balances[j])?
.checked_mul(fee_denom - fee)?;
let denominator = den_xp
.checked_mul(balances[i])?
.checked_mul(fee_denom)?;
Some((numerator, denominator))
}
pub fn calc_withdraw_one_coin(
balances: &[U256],
rates: &[U256],
amp: U256,
fee: U256,
token_amount: U256,
i: usize,
total_supply: U256,
) -> Option<U256> {
if token_amount.is_zero() || total_supply.is_zero() || i >= balances.len() {
return None;
}
let precision = U256::from(PRECISION);
let fee_denom = U256::from(FEE_DENOMINATOR);
let n = balances.len();
let n_u256 = U256::from(n);
let reduced_fee = fee
.checked_mul(n_u256)?
.checked_div(U256::from(4).checked_mul(U256::from(n - 1))?)?;
let xp: Vec<U256> = balances
.iter()
.zip(rates.iter())
.map(|(b, r)| *b * *r / precision)
.collect();
let d0 = get_d(&xp, amp)?;
let d1 = d0.checked_sub(
token_amount
.checked_mul(d0)?
.checked_div(total_supply)?,
)?;
let new_y = get_y_d(i, &xp, d1, amp)?;
let mut xp_reduced = xp.clone();
for j in 0..n {
let dx_expected = if j == i {
xp[j]
.checked_mul(d1)?
.checked_div(d0)?
.checked_sub(new_y)?
} else {
xp[j].checked_sub(xp[j].checked_mul(d1)?.checked_div(d0)?)?
};
xp_reduced[j] = xp_reduced[j].checked_sub(
reduced_fee
.checked_mul(dx_expected)?
.checked_div(fee_denom)?,
)?;
}
let new_y_reduced = get_y_d(i, &xp_reduced, d1, amp)?;
let dy = xp_reduced[i].checked_sub(new_y_reduced)?;
let result = dy
.checked_sub(U256::from(1))?
.checked_mul(precision)?
.checked_div(rates[i])?;
if result.is_zero() {
return None;
}
Some(result)
}
pub fn calc_add_liquidity(
balances: &[U256],
rates: &[U256],
amp: U256,
fee: U256,
amounts: &[U256],
total_supply: U256,
) -> Option<U256> {
if amounts.len() != balances.len() {
return None;
}
if amounts.iter().all(|a| a.is_zero()) {
return None;
}
let precision = U256::from(PRECISION);
let fee_denom = U256::from(FEE_DENOMINATOR);
let n = balances.len();
let n_u256 = U256::from(n);
let reduced_fee = fee
.checked_mul(n_u256)?
.checked_div(U256::from(4).checked_mul(U256::from(n - 1))?)?;
let normalize = |bals: &[U256]| -> Vec<U256> {
bals.iter()
.zip(rates.iter())
.map(|(b, r)| *b * *r / precision)
.collect()
};
let d0 = if total_supply > U256::ZERO { get_d(&normalize(balances), amp)? } else { U256::ZERO };
let new_balances: Vec<U256> = balances
.iter()
.zip(amounts.iter())
.map(|(b, a)| *b + *a)
.collect();
let d1 = get_d(&normalize(&new_balances), amp)?;
if d1 <= d0 {
return None;
}
if total_supply.is_zero() {
return Some(d1);
}
let mut new_balances_after_fee = new_balances.clone();
for idx in 0..n {
let ideal = d1
.checked_mul(balances[idx])?
.checked_div(d0)?;
let difference = if ideal > new_balances[idx] {
ideal - new_balances[idx]
} else {
new_balances[idx] - ideal
};
let fee_i = reduced_fee
.checked_mul(difference)?
.checked_div(fee_denom)?;
new_balances_after_fee[idx] = new_balances_after_fee[idx].checked_sub(fee_i)?;
}
let d2 = get_d(&normalize(&new_balances_after_fee), amp)?;
let mint_amount = total_supply
.checked_mul(d2.checked_sub(d0)?)?
.checked_div(d0)?;
if mint_amount.is_zero() {
return None;
}
Some(mint_amount)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip() {
let rate18 = U256::from(1_000_000_000_000_000_000u128);
let balances = [
U256::from(50_000_000_000_000_000_000_000_000u128),
U256::from(50_000_000_000_000_000_000_000_000u128),
U256::from(50_000_000_000_000_000_000_000_000u128),
];
let rates = [rate18, rate18, rate18];
let amp = U256::from(2000u64);
let fee = U256::from(1_000_000u64);
let dx = U256::from(1_000_000_000_000_000_000_000u128);
let dy = get_amount_out(&balances, &rates, amp, fee, 0, 1, dx).expect("out");
let dx_recovered = get_amount_in(&balances, &rates, amp, fee, 0, 1, dy).expect("in");
assert!(dx_recovered >= dx);
assert!(dx_recovered <= dx + U256::from(2));
let dy_check =
get_amount_out(&balances, &rates, amp, fee, 0, 1, dx_recovered).expect("check");
assert!(dy_check >= dy);
}
#[test]
fn spot_price_balanced_near_one() {
let rate18 = U256::from(1_000_000_000_000_000_000u128);
let balances = [
U256::from(50_000_000_000_000_000_000_000_000u128),
U256::from(50_000_000_000_000_000_000_000_000u128),
U256::from(50_000_000_000_000_000_000_000_000u128),
];
let rates = [rate18, rate18, rate18];
let amp = U256::from(2000u64);
let fee = U256::from(1_000_000u64);
let (num, den) = spot_price(&balances, &rates, amp, fee, 0, 1).expect("price");
let diff = if num > den { num - den } else { den - num };
assert!(diff * U256::from(1000) < den, "spot price not near 1");
}
#[test]
fn spot_price_symmetry() {
let rate18 = U256::from(1_000_000_000_000_000_000u128);
let balances = [
U256::from(50_000_000_000_000_000_000_000_000u128),
U256::from(50_000_000_000_000_000_000_000_000u128),
U256::from(50_000_000_000_000_000_000_000_000u128),
];
let rates = [rate18, rate18, rate18];
let amp = U256::from(2000u64);
let fee = U256::from(1_000_000u64);
let (num_ij, den_ij) = spot_price(&balances, &rates, amp, fee, 0, 1).expect("price_ij");
let (num_ji, den_ji) = spot_price(&balances, &rates, amp, fee, 1, 0).expect("price_ji");
let product_num = num_ij * num_ji;
let product_den = den_ij * den_ji;
let diff = if product_num > product_den {
product_num - product_den
} else {
product_den - product_num
};
assert!(diff * U256::from(1000) < product_den, "symmetry violated");
}
#[test]
fn spot_price_consistent_with_swap() {
let rate18 = U256::from(1_000_000_000_000_000_000u128);
let balances = [
U256::from(50_000_000_000_000_000_000_000_000u128),
U256::from(50_000_000_000_000_000_000_000_000u128),
U256::from(50_000_000_000_000_000_000_000_000u128),
];
let rates = [rate18, rate18, rate18];
let amp = U256::from(2000u64);
let fee = U256::from(1_000_000u64);
let dx = U256::from(1_000_000_000_000_000u128);
let dy = get_amount_out(&balances, &rates, amp, fee, 0, 1, dx).expect("out");
let (num, den) = spot_price(&balances, &rates, amp, fee, 0, 1).expect("price");
let lhs = dy * den;
let rhs = dx * num;
let diff = if lhs > rhs { lhs - rhs } else { rhs - lhs };
assert!(diff * U256::from(100) < rhs, "spot price inconsistent with swap");
}
#[test]
fn calc_withdraw_one_coin_matches_onchain() {
let balances = [
U256::from(63975337809806329031583135u128), U256::from(61219263170093u128), U256::from(37832425459809u128), ];
let rates = [
U256::from(1_000_000_000_000_000_000u128), U256::from(1_000_000_000_000_000_000_000_000_000_000u128), U256::from(1_000_000_000_000_000_000_000_000_000_000u128), ];
let amp = U256::from(4000u64);
let fee = U256::from(1500000u64);
let total_supply = U256::from(156782246573983669718736356u128);
let token_amount = U256::from(1_000_000_000_000_000_000u128); let result =
calc_withdraw_one_coin(&balances, &rates, amp, fee, token_amount, 1, total_supply)
.expect("should succeed");
assert_eq!(result, U256::from(1039789u64), "must match on-chain exactly");
}
#[test]
fn calc_add_liquidity_and_withdraw_roundtrip() {
let balances = [
U256::from(63975337809806329031583135u128),
U256::from(61219263170093u128),
U256::from(37832425459809u128),
];
let rates = [
U256::from(1_000_000_000_000_000_000u128),
U256::from(1_000_000_000_000_000_000_000_000_000_000u128),
U256::from(1_000_000_000_000_000_000_000_000_000_000u128),
];
let amp = U256::from(4000u64);
let fee = U256::from(1500000u64);
let total_supply = U256::from(156782246573983669718736356u128);
let deposit = U256::from(1_000_000_000u64); let amounts = [U256::ZERO, deposit, U256::ZERO];
let lp_minted = calc_add_liquidity(&balances, &rates, amp, fee, &amounts, total_supply)
.expect("add should succeed");
let new_supply = total_supply + lp_minted;
let withdrawn = calc_withdraw_one_coin(
&[balances[0], balances[1] + deposit, balances[2]],
&rates,
amp,
fee,
lp_minted,
1,
new_supply,
)
.expect("withdraw should succeed");
assert!(withdrawn < deposit, "should lose some to fees");
assert!(
withdrawn > deposit * U256::from(99) / U256::from(100),
"too much fee loss: deposited {deposit}, withdrew {withdrawn}"
);
}
}