use {
crate::{
curve::calculator::{
CurveCalculator, DynPack, RoundDirection, SwapWithoutFeesResult, TradeDirection,
TradingTokenResult,
},
error::SwapError,
},
arrayref::{array_mut_ref, array_ref},
safecoin_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;
fn compute_a(amp: u64) -> Option<u64> {
amp.checked_mul(N_COINS as u64)
}
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)
}
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)
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct StableCurve {
pub amp: u64,
}
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)
}
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)?; if sum_x == 0 {
Some(0)
} else {
let mut d_previous: U256;
let mut d: U256 = sum_x.into();
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 = calculate_step(&d, leverage, sum_x, &d_product)?;
if d == d_previous {
break;
}
}
u128::try_from(d).ok()
}
}
fn compute_new_destination_amount(
leverage: u64,
new_source_amount: u128,
d_val: u128,
) -> Option<u128> {
let leverage: U256 = leverage.into();
let new_source_amount: U256 = new_source_amount.into();
let d_val: U256 = d_val.into();
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)?)?;
let b = new_source_amount.checked_add(d_val.checked_div(leverage)?)?;
let mut y = d_val;
for _ in 0..ITERATIONS {
let (y_new, _) = (checked_u8_power(&y, 2)?.checked_add(c)?)
.checked_ceil_div(checked_u8_mul(&y, 2)?.checked_add(b)?.checked_sub(d_val)?)?;
if y_new == y {
break;
} else {
y = y_new;
}
}
u128::try_from(y).ok()
}
impl CurveCalculator for StableCurve {
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,
})
}
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,
})
}
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> {
Ok(())
}
}
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::*;
use sim::StableSwapModel;
#[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);
}
proptest! {
#[test]
fn swap_no_fee(
swap_source_amount in 100..1_000_000_000_000_000_000u128,
swap_destination_amount in 100..1_000_000_000_000_000_000u128,
source_amount in 100..100_000_000_000u128,
amp in 1..150u64
) {
prop_assume!(source_amount < swap_source_amount);
let curve = StableCurve { amp };
let model: StableSwapModel = StableSwapModel::new(
curve.amp.into(),
vec![swap_source_amount, swap_destination_amount],
N_COINS,
);
let result = curve.swap_without_fees(
source_amount,
swap_source_amount,
swap_destination_amount,
TradeDirection::AtoB,
);
let result = result.unwrap();
let sim_result = model.sim_exchange(0, 1, source_amount);
let diff =
(sim_result as i128 - result.destination_amount_swapped as i128).abs();
let tolerance = std::cmp::max(2, sim_result as i128 / 1_000_000_000);
assert!(
diff <= tolerance,
"result={}, sim_result={}, amp={}, source_amount={}, swap_source_amount={}, swap_destination_amount={}, diff={}",
result.destination_amount_swapped,
sim_result,
amp,
source_amount,
swap_source_amount,
swap_destination_amount,
diff
);
}
}
#[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(&.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;
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;
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(
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
);
}
}
}