solana-program-library/token-swap/program/src/curve/stable.rs

617 lines
21 KiB
Rust

//! The curve.fi invariant calculator.
use {
crate::{
curve::calculator::{
CurveCalculator, DynPack, RoundDirection, SwapWithoutFeesResult, TradeDirection,
TradingTokenResult,
},
error::SwapError,
},
arrayref::{array_mut_ref, array_ref},
solana_program::{
program_error::ProgramError,
program_pack::{IsInitialized, Pack, Sealed},
},
spl_math::{checked_ceil_div::CheckedCeilDiv, precise_number::PreciseNumber, uint::U256},
std::convert::TryFrom,
};
const N_COINS: u8 = 2;
const N_COINS_SQUARED: u8 = 4;
const ITERATIONS: u8 = 32;
/// Calculates A for deriving D
///
/// Per discussion with the designer and writer of stable curves, this A is not
/// the same as the A from the whitepaper, it's actually `A * n**(n-1)`, so when
/// you set A, you actually set `A * n**(n-1)`. This is because `D**n / prod(x)`
/// loses precision with a huge A value.
///
/// There is little information to document this choice, but the original contracts
/// use this same convention, see a comment in the code at:
/// https://github.com/curvefi/curve-contract/blob/b0bbf77f8f93c9c5f4e415bce9cd71f0cdee960e/contracts/pool-templates/base/SwapTemplateBase.vy#L136
fn compute_a(amp: u64) -> Option<u64> {
amp.checked_mul(N_COINS as u64)
}
/// Returns self to the power of b
fn checked_u8_power(a: &U256, b: u8) -> Option<U256> {
let mut result = *a;
for _ in 1..b {
result = result.checked_mul(*a)?;
}
Some(result)
}
/// Returns self multiplied by b
fn checked_u8_mul(a: &U256, b: u8) -> Option<U256> {
let mut result = *a;
for _ in 1..b {
result = result.checked_add(*a)?;
}
Some(result)
}
/// StableCurve struct implementing CurveCalculator
#[derive(Clone, Debug, Default, PartialEq)]
pub struct StableCurve {
/// Amplifier constant
pub amp: u64,
}
/// d = (leverage * sum_x + d_product * n_coins) * initial_d / ((leverage - 1) * initial_d + (n_coins + 1) * d_product)
fn calculate_step(initial_d: &U256, leverage: u64, sum_x: u128, d_product: &U256) -> Option<U256> {
let leverage_mul = U256::from(leverage).checked_mul(sum_x.into())?;
let d_p_mul = checked_u8_mul(d_product, N_COINS)?;
let l_val = leverage_mul.checked_add(d_p_mul)?.checked_mul(*initial_d)?;
let leverage_sub = initial_d.checked_mul((leverage.checked_sub(1)?).into())?;
let n_coins_sum = checked_u8_mul(d_product, N_COINS.checked_add(1)?)?;
let r_val = leverage_sub.checked_add(n_coins_sum)?;
l_val.checked_div(r_val)
}
/// Compute stable swap invariant (D)
/// Equation:
/// A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i))
fn compute_d(leverage: u64, amount_a: u128, amount_b: u128) -> Option<u128> {
let amount_a_times_coins =
checked_u8_mul(&U256::from(amount_a), N_COINS)?.checked_add(U256::one())?;
let amount_b_times_coins =
checked_u8_mul(&U256::from(amount_b), N_COINS)?.checked_add(U256::one())?;
let sum_x = amount_a.checked_add(amount_b)?; // sum(x_i), a.k.a S
if sum_x == 0 {
Some(0)
} else {
let mut d_previous: U256;
let mut d: U256 = sum_x.into();
// Newton's method to approximate D
for _ in 0..ITERATIONS {
let mut d_product = d;
d_product = d_product
.checked_mul(d)?
.checked_div(amount_a_times_coins)?;
d_product = d_product
.checked_mul(d)?
.checked_div(amount_b_times_coins)?;
d_previous = d;
//d = (leverage * sum_x + d_p * n_coins) * d / ((leverage - 1) * d + (n_coins + 1) * d_p);
d = calculate_step(&d, leverage, sum_x, &d_product)?;
// Equality with the precision of 1
if d == d_previous {
break;
}
}
u128::try_from(d).ok()
}
}
/// Compute swap amount `y` in proportion to `x`
/// Solve for y:
/// y**2 + y * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A)
/// y**2 + b*y = c
fn compute_new_destination_amount(
leverage: u64,
new_source_amount: u128,
d_val: u128,
) -> Option<u128> {
// Upscale to U256
let leverage: U256 = leverage.into();
let new_source_amount: U256 = new_source_amount.into();
let d_val: U256 = d_val.into();
let zero = U256::from(0u128);
let one = U256::from(1u128);
// sum' = prod' = x
// c = D ** (n + 1) / (n ** (2 * n) * prod' * A)
let c = checked_u8_power(&d_val, N_COINS.checked_add(1)?)?
.checked_div(checked_u8_mul(&new_source_amount, N_COINS_SQUARED)?.checked_mul(leverage)?)?;
// b = sum' - (A*n**n - 1) * D / (A * n**n)
let b = new_source_amount.checked_add(d_val.checked_div(leverage)?)?;
// Solve for y by approximating: y**2 + b*y = c
let mut y = d_val;
for _ in 0..ITERATIONS {
let numerator = checked_u8_power(&y, 2)?.checked_add(c)?;
let denominator = checked_u8_mul(&y, 2)?.checked_add(b)?.checked_sub(d_val)?;
// checked_ceil_div is conservative, not allowing for a 0 return, but we can
// ceiling to 1 token in this case since we're solving through approximation,
// and not doing a constant product calculation
let (y_new, _) = numerator.checked_ceil_div(denominator).unwrap_or_else(|| {
if numerator == U256::from(0u128) {
(zero, zero)
} else {
(one, zero)
}
});
if y_new == y {
break;
} else {
y = y_new;
}
}
u128::try_from(y).ok()
}
impl CurveCalculator for StableCurve {
/// Stable curve
fn swap_without_fees(
&self,
source_amount: u128,
swap_source_amount: u128,
swap_destination_amount: u128,
_trade_direction: TradeDirection,
) -> Option<SwapWithoutFeesResult> {
if source_amount == 0 {
return Some(SwapWithoutFeesResult {
source_amount_swapped: 0,
destination_amount_swapped: 0,
});
}
let leverage = compute_a(self.amp)?;
let new_source_amount = swap_source_amount.checked_add(source_amount)?;
let new_destination_amount = compute_new_destination_amount(
leverage,
new_source_amount,
compute_d(leverage, swap_source_amount, swap_destination_amount)?,
)?;
let amount_swapped = swap_destination_amount.checked_sub(new_destination_amount)?;
Some(SwapWithoutFeesResult {
source_amount_swapped: source_amount,
destination_amount_swapped: amount_swapped,
})
}
/// Re-implementation of `remove_liquidity`:
///
/// <https://github.com/curvefi/curve-contract/blob/80bbe179083c9a7062e4c482b0be3bfb7501f2bd/contracts/pool-templates/base/SwapTemplateBase.vy#L513>
fn pool_tokens_to_trading_tokens(
&self,
pool_tokens: u128,
pool_token_supply: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
round_direction: RoundDirection,
) -> Option<TradingTokenResult> {
let mut token_a_amount = pool_tokens
.checked_mul(swap_token_a_amount)?
.checked_div(pool_token_supply)?;
let mut token_b_amount = pool_tokens
.checked_mul(swap_token_b_amount)?
.checked_div(pool_token_supply)?;
let (token_a_amount, token_b_amount) = match round_direction {
RoundDirection::Floor => (token_a_amount, token_b_amount),
RoundDirection::Ceiling => {
let token_a_remainder = pool_tokens
.checked_mul(swap_token_a_amount)?
.checked_rem(pool_token_supply)?;
if token_a_remainder > 0 && token_a_amount > 0 {
token_a_amount += 1;
}
let token_b_remainder = pool_tokens
.checked_mul(swap_token_b_amount)?
.checked_rem(pool_token_supply)?;
if token_b_remainder > 0 && token_b_amount > 0 {
token_b_amount += 1;
}
(token_a_amount, token_b_amount)
}
};
Some(TradingTokenResult {
token_a_amount,
token_b_amount,
})
}
/// Get the amount of pool tokens for the given amount of token A or B.
/// Re-implementation of `calc_token_amount`:
///
/// <https://github.com/curvefi/curve-contract/blob/80bbe179083c9a7062e4c482b0be3bfb7501f2bd/contracts/pool-templates/base/SwapTemplateBase.vy#L267>
fn deposit_single_token_type(
&self,
source_amount: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
pool_supply: u128,
trade_direction: TradeDirection,
) -> Option<u128> {
if source_amount == 0 {
return Some(0);
}
let leverage = compute_a(self.amp)?;
let d0 = PreciseNumber::new(compute_d(
leverage,
swap_token_a_amount,
swap_token_b_amount,
)?)?;
let (deposit_token_amount, other_token_amount) = match trade_direction {
TradeDirection::AtoB => (swap_token_a_amount, swap_token_b_amount),
TradeDirection::BtoA => (swap_token_b_amount, swap_token_a_amount),
};
let updated_deposit_token_amount = deposit_token_amount.checked_add(source_amount)?;
let d1 = PreciseNumber::new(compute_d(
leverage,
updated_deposit_token_amount,
other_token_amount,
)?)?;
let diff = d1.checked_sub(&d0)?;
let final_amount =
(diff.checked_mul(&PreciseNumber::new(pool_supply)?))?.checked_div(&d0)?;
final_amount.floor()?.to_imprecise()
}
fn withdraw_single_token_type_exact_out(
&self,
source_amount: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
pool_supply: u128,
trade_direction: TradeDirection,
) -> Option<u128> {
if source_amount == 0 {
return Some(0);
}
let leverage = compute_a(self.amp)?;
let d0 = PreciseNumber::new(compute_d(
leverage,
swap_token_a_amount,
swap_token_b_amount,
)?)?;
let (withdraw_token_amount, other_token_amount) = match trade_direction {
TradeDirection::AtoB => (swap_token_a_amount, swap_token_b_amount),
TradeDirection::BtoA => (swap_token_b_amount, swap_token_a_amount),
};
let updated_deposit_token_amount = withdraw_token_amount.checked_sub(source_amount)?;
let d1 = PreciseNumber::new(compute_d(
leverage,
updated_deposit_token_amount,
other_token_amount,
)?)?;
let diff = d0.checked_sub(&d1)?;
let final_amount =
(diff.checked_mul(&PreciseNumber::new(pool_supply)?))?.checked_div(&d0)?;
final_amount.ceiling()?.to_imprecise()
}
fn normalized_value(
&self,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
) -> Option<PreciseNumber> {
#[cfg(not(any(test, feature = "fuzz")))]
{
let leverage = compute_a(self.amp)?;
PreciseNumber::new(compute_d(
leverage,
swap_token_a_amount,
swap_token_b_amount,
)?)
}
#[cfg(any(test, feature = "fuzz"))]
{
use roots::{find_roots_cubic_normalized, Roots};
let x = swap_token_a_amount as f64;
let y = swap_token_b_amount as f64;
let c = (4.0 * (self.amp as f64)) - 1.0;
let d = 16.0 * (self.amp as f64) * x * y * (x + y);
let roots = find_roots_cubic_normalized(0.0, c, d);
let x0 = match roots {
Roots::No(_) => panic!("No roots found for cubic equations"),
Roots::One(x) => x[0],
Roots::Two(_) => panic!("Two roots found for cubic, mathematically impossible"),
Roots::Three(x) => x[1],
Roots::Four(_) => panic!("Four roots found for cubic, mathematically impossible"),
};
let root_uint = (x0 * ((10f64).powf(11.0))).round() as u128;
let precision = PreciseNumber::new(10)?.checked_pow(11)?;
let two = PreciseNumber::new(2)?;
PreciseNumber::new(root_uint)?
.checked_div(&precision)?
.checked_div(&two)
}
}
fn validate(&self) -> Result<(), SwapError> {
// TODO are all amps valid?
Ok(())
}
}
/// IsInitialized is required to use `Pack::pack` and `Pack::unpack`
impl IsInitialized for StableCurve {
fn is_initialized(&self) -> bool {
true
}
}
impl Sealed for StableCurve {}
impl Pack for StableCurve {
const LEN: usize = 8;
fn pack_into_slice(&self, output: &mut [u8]) {
(self as &dyn DynPack).pack_into_slice(output);
}
fn unpack_from_slice(input: &[u8]) -> Result<StableCurve, ProgramError> {
let amp = array_ref![input, 0, 8];
Ok(Self {
amp: u64::from_le_bytes(*amp),
})
}
}
impl DynPack for StableCurve {
fn pack_into_slice(&self, output: &mut [u8]) {
let amp = array_mut_ref![output, 0, 8];
*amp = self.amp.to_le_bytes();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::curve::calculator::{
test::{
check_curve_value_from_swap, check_deposit_token_conversion,
check_pool_value_from_deposit, check_pool_value_from_withdraw,
check_withdraw_token_conversion, total_and_intermediate,
CONVERSION_BASIS_POINTS_GUARANTEE,
},
RoundDirection, INITIAL_SWAP_POOL_AMOUNT,
};
use proptest::prelude::*;
#[test]
fn initial_pool_amount() {
let amp = 1;
let calculator = StableCurve { amp };
assert_eq!(calculator.new_pool_supply(), INITIAL_SWAP_POOL_AMOUNT);
}
fn check_pool_token_rate(
token_a: u128,
token_b: u128,
deposit: u128,
supply: u128,
expected_a: u128,
expected_b: u128,
) {
let amp = 1;
let calculator = StableCurve { amp };
let results = calculator
.pool_tokens_to_trading_tokens(
deposit,
supply,
token_a,
token_b,
RoundDirection::Ceiling,
)
.unwrap();
assert_eq!(results.token_a_amount, expected_a);
assert_eq!(results.token_b_amount, expected_b);
}
#[test]
fn trading_token_conversion() {
check_pool_token_rate(2, 49, 5, 10, 1, 25);
check_pool_token_rate(100, 202, 5, 101, 5, 10);
check_pool_token_rate(5, 501, 2, 10, 1, 101);
}
#[test]
fn swap_zero() {
let curve = StableCurve { amp: 100 };
let result = curve.swap_without_fees(0, 100, 1_000_000_000_000_000, TradeDirection::AtoB);
let result = result.unwrap();
assert_eq!(result.source_amount_swapped, 0);
assert_eq!(result.destination_amount_swapped, 0);
}
#[test]
fn pack_curve() {
let amp = 1;
let curve = StableCurve { amp };
let mut packed = [0u8; StableCurve::LEN];
Pack::pack_into_slice(&curve, &mut packed[..]);
let unpacked = StableCurve::unpack(&packed).unwrap();
assert_eq!(curve, unpacked);
let mut packed = vec![];
packed.extend_from_slice(&amp.to_le_bytes());
let unpacked = StableCurve::unpack(&packed).unwrap();
assert_eq!(curve, unpacked);
}
proptest! {
#[test]
fn curve_value_does_not_decrease_from_deposit(
pool_token_amount in 1..u64::MAX,
pool_token_supply in 1..u64::MAX,
swap_token_a_amount in 1..u64::MAX,
swap_token_b_amount in 1..u64::MAX,
amp in 1..100,
) {
let pool_token_amount = pool_token_amount as u128;
let pool_token_supply = pool_token_supply as u128;
let swap_token_a_amount = swap_token_a_amount as u128;
let swap_token_b_amount = swap_token_b_amount as u128;
// Make sure we will get at least one trading token out for each
// side, otherwise the calculation fails
prop_assume!(pool_token_amount * swap_token_a_amount / pool_token_supply >= 1);
prop_assume!(pool_token_amount * swap_token_b_amount / pool_token_supply >= 1);
let curve = StableCurve {
amp: amp as u64
};
check_pool_value_from_deposit(
&curve,
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
);
}
}
proptest! {
#[test]
fn curve_value_does_not_decrease_from_withdraw(
(pool_token_supply, pool_token_amount) in total_and_intermediate(),
swap_token_a_amount in 1..u64::MAX,
swap_token_b_amount in 1..u64::MAX,
amp in 1..100,
) {
let pool_token_amount = pool_token_amount as u128;
let pool_token_supply = pool_token_supply as u128;
let swap_token_a_amount = swap_token_a_amount as u128;
let swap_token_b_amount = swap_token_b_amount as u128;
// Make sure we will get at least one trading token out for each
// side, otherwise the calculation fails
prop_assume!(pool_token_amount * swap_token_a_amount / pool_token_supply >= 1);
prop_assume!(pool_token_amount * swap_token_b_amount / pool_token_supply >= 1);
let curve = StableCurve {
amp: amp as u64
};
check_pool_value_from_withdraw(
&curve,
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
);
}
}
proptest! {
#[test]
fn curve_value_does_not_decrease_from_swap(
source_token_amount in 1..u64::MAX,
swap_source_amount in 1..u64::MAX,
swap_destination_amount in 1..u64::MAX,
amp in 1..100,
) {
let curve = StableCurve { amp: amp as u64 };
check_curve_value_from_swap(
&curve,
source_token_amount as u128,
swap_source_amount as u128,
swap_destination_amount as u128,
TradeDirection::AtoB
);
}
}
proptest! {
#[test]
fn deposit_token_conversion(
// in the pool token conversion calcs, we simulate trading half of
// source_token_amount, so this needs to be at least 2
source_token_amount in 2..u64::MAX,
swap_source_amount in 1..u64::MAX,
swap_destination_amount in 2..u64::MAX,
pool_supply in INITIAL_SWAP_POOL_AMOUNT..u64::MAX as u128,
amp in 1..100u64,
) {
let curve = StableCurve { amp };
check_deposit_token_conversion(
&curve,
source_token_amount as u128,
swap_source_amount as u128,
swap_destination_amount as u128,
TradeDirection::AtoB,
pool_supply,
CONVERSION_BASIS_POINTS_GUARANTEE * 100,
);
check_deposit_token_conversion(
&curve,
source_token_amount as u128,
swap_source_amount as u128,
swap_destination_amount as u128,
TradeDirection::BtoA,
pool_supply,
CONVERSION_BASIS_POINTS_GUARANTEE * 100,
);
}
}
proptest! {
#[test]
fn withdraw_token_conversion(
(pool_token_supply, pool_token_amount) in total_and_intermediate(),
swap_token_a_amount in 1..u64::MAX,
swap_token_b_amount in 1..u64::MAX,
amp in 1..100u64,
) {
let curve = StableCurve { amp };
check_withdraw_token_conversion(
&curve,
pool_token_amount as u128,
pool_token_supply as u128,
swap_token_a_amount as u128,
swap_token_b_amount as u128,
TradeDirection::AtoB,
CONVERSION_BASIS_POINTS_GUARANTEE
);
check_withdraw_token_conversion(
&curve,
pool_token_amount as u128,
pool_token_supply as u128,
swap_token_a_amount as u128,
swap_token_b_amount as u128,
TradeDirection::BtoA,
CONVERSION_BASIS_POINTS_GUARANTEE
);
}
}
// this test comes from a failed proptest
#[test]
fn withdraw_token_conversion_huge_withdrawal() {
let pool_token_supply: u64 = 12798273514859089136;
let pool_token_amount: u64 = 12798243809352362806;
let swap_token_a_amount: u64 = 10000000000000000000;
let swap_token_b_amount: u64 = 6000000000000000000;
let amp = 72;
let curve = StableCurve { amp };
check_withdraw_token_conversion(
&curve,
pool_token_amount as u128,
pool_token_supply as u128,
swap_token_a_amount as u128,
swap_token_b_amount as u128,
TradeDirection::AtoB,
CONVERSION_BASIS_POINTS_GUARANTEE,
);
}
}