use {
crate::{
curve::calculator::{
map_zero_to_none, CurveCalculator, DynPack, RoundDirection, SwapWithoutFeesResult,
TradeDirection, TradingTokenResult,
},
error::SwapError,
},
solana_program::{
program_error::ProgramError,
program_pack::{IsInitialized, Pack, Sealed},
},
spl_math::{checked_ceil_div::CheckedCeilDiv, precise_number::PreciseNumber},
};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ConstantProductCurve;
pub fn swap(
source_amount: u128,
swap_source_amount: u128,
swap_destination_amount: u128,
) -> Option<SwapWithoutFeesResult> {
let invariant = swap_source_amount.checked_mul(swap_destination_amount)?;
let new_swap_source_amount = swap_source_amount.checked_add(source_amount)?;
let (new_swap_destination_amount, new_swap_source_amount) =
invariant.checked_ceil_div(new_swap_source_amount)?;
let source_amount_swapped = new_swap_source_amount.checked_sub(swap_source_amount)?;
let destination_amount_swapped =
map_zero_to_none(swap_destination_amount.checked_sub(new_swap_destination_amount)?)?;
Some(SwapWithoutFeesResult {
source_amount_swapped,
destination_amount_swapped,
})
}
pub fn pool_tokens_to_trading_tokens(
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,
})
}
pub fn deposit_single_token_type(
source_amount: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
pool_supply: u128,
trade_direction: TradeDirection,
round_direction: RoundDirection,
) -> Option<u128> {
let swap_source_amount = match trade_direction {
TradeDirection::AtoB => swap_token_a_amount,
TradeDirection::BtoA => swap_token_b_amount,
};
let swap_source_amount = PreciseNumber::new(swap_source_amount)?;
let source_amount = PreciseNumber::new(source_amount)?;
let ratio = source_amount.checked_div(&swap_source_amount)?;
let one = PreciseNumber::new(1)?;
let base = one.checked_add(&ratio)?;
let root = base.sqrt()?.checked_sub(&one)?;
let pool_supply = PreciseNumber::new(pool_supply)?;
let pool_tokens = pool_supply.checked_mul(&root)?;
match round_direction {
RoundDirection::Floor => pool_tokens.floor()?.to_imprecise(),
RoundDirection::Ceiling => pool_tokens.ceiling()?.to_imprecise(),
}
}
pub fn withdraw_single_token_type_exact_out(
source_amount: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
pool_supply: u128,
trade_direction: TradeDirection,
round_direction: RoundDirection,
) -> Option<u128> {
let swap_source_amount = match trade_direction {
TradeDirection::AtoB => swap_token_a_amount,
TradeDirection::BtoA => swap_token_b_amount,
};
let swap_source_amount = PreciseNumber::new(swap_source_amount)?;
let source_amount = PreciseNumber::new(source_amount)?;
let ratio = source_amount.checked_div(&swap_source_amount)?;
let one = PreciseNumber::new(1)?;
let base = one.checked_sub(&ratio)?;
let root = one.checked_sub(&base.sqrt()?)?;
let pool_supply = PreciseNumber::new(pool_supply)?;
let pool_tokens = pool_supply.checked_mul(&root)?;
match round_direction {
RoundDirection::Floor => pool_tokens.floor()?.to_imprecise(),
RoundDirection::Ceiling => pool_tokens.ceiling()?.to_imprecise(),
}
}
pub fn normalized_value(
swap_token_a_amount: u128,
swap_token_b_amount: u128,
) -> Option<PreciseNumber> {
let swap_token_a_amount = PreciseNumber::new(swap_token_a_amount)?;
let swap_token_b_amount = PreciseNumber::new(swap_token_b_amount)?;
swap_token_a_amount
.checked_mul(&swap_token_b_amount)?
.sqrt()
}
impl CurveCalculator for ConstantProductCurve {
fn swap_without_fees(
&self,
source_amount: u128,
swap_source_amount: u128,
swap_destination_amount: u128,
_trade_direction: TradeDirection,
) -> Option<SwapWithoutFeesResult> {
swap(source_amount, swap_source_amount, swap_destination_amount)
}
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> {
pool_tokens_to_trading_tokens(
pool_tokens,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
round_direction,
)
}
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> {
deposit_single_token_type(
source_amount,
swap_token_a_amount,
swap_token_b_amount,
pool_supply,
trade_direction,
RoundDirection::Floor,
)
}
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> {
withdraw_single_token_type_exact_out(
source_amount,
swap_token_a_amount,
swap_token_b_amount,
pool_supply,
trade_direction,
RoundDirection::Ceiling,
)
}
fn normalized_value(
&self,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
) -> Option<PreciseNumber> {
normalized_value(swap_token_a_amount, swap_token_b_amount)
}
fn validate(&self) -> Result<(), SwapError> {
Ok(())
}
}
impl IsInitialized for ConstantProductCurve {
fn is_initialized(&self) -> bool {
true
}
}
impl Sealed for ConstantProductCurve {}
impl Pack for ConstantProductCurve {
const LEN: usize = 0;
fn pack_into_slice(&self, output: &mut [u8]) {
(self as &dyn DynPack).pack_into_slice(output);
}
fn unpack_from_slice(_input: &[u8]) -> Result<ConstantProductCurve, ProgramError> {
Ok(Self {})
}
}
impl DynPack for ConstantProductCurve {
fn pack_into_slice(&self, _output: &mut [u8]) {}
}
#[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 calculator = ConstantProductCurve {};
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 calculator = ConstantProductCurve {};
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 fail_trading_token_conversion() {
let calculator = ConstantProductCurve {};
let results =
calculator.pool_tokens_to_trading_tokens(5, 10, u128::MAX, 0, RoundDirection::Floor);
assert!(results.is_none());
let results =
calculator.pool_tokens_to_trading_tokens(5, 10, 0, u128::MAX, RoundDirection::Floor);
assert!(results.is_none());
}
#[test]
fn pack_constant_product_curve() {
let curve = ConstantProductCurve {};
let mut packed = [0u8; ConstantProductCurve::LEN];
Pack::pack_into_slice(&curve, &mut packed[..]);
let unpacked = ConstantProductCurve::unpack(&packed).unwrap();
assert_eq!(curve, unpacked);
let packed = vec![];
let unpacked = ConstantProductCurve::unpack(&packed).unwrap();
assert_eq!(curve, unpacked);
}
fn test_truncation(
curve: &ConstantProductCurve,
source_amount: u128,
swap_source_amount: u128,
swap_destination_amount: u128,
expected_source_amount_swapped: u128,
expected_destination_amount_swapped: u128,
) {
let invariant = swap_source_amount * swap_destination_amount;
let result = curve
.swap_without_fees(
source_amount,
swap_source_amount,
swap_destination_amount,
TradeDirection::AtoB,
)
.unwrap();
assert_eq!(result.source_amount_swapped, expected_source_amount_swapped);
assert_eq!(
result.destination_amount_swapped,
expected_destination_amount_swapped
);
let new_invariant = (swap_source_amount + result.source_amount_swapped)
* (swap_destination_amount - result.destination_amount_swapped);
assert!(new_invariant >= invariant);
}
#[test]
fn constant_product_swap_rounding() {
let curve = ConstantProductCurve::default();
assert!(curve
.swap_without_fees(10, 70_000_000_000, 4_000_000, TradeDirection::AtoB)
.is_none());
let tests: &[(u128, u128, u128, u128, u128)] = &[
(10, 4_000_000, 70_000_000_000, 10, 174_999), (20, 30_000 - 20, 10_000, 18, 6), (19, 30_000 - 20, 10_000, 18, 6), (18, 30_000 - 20, 10_000, 18, 6), (10, 20_000, 30_000, 10, 14), (10, 20_000 - 9, 30_000, 10, 14), (10, 20_000 - 10, 30_000, 10, 15), (100, 60_000, 30_000, 99, 49), (99, 60_000, 30_000, 99, 49), (98, 60_000, 30_000, 97, 48), ];
for (
source_amount,
swap_source_amount,
swap_destination_amount,
expected_source_amount,
expected_destination_amount,
) in tests.iter()
{
test_truncation(
&curve,
*source_amount,
*swap_source_amount,
*swap_destination_amount,
*expected_source_amount,
*expected_destination_amount,
);
}
}
proptest! {
#[test]
fn deposit_token_conversion(
source_token_amount in 2..u64::MAX,
swap_source_amount in 1..u64::MAX,
swap_destination_amount in 1..u64::MAX,
pool_supply in INITIAL_SWAP_POOL_AMOUNT..u64::MAX as u128,
) {
let curve = ConstantProductCurve {};
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,
);
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,
);
}
}
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,
) {
let curve = ConstantProductCurve {};
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
);
}
}
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,
) {
let curve = ConstantProductCurve {};
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 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,
) {
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 = ConstantProductCurve {};
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,
) {
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 = ConstantProductCurve {};
check_pool_value_from_withdraw(
&curve,
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
);
}
}
}