swap: Add rounding correction for deposit / withdraw (#1061)

* Add rounding specification for deposit / withdraw

The fuzzing test was improved to make sure that the value of pool tokens
(minus fees) never changed, which revealed a calc bug with deposits and
withdrawals.  If someone withdraws, they can take additional small
value out of the pool in the form of truncated value.

Before, deposit and withdrawal used the same calculation, rounding up the token
A and B amounts.  Remember that deposit / withdrawal specify the pool
token amount, and calculate the amount of A / B required to get to that
many pool tokens.  On withdrawal, we were giving back a little bit too
much.  The concept is that everything would even out since a deposit is
required for a withdrawal, which makes everything a wash.

Fuzzing found this issue very quickly! The change is to introduce a
rounding parameter to avoid ever giving away too much value, either
during deposit or withdrawal.

* Cleanup

* Add withdraw test

* Cleanup

* Cargo fmt

* Remove curve tests from fuzzing cfg

* Update JS test

* Cleanup test

* Update honggfuzz again

* Fix fee on JS test

* Force correct version of honggfuzz for CI (remove later)

* Improve value tests using PreciseNumber everywhere

* Fix ceiling

* Add comment for 0 tokens

* Fix ceiling div, add in constant price calc

* Revert ceiling division as template and use trait, too much boilerplate

* Run cargo fmt
This commit is contained in:
Jon Cinque 2021-01-15 18:17:27 +01:00 committed by GitHub
parent d050568afd
commit 40b7690a58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 662 additions and 175 deletions

View File

@ -9,6 +9,6 @@ set -x
cargo --version
cargo install rustfilt || true
cargo install honggfuzz --version=0.5.52 || true
cargo install honggfuzz --version=0.5.52 --force || true
cargo +"$rust_stable" build-bpf --version

View File

@ -65,7 +65,7 @@ let currentFeeAmount = 0;
// need to get slightly tweaked in the two cases.
const SWAP_AMOUNT_IN = 100000;
const SWAP_AMOUNT_OUT = SWAP_PROGRAM_OWNER_FEE_ADDRESS ? 90661 : 90674;
const SWAP_FEE = SWAP_PROGRAM_OWNER_FEE_ADDRESS ? 22272 : 22276;
const SWAP_FEE = SWAP_PROGRAM_OWNER_FEE_ADDRESS ? 22273 : 22276;
const HOST_SWAP_FEE = SWAP_PROGRAM_OWNER_FEE_ADDRESS
? Math.floor((SWAP_FEE * HOST_FEE_NUMERATOR) / HOST_FEE_DENOMINATOR)
: 0;
@ -362,10 +362,10 @@ export async function withdrawAllTokenTypes(): Promise<void> {
);
}
const poolTokenAmount = POOL_TOKEN_AMOUNT - feeAmount;
const tokenA = Math.ceil(
const tokenA = Math.floor(
(swapTokenA.amount.toNumber() * poolTokenAmount) / supply,
);
const tokenB = Math.ceil(
const tokenB = Math.floor(
(swapTokenB.amount.toNumber() * poolTokenAmount) / supply,
);

View File

@ -7,8 +7,10 @@ use spl_token_swap_fuzz::{
use spl_token_swap::{
curve::{
base::{CurveType, SwapCurve},
calculator::CurveCalculator,
constant_product::ConstantProductCurve,
fees::Fees,
math::PreciseNumber,
},
error::SwapError,
instruction::{
@ -102,9 +104,10 @@ fn run_fuzz_instructions(fuzz_instructions: Vec<FuzzInstruction>) {
host_fee_numerator,
host_fee_denominator,
};
let curve = ConstantProductCurve {};
let swap_curve = SwapCurve {
curve_type: CurveType::ConstantProduct,
calculator: Box::new(ConstantProductCurve {}),
calculator: Box::new(curve.clone()),
};
let mut token_swap = NativeTokenSwap::new(
fees,
@ -203,20 +206,29 @@ fn run_fuzz_instructions(fuzz_instructions: Vec<FuzzInstruction>) {
);
}
let pool_tokens = [&token_swap.pool_token_account, &token_swap.pool_fee_account]
.iter()
.map(|&x| get_token_balance(x))
.sum::<u64>() as u128;
// Omit fees intentionally, because fees in the form of pool tokens can
// dilute the value of the pool. For example, if we perform a small swap
// whose fee is worth less than 1 pool token, we may round up to 1 pool
// token and mint it as the fee. Depending on the size of the pool, this
// fee can actually reduce the value of pool tokens.
let pool_token_amount =
pool_tokens + pool_accounts.values().map(get_token_balance).sum::<u64>() as u128;
let swap_token_a_amount = get_token_balance(&token_swap.token_a_account) as u128;
let swap_token_b_amount = get_token_balance(&token_swap.token_b_account) as u128;
let lost_a_value = initial_swap_token_a_amount * pool_token_amount
> swap_token_a_amount * initial_pool_token_amount;
let lost_b_value = initial_swap_token_b_amount * pool_token_amount
> swap_token_b_amount * initial_pool_token_amount;
assert!(!(lost_a_value && lost_b_value));
let initial_pool_value = curve
.normalized_value(initial_swap_token_a_amount, initial_swap_token_b_amount)
.unwrap();
let pool_value = curve
.normalized_value(swap_token_a_amount, swap_token_b_amount)
.unwrap();
let pool_token_amount = PreciseNumber::new(pool_token_amount).unwrap();
let initial_pool_token_amount = PreciseNumber::new(initial_pool_token_amount).unwrap();
assert!(initial_pool_value
.checked_div(&initial_pool_token_amount)
.unwrap()
.less_than_or_equal(&pool_value.checked_div(&pool_token_amount).unwrap()));
// check total token a and b amounts
let after_total_token_a = token_a_accounts

View File

@ -51,5 +51,5 @@ pub fn transfer(
from.amount -= amount;
to.amount += amount;
TokenAccount::pack(from, &mut from_account.data[..]).unwrap();
TokenAccount::pack(to, &mut from_account.data[..]).unwrap();
TokenAccount::pack(to, &mut to_account.data[..]).unwrap();
}

View File

@ -6,7 +6,7 @@ use solana_program::{
};
use crate::curve::{
calculator::{CurveCalculator, SwapWithoutFeesResult, TradeDirection},
calculator::{CurveCalculator, RoundDirection, SwapWithoutFeesResult, TradeDirection},
constant_price::ConstantPriceCurve,
constant_product::ConstantProductCurve,
fees::Fees,
@ -101,6 +101,7 @@ impl SwapCurve {
}
/// Get the amount of pool tokens for the given amount of token A or B
#[allow(clippy::too_many_arguments)]
pub fn trading_tokens_to_pool_tokens(
&self,
source_amount: u128,
@ -108,14 +109,15 @@ impl SwapCurve {
swap_token_b_amount: u128,
pool_supply: u128,
trade_direction: TradeDirection,
round_direction: RoundDirection,
fees: &Fees,
) -> Option<u128> {
// Get the trading fee incurred if *half* the source amount is swapped
// for the other side. Reference at:
// https://github.com/balancer-labs/balancer-core/blob/f4ed5d65362a8d6cec21662fb6eae233b0babc1f/contracts/BMath.sol#L117
if source_amount == 0 {
return Some(0);
}
// Get the trading fee incurred if *half* the source amount is swapped
// for the other side. Reference at:
// https://github.com/balancer-labs/balancer-core/blob/f4ed5d65362a8d6cec21662fb6eae233b0babc1f/contracts/BMath.sol#L117
let half_source_amount = std::cmp::max(1, source_amount.checked_div(2)?);
let trade_fee = fees.trading_fee(half_source_amount)?;
let source_amount = source_amount.checked_sub(trade_fee)?;
@ -125,6 +127,7 @@ impl SwapCurve {
swap_token_b_amount,
pool_supply,
trade_direction,
round_direction,
)
}
}

View File

@ -1,9 +1,6 @@
//! Swap calculations
use crate::{
curve::math::{ceiling_division, PreciseNumber},
error::SwapError,
};
use crate::{curve::math::PreciseNumber, error::SwapError};
use std::fmt::Debug;
/// Initial amount of pool tokens for swap contract, hard-coded to something
@ -36,6 +33,17 @@ pub enum TradeDirection {
BtoA,
}
/// The direction to round. Used for pool token to trading token conversions to
/// avoid losing value on any deposit or withdrawal.
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RoundDirection {
/// Floor the value, ie. 1.9 => 1, 1.1 => 1, 1.5 => 1
Floor,
/// Ceiling the value, ie. 1.9 => 2, 1.1 => 2, 1.5 => 2
Ceiling,
}
impl TradeDirection {
/// Given a trade direction, gives the opposite direction of the trade, so
/// A to B becomes B to A, and vice versa
@ -92,29 +100,14 @@ pub trait CurveCalculator: Debug + DynPack {
/// Get the amount of trading tokens for the given amount of pool tokens,
/// provided the total trading tokens and supply of pool tokens.
///
/// The default implementation is a simple ratio calculation for how many
/// trading tokens correspond to a certain number of pool tokens
fn pool_tokens_to_trading_tokens(
&self,
pool_tokens: u128,
pool_token_supply: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
) -> Option<TradingTokenResult> {
let (token_a_amount, _) = ceiling_division(
pool_tokens.checked_mul(swap_token_a_amount)?,
pool_token_supply,
)?;
let (token_b_amount, _) = ceiling_division(
pool_tokens.checked_mul(swap_token_b_amount)?,
pool_token_supply,
)?;
Some(TradingTokenResult {
token_a_amount,
token_b_amount,
})
}
round_direction: RoundDirection,
) -> Option<TradingTokenResult>;
/// Get the amount of pool tokens for the given amount of token A or B.
///
@ -124,7 +117,7 @@ pub trait CurveCalculator: Debug + DynPack {
/// performed, this will change the spot price of the pool.
///
/// See more background for the calculation at:
/// https://balancer.finance/whitepaper/#single-asset-deposit
/// https://balancer.finance/whitepaper/#single-asset-deposit-withdrawal
fn trading_tokens_to_pool_tokens(
&self,
source_amount: u128,
@ -132,20 +125,8 @@ pub trait CurveCalculator: Debug + DynPack {
swap_token_b_amount: u128,
pool_supply: u128,
trade_direction: TradeDirection,
) -> 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)?;
pool_supply.checked_mul(&root)?.to_imprecise()
}
round_direction: RoundDirection,
) -> Option<u128>;
/// Validate that the given curve has no invalid parameters
fn validate(&self) -> Result<(), SwapError>;
@ -182,28 +163,19 @@ pub trait CurveCalculator: Debug + DynPack {
/// This is useful for testing the curves, to make sure that value is not
/// lost on any trade. It can also be used to find out the relative value
/// of pool tokens or liquidity tokens.
///
/// The default implementation for this function gives the square root of
/// the Uniswap invariant.
fn normalized_value(
&self,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
) -> Option<u128> {
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()?
.to_imprecise()
}
) -> Option<PreciseNumber>;
}
/// Test helpers for curves
#[cfg(any(test, fuzzing))]
#[cfg(test)]
pub mod test {
use super::*;
use crate::curve::math::U256;
use proptest::prelude::*;
/// The epsilon for most curves when performing the conversion test,
/// comparing a one-sided deposit to a swap + deposit.
@ -248,6 +220,7 @@ pub mod test {
swap_token_b_amount,
pool_supply,
trade_direction,
RoundDirection::Floor,
)
.unwrap();
@ -269,6 +242,7 @@ pub mod test {
swap_token_b_amount,
pool_supply,
trade_direction,
RoundDirection::Floor,
)
.unwrap();
let pool_tokens_from_destination = curve
@ -278,6 +252,7 @@ pub mod test {
swap_token_b_amount,
pool_supply + pool_tokens_from_source,
opposite_direction,
RoundDirection::Floor,
)
.unwrap();
@ -343,10 +318,14 @@ pub mod test {
let new_value = curve
.normalized_value(swap_token_a_amount, swap_token_b_amount)
.unwrap();
assert!(new_value >= previous_value);
assert!(new_value.greater_than_or_equal(&previous_value));
let epsilon = 1; // Extremely close!
let difference = new_value - previous_value;
let difference = new_value
.checked_sub(&previous_value)
.unwrap()
.to_imprecise()
.unwrap();
assert!(difference <= epsilon);
}
@ -369,6 +348,7 @@ pub mod test {
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
RoundDirection::Ceiling,
)
.unwrap();
let new_swap_token_a_amount = swap_token_a_amount + deposit_result.token_a_amount;
@ -399,4 +379,61 @@ pub mod test {
>= swap_token_b_amount * new_pool_token_supply
);
}
/// Test function checking that a withdraw never reduces the value of pool
/// tokens.
///
/// Since curve calculations use unsigned integers, there is potential for
/// truncation at some point, meaning a potential for value to be lost if
/// too much is given to the depositor.
pub fn check_pool_value_from_withdraw(
curve: &dyn CurveCalculator,
pool_token_amount: u128,
pool_token_supply: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
) {
let withdraw_result = curve
.pool_tokens_to_trading_tokens(
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
RoundDirection::Floor,
)
.unwrap();
let new_swap_token_a_amount = swap_token_a_amount - withdraw_result.token_a_amount;
let new_swap_token_b_amount = swap_token_b_amount - withdraw_result.token_b_amount;
let new_pool_token_supply = pool_token_supply - pool_token_amount;
let value = curve
.normalized_value(swap_token_a_amount, swap_token_b_amount)
.unwrap();
// since we can get rounding issues on the pool value which make it seem that the
// value per token has gone down, we bump it up by an epsilon of 1 to
// cover all cases
let new_value = curve
.normalized_value(new_swap_token_a_amount, new_swap_token_b_amount)
.unwrap();
// the following inequality must hold:
// new_pool_value / new_pool_token_supply >= pool_value / pool_token_supply
// which can also be written:
// new_pool_value * pool_token_supply >= pool_value * new_pool_token_supply
let pool_token_supply = PreciseNumber::new(pool_token_supply).unwrap();
let new_pool_token_supply = PreciseNumber::new(new_pool_token_supply).unwrap();
assert!(new_value
.checked_mul(&pool_token_supply)
.unwrap()
.greater_than_or_equal(&value.checked_mul(&new_pool_token_supply).unwrap()));
}
prop_compose! {
pub fn total_and_intermediate()(total in 1..u64::MAX)
(intermediate in 1..total, total in Just(total))
-> (u64, u64) {
(total, intermediate)
}
}
}

View File

@ -2,10 +2,10 @@
use crate::{
curve::calculator::{
map_zero_to_none, CurveCalculator, DynPack, SwapWithoutFeesResult, TradeDirection,
TradingTokenResult,
map_zero_to_none, CurveCalculator, DynPack, RoundDirection, SwapWithoutFeesResult,
TradeDirection, TradingTokenResult,
},
curve::math::{ceiling_division, U256},
curve::math::{CheckedCeilDiv, PreciseNumber, U256},
error::SwapError,
};
use arrayref::{array_mut_ref, array_ref};
@ -67,18 +67,36 @@ impl CurveCalculator for ConstantPriceCurve {
pool_token_supply: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
round_direction: RoundDirection,
) -> Option<TradingTokenResult> {
let token_b_price = self.token_b_price as u128;
let total_value = self.normalized_value(swap_token_a_amount, swap_token_b_amount)?;
let total_value = self
.normalized_value(swap_token_a_amount, swap_token_b_amount)?
.to_imprecise()?;
let (token_a_amount, _) =
ceiling_division(pool_tokens.checked_mul(total_value)?, pool_token_supply)?;
let (token_b_amount, _) = ceiling_division(
pool_tokens
.checked_mul(total_value)?
.checked_div(token_b_price)?,
pool_token_supply,
)?;
let (token_a_amount, token_b_amount) = match round_direction {
RoundDirection::Floor => {
let token_a_amount = pool_tokens
.checked_mul(total_value)?
.checked_div(pool_token_supply)?;
let token_b_amount = pool_tokens
.checked_mul(total_value)?
.checked_div(token_b_price)?
.checked_div(pool_token_supply)?;
(token_a_amount, token_b_amount)
}
RoundDirection::Ceiling => {
let (token_a_amount, _) = pool_tokens
.checked_mul(total_value)?
.checked_ceil_div(pool_token_supply)?;
let (pool_value_as_token_b, _) = pool_tokens
.checked_mul(total_value)?
.checked_ceil_div(token_b_price)?;
let (token_b_amount, _) =
pool_value_as_token_b.checked_ceil_div(pool_token_supply)?;
(token_a_amount, token_b_amount)
}
};
Some(TradingTokenResult {
token_a_amount,
token_b_amount,
@ -95,6 +113,7 @@ impl CurveCalculator for ConstantPriceCurve {
swap_token_b_amount: u128,
pool_supply: u128,
trade_direction: TradeDirection,
round_direction: RoundDirection,
) -> Option<u128> {
let token_b_price = U256::from(self.token_b_price);
let given_value = match trade_direction {
@ -105,12 +124,21 @@ impl CurveCalculator for ConstantPriceCurve {
.checked_mul(token_b_price)?
.checked_add(U256::from(swap_token_a_amount))?;
let pool_supply = U256::from(pool_supply);
Some(
pool_supply
.checked_mul(given_value)?
.checked_div(total_value)?
.as_u128(),
)
match round_direction {
RoundDirection::Floor => Some(
pool_supply
.checked_mul(given_value)?
.checked_div(total_value)?
.as_u128(),
),
RoundDirection::Ceiling => Some(
pool_supply
.checked_mul(given_value)?
.checked_ceil_div(total_value)?
.0
.as_u128(),
),
}
}
fn validate(&self) -> Result<(), SwapError> {
@ -141,20 +169,21 @@ impl CurveCalculator for ConstantPriceCurve {
&self,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
) -> Option<u128> {
) -> Option<PreciseNumber> {
let swap_token_b_value = swap_token_b_amount.checked_mul(self.token_b_price as u128)?;
// special logic in case we're close to the limits, avoid overflowing u128
if swap_token_b_value.saturating_sub(std::u64::MAX.into())
let value = if swap_token_b_value.saturating_sub(std::u64::MAX.into())
> (std::u128::MAX.saturating_sub(std::u64::MAX.into()))
{
swap_token_b_value
.checked_div(2)?
.checked_add(swap_token_a_amount.checked_div(2)?)
.checked_add(swap_token_a_amount.checked_div(2)?)?
} else {
swap_token_a_amount
.checked_add(swap_token_b_value)?
.checked_div(2)
}
.checked_div(2)?
};
PreciseNumber::new(value)
}
}
@ -191,7 +220,7 @@ mod tests {
use super::*;
use crate::curve::calculator::{
test::{
check_curve_value_from_swap, check_pool_token_conversion,
check_curve_value_from_swap, check_pool_token_conversion, total_and_intermediate,
CONVERSION_BASIS_POINTS_GUARANTEE,
},
INITIAL_SWAP_POOL_AMOUNT,
@ -447,13 +476,14 @@ mod tests {
let value = curve.normalized_value(swap_token_a_amount, swap_token_b_amount).unwrap();
// Make sure we trade at least one of each token
prop_assume!(pool_token_amount * value >= 2 * token_b_price * pool_token_supply);
prop_assume!(pool_token_amount * value.to_imprecise().unwrap() >= 2 * token_b_price * pool_token_supply);
let deposit_result = curve
.pool_tokens_to_trading_tokens(
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
RoundDirection::Ceiling,
)
.unwrap();
let new_swap_token_a_amount = swap_token_a_amount + deposit_result.token_a_amount;
@ -467,15 +497,60 @@ mod tests {
// which reduces to:
// new_value * pool_token_supply >= value * new_pool_token_supply
// These numbers can be just slightly above u64 after the deposit, which
// means that their multiplication can be just above the range of u128.
// For ease of testing, we bump these up to U256.
let pool_token_supply = U256::from(pool_token_supply);
let new_pool_token_supply = U256::from(new_pool_token_supply);
let value = U256::from(value);
let new_value = U256::from(new_value);
let pool_token_supply = PreciseNumber::new(pool_token_supply).unwrap();
let new_pool_token_supply = PreciseNumber::new(new_pool_token_supply).unwrap();
//let value = U256::from(value);
//let new_value = U256::from(new_value);
assert!(new_value * pool_token_supply >= value * new_pool_token_supply);
assert!(new_value.checked_mul(&pool_token_supply).unwrap().greater_than_or_equal(&value.checked_mul(&new_pool_token_supply).unwrap()));
}
}
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..u32::MAX, // kept small to avoid proptest rejections
token_b_price in 1..u32::MAX, // kept small to avoid proptest rejections
) {
let curve = ConstantPriceCurve { token_b_price: token_b_price as u64 };
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;
let token_b_price = token_b_price as u128;
let value = curve.normalized_value(swap_token_a_amount, swap_token_b_amount).unwrap();
// Make sure we trade at least one of each token
prop_assume!(pool_token_amount * value.to_imprecise().unwrap() >= 2 * token_b_price * pool_token_supply);
prop_assume!(pool_token_amount <= pool_token_supply);
let withdraw_result = curve
.pool_tokens_to_trading_tokens(
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
RoundDirection::Floor,
)
.unwrap();
prop_assume!(withdraw_result.token_a_amount <= swap_token_a_amount);
prop_assume!(withdraw_result.token_b_amount <= swap_token_b_amount);
let new_swap_token_a_amount = swap_token_a_amount - withdraw_result.token_a_amount;
let new_swap_token_b_amount = swap_token_b_amount - withdraw_result.token_b_amount;
let new_pool_token_supply = pool_token_supply - pool_token_amount;
let new_value = curve.normalized_value(new_swap_token_a_amount, new_swap_token_b_amount).unwrap();
// the following inequality must hold:
// new_value / new_pool_token_supply >= value / pool_token_supply
// which reduces to:
// new_value * pool_token_supply >= value * new_pool_token_supply
let pool_token_supply = PreciseNumber::new(pool_token_supply).unwrap();
let new_pool_token_supply = PreciseNumber::new(new_pool_token_supply).unwrap();
assert!(new_value.checked_mul(&pool_token_supply).unwrap().greater_than_or_equal(&value.checked_mul(&new_pool_token_supply).unwrap()));
}
}
}

View File

@ -7,9 +7,10 @@ use solana_program::{
use crate::{
curve::calculator::{
map_zero_to_none, CurveCalculator, DynPack, SwapWithoutFeesResult, TradeDirection,
map_zero_to_none, CurveCalculator, DynPack, RoundDirection, SwapWithoutFeesResult,
TradeDirection, TradingTokenResult,
},
curve::math::ceiling_division,
curve::math::{CheckedCeilDiv, PreciseNumber},
error::SwapError,
};
@ -31,7 +32,7 @@ pub fn swap(
let new_swap_source_amount = swap_source_amount.checked_add(source_amount)?;
let (new_swap_destination_amount, new_swap_source_amount) =
ceiling_division(invariant, 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 =
@ -43,6 +44,100 @@ pub fn swap(
})
}
/// Get the amount of trading tokens for the given amount of pool tokens,
/// provided the total trading tokens and supply of pool tokens.
///
/// The constant product implementation is a simple ratio calculation for how many
/// trading tokens correspond to a certain number of pool tokens
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)?;
// Also check for 0 token A and B amount to avoid taking too much
// for tiny amounts of pool tokens. For example, if someone asks
// for 1 pool token, which is worth 0.01 token A, we avoid the
// ceiling of taking 1 token A and instead return 0, for it to be
// rejected later in processing.
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.
///
/// The constant product implementation uses the Balancer formulas found at
/// https://balancer.finance/whitepaper/#single-asset-deposit, specifically
/// in the case for 2 tokens, each weighted at 1/2.
pub fn trading_tokens_to_pool_tokens(
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(),
}
}
/// Calculates the total normalized value of the curve given the liquidity
/// parameters.
///
/// The constant product implementation for this function gives the square root of
/// the Uniswap invariant.
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 {
/// Constant product swap ensures x * y = constant
fn swap_without_fees(
@ -55,6 +150,53 @@ impl CurveCalculator for ConstantProductCurve {
swap(source_amount, swap_source_amount, swap_destination_amount)
}
/// The constant product implementation is a simple ratio calculation for how many
/// trading tokens correspond to a certain number of pool tokens
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,
)
}
/// Get the amount of pool tokens for the given amount of token A or B.
fn trading_tokens_to_pool_tokens(
&self,
source_amount: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
pool_supply: u128,
trade_direction: TradeDirection,
round_direction: RoundDirection,
) -> Option<u128> {
trading_tokens_to_pool_tokens(
source_amount,
swap_token_a_amount,
swap_token_b_amount,
pool_supply,
trade_direction,
round_direction,
)
}
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(())
}
@ -88,9 +230,10 @@ mod tests {
use crate::curve::calculator::{
test::{
check_curve_value_from_swap, check_pool_token_conversion,
check_pool_value_from_deposit, CONVERSION_BASIS_POINTS_GUARANTEE,
check_pool_value_from_deposit, check_pool_value_from_withdraw, total_and_intermediate,
CONVERSION_BASIS_POINTS_GUARANTEE,
},
INITIAL_SWAP_POOL_AMOUNT,
RoundDirection, INITIAL_SWAP_POOL_AMOUNT,
};
use proptest::prelude::*;
@ -110,7 +253,13 @@ mod tests {
) {
let calculator = ConstantProductCurve {};
let results = calculator
.pool_tokens_to_trading_tokens(deposit, supply, token_a, token_b)
.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);
@ -126,9 +275,11 @@ mod tests {
#[test]
fn fail_trading_token_conversion() {
let calculator = ConstantProductCurve {};
let results = calculator.pool_tokens_to_trading_tokens(5, 10, u128::MAX, 0);
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);
let results =
calculator.pool_tokens_to_trading_tokens(5, 10, 0, u128::MAX, RoundDirection::Floor);
assert!(results.is_none());
}
@ -290,4 +441,30 @@ mod tests {
);
}
}
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;
// 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 = ConstantProductCurve {};
check_pool_value_from_withdraw(
&curve,
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
);
}
}
}

View File

@ -58,28 +58,63 @@ impl U256 {
///
/// This calculation fails if the divisor is larger than the dividend, to avoid
/// having a result like: 1 / 1000 = 1.
pub fn ceiling_division(dividend: u128, mut divisor: u128) -> Option<(u128, u128)> {
let mut quotient = dividend.checked_div(divisor)?;
// Avoid dividing a small number by a big one and returning 1, and instead
// fail.
if quotient == 0 {
return None;
}
pub trait CheckedCeilDiv: Sized {
/// Perform ceiling division
fn checked_ceil_div(&self, rhs: Self) -> Option<(Self, Self)>;
}
// Ceiling the destination amount if there's any remainder, which will
// almost always be the case.
let remainder = dividend.checked_rem(divisor)?;
if remainder > 0 {
quotient = quotient.checked_add(1)?;
// calculate the minimum amount needed to get the dividend amount to
// avoid truncating too much
divisor = dividend.checked_div(quotient)?;
let remainder = dividend.checked_rem(quotient)?;
if remainder > 0 {
divisor = divisor.checked_add(1)?;
impl CheckedCeilDiv for u128 {
fn checked_ceil_div(&self, mut rhs: Self) -> Option<(Self, Self)> {
let mut quotient = self.checked_div(rhs)?;
// Avoid dividing a small number by a big one and returning 1, and instead
// fail.
if quotient == 0 {
return None;
}
// Ceiling the destination amount if there's any remainder, which will
// almost always be the case.
let remainder = self.checked_rem(rhs)?;
if remainder > 0 {
quotient = quotient.checked_add(1)?;
// calculate the minimum amount needed to get the dividend amount to
// avoid truncating too much
rhs = self.checked_div(quotient)?;
let remainder = self.checked_rem(quotient)?;
if remainder > 0 {
rhs = rhs.checked_add(1)?;
}
}
Some((quotient, rhs))
}
}
impl CheckedCeilDiv for U256 {
fn checked_ceil_div(&self, mut rhs: Self) -> Option<(Self, Self)> {
let mut quotient = self.checked_div(rhs)?;
let zero = U256::from(0);
let one = U256::from(1);
// Avoid dividing a small number by a big one and returning 1, and instead
// fail.
if quotient == zero {
return None;
}
// Ceiling the destination amount if there's any remainder, which will
// almost always be the case.
let remainder = self.checked_rem(rhs)?;
if remainder > zero {
quotient = quotient.checked_add(one)?;
// calculate the minimum amount needed to get the dividend amount to
// avoid truncating too much
rhs = self.checked_div(quotient)?;
let remainder = self.checked_rem(quotient)?;
if remainder > zero {
rhs = rhs.checked_add(one)?;
}
}
Some((quotient, rhs))
}
Some((quotient, divisor))
}
/// The representation of the number one as a precise number as 10^12
@ -201,6 +236,16 @@ impl PreciseNumber {
Some(Self { value })
}
/// Ceiling a precise value to a precision of ONE
pub fn ceiling(&self) -> Option<Self> {
let value = self
.value
.checked_add(one().checked_sub(U256::from(1))?)?
.checked_div(one())?
.checked_mul(one())?;
Some(Self { value })
}
/// Performs a checked division on two precise numbers
pub fn checked_div(&self, rhs: &Self) -> Option<Self> {
if *rhs == Self::zero() {
@ -610,6 +655,28 @@ mod tests {
}
}
#[test]
fn test_floor() {
let whole_number = PreciseNumber::new(2).unwrap();
let mut decimal_number = PreciseNumber::new(2).unwrap();
decimal_number.value += U256::from(1);
let floor = decimal_number.floor().unwrap();
let floor_again = floor.floor().unwrap();
assert_eq!(whole_number.value, floor.value);
assert_eq!(whole_number.value, floor_again.value);
}
#[test]
fn test_ceiling() {
let whole_number = PreciseNumber::new(2).unwrap();
let mut decimal_number = PreciseNumber::new(2).unwrap();
decimal_number.value -= U256::from(1);
let ceiling = decimal_number.ceiling().unwrap();
let ceiling_again = ceiling.ceiling().unwrap();
assert_eq!(whole_number.value, ceiling.value);
assert_eq!(whole_number.value, ceiling_again.value);
}
proptest! {
#[test]
fn test_square_root(a in 0..u128::MAX) {

View File

@ -3,10 +3,13 @@
use crate::{
curve::{
calculator::{
CurveCalculator, DynPack, SwapWithoutFeesResult, TradeDirection, TradingTokenResult,
CurveCalculator, DynPack, RoundDirection, SwapWithoutFeesResult, TradeDirection,
TradingTokenResult,
},
constant_product::swap,
math::{ceiling_division, PreciseNumber},
constant_product::{
normalized_value, pool_tokens_to_trading_tokens, swap, trading_tokens_to_pool_tokens,
},
math::PreciseNumber,
},
error::SwapError,
};
@ -58,20 +61,16 @@ impl CurveCalculator for OffsetCurve {
pool_token_supply: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
round_direction: RoundDirection,
) -> Option<TradingTokenResult> {
let token_b_offset = self.token_b_offset as u128;
let (token_a_amount, _) = ceiling_division(
pool_tokens.checked_mul(swap_token_a_amount)?,
pool_tokens_to_trading_tokens(
pool_tokens,
pool_token_supply,
)?;
let (token_b_amount, _) = ceiling_division(
pool_tokens.checked_mul(swap_token_b_amount.checked_add(token_b_offset)?)?,
pool_token_supply,
)?;
Some(TradingTokenResult {
token_a_amount,
token_b_amount,
})
swap_token_a_amount,
swap_token_b_amount.checked_add(token_b_offset)?,
round_direction,
)
}
/// Get the amount of pool tokens for the given amount of token A and B,
@ -83,20 +82,17 @@ impl CurveCalculator for OffsetCurve {
swap_token_b_amount: u128,
pool_supply: u128,
trade_direction: TradeDirection,
round_direction: RoundDirection,
) -> Option<u128> {
let token_b_offset = self.token_b_offset as u128;
let swap_source_amount = match trade_direction {
TradeDirection::AtoB => swap_token_a_amount,
TradeDirection::BtoA => swap_token_b_amount.checked_add(token_b_offset)?,
};
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)?;
pool_supply.checked_mul(&root)?.to_imprecise()
trading_tokens_to_pool_tokens(
source_amount,
swap_token_a_amount,
swap_token_b_amount.checked_add(token_b_offset)?,
pool_supply,
trade_direction,
round_direction,
)
}
fn validate(&self) -> Result<(), SwapError> {
@ -129,14 +125,12 @@ impl CurveCalculator for OffsetCurve {
&self,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
) -> Option<u128> {
let swap_token_a_amount = PreciseNumber::new(swap_token_a_amount)?;
let swap_token_b_amount =
PreciseNumber::new(swap_token_b_amount.checked_add(self.token_b_offset as u128)?)?;
swap_token_a_amount
.checked_mul(&swap_token_b_amount)?
.sqrt()?
.to_imprecise()
) -> Option<PreciseNumber> {
let token_b_offset = self.token_b_offset as u128;
normalized_value(
swap_token_a_amount,
swap_token_b_amount.checked_add(token_b_offset)?,
)
}
}
@ -174,7 +168,8 @@ mod tests {
use crate::curve::calculator::{
test::{
check_curve_value_from_swap, check_pool_token_conversion,
check_pool_value_from_deposit, CONVERSION_BASIS_POINTS_GUARANTEE,
check_pool_value_from_deposit, check_pool_value_from_withdraw, total_and_intermediate,
CONVERSION_BASIS_POINTS_GUARANTEE,
},
INITIAL_SWAP_POOL_AMOUNT,
};
@ -289,6 +284,14 @@ mod tests {
assert_eq!(result.destination_amount_swapped, 499);
}
prop_compose! {
pub fn values_sum_within_u64()(total in 1..u64::MAX)
(amount in 1..total, total in Just(total))
-> (u64, u64) {
(total - amount, amount)
}
}
proptest! {
#[test]
fn pool_token_conversion_a_to_b(
@ -437,8 +440,7 @@ mod tests {
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,
token_b_offset in 1..u64::MAX,
(swap_token_b_amount, token_b_offset) in values_sum_within_u64(),
) {
let curve = OffsetCurve { token_b_offset };
let pool_token_amount = pool_token_amount as u128;
@ -447,9 +449,6 @@ mod tests {
let swap_token_b_amount = swap_token_b_amount as u128;
let token_b_offset = token_b_offset as u128;
// For ease of calc, make sure the token B side stays within u64
prop_assume!(token_b_offset + swap_token_b_amount <= u64::MAX.into());
// 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);
@ -463,4 +462,45 @@ mod tests {
);
}
}
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, token_b_offset) in values_sum_within_u64(),
) {
let curve = OffsetCurve { token_b_offset };
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;
let token_b_offset = token_b_offset 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 + token_b_offset) / pool_token_supply >= 1);
// make sure we don't overdraw from either side
let withdraw_result = curve
.pool_tokens_to_trading_tokens(
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
RoundDirection::Floor,
)
.unwrap();
prop_assume!(withdraw_result.token_b_amount <= swap_token_b_amount); // avoid overdrawing to 0 for calc
check_pool_value_from_withdraw(
&curve,
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
);
}
}
}

View File

@ -6,7 +6,16 @@ use solana_program::{
program_pack::{IsInitialized, Pack, Sealed},
};
use crate::curve::calculator::{CurveCalculator, DynPack, SwapWithoutFeesResult, TradeDirection};
use crate::curve::{
calculator::{
CurveCalculator, DynPack, RoundDirection, SwapWithoutFeesResult, TradeDirection,
TradingTokenResult,
},
constant_product::{
normalized_value, pool_tokens_to_trading_tokens, trading_tokens_to_pool_tokens,
},
math::PreciseNumber,
};
use arrayref::{array_mut_ref, array_ref};
use std::convert::TryFrom;
@ -136,6 +145,51 @@ impl CurveCalculator for StableCurve {
})
}
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,
)
}
/// Get the amount of pool tokens for the given amount of token A or B.
fn trading_tokens_to_pool_tokens(
&self,
source_amount: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
pool_supply: u128,
trade_direction: TradeDirection,
round_direction: RoundDirection,
) -> Option<u128> {
trading_tokens_to_pool_tokens(
source_amount,
swap_token_a_amount,
swap_token_b_amount,
pool_supply,
trade_direction,
round_direction,
)
}
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> {
// TODO are all amps valid?
Ok(())
@ -173,7 +227,7 @@ impl DynPack for StableCurve {
#[cfg(test)]
mod tests {
use super::*;
use crate::curve::calculator::INITIAL_SWAP_POOL_AMOUNT;
use crate::curve::calculator::{RoundDirection, INITIAL_SWAP_POOL_AMOUNT};
use proptest::prelude::*;
use sim::StableSwapModel;
@ -195,7 +249,13 @@ mod tests {
let amp = 1;
let calculator = StableCurve { amp };
let results = calculator
.pool_tokens_to_trading_tokens(deposit, supply, token_a, token_b)
.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);
@ -212,9 +272,11 @@ mod tests {
fn fail_trading_token_conversion() {
let amp = 1;
let calculator = StableCurve { amp };
let results = calculator.pool_tokens_to_trading_tokens(5, 10, u128::MAX, 0);
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);
let results =
calculator.pool_tokens_to_trading_tokens(5, 10, 0, u128::MAX, RoundDirection::Floor);
assert!(results.is_none());
}

View File

@ -2,7 +2,11 @@
use crate::constraints::{SwapConstraints, SWAP_CONSTRAINTS};
use crate::{
curve::{base::SwapCurve, calculator::TradeDirection, fees::Fees},
curve::{
base::SwapCurve,
calculator::{RoundDirection, TradeDirection},
fees::Fees,
},
error::SwapError,
instruction::{
DepositAllTokenTypes, DepositSingleTokenTypeExactAmountIn, Initialize, Swap,
@ -429,6 +433,7 @@ impl Processor {
swap_token_b_amount,
to_u128(pool_mint.supply)?,
trade_direction,
RoundDirection::Ceiling,
&token_swap.fees,
)
.ok_or(SwapError::FeeCalculationFailure)?;
@ -538,6 +543,7 @@ impl Processor {
pool_mint_supply,
to_u128(token_a.amount)?,
to_u128(token_b.amount)?,
RoundDirection::Ceiling,
)
.ok_or(SwapError::ZeroTradingTokens)?;
let token_a_amount = to_u64(results.token_a_amount)?;
@ -649,6 +655,7 @@ impl Processor {
to_u128(pool_mint.supply)?,
to_u128(token_a.amount)?,
to_u128(token_b.amount)?,
RoundDirection::Floor,
)
.ok_or(SwapError::ZeroTradingTokens)?;
let token_a_amount = to_u64(results.token_a_amount)?;
@ -777,6 +784,7 @@ impl Processor {
to_u128(swap_token_b.amount)?,
pool_mint_supply,
trade_direction,
RoundDirection::Floor,
&token_swap.fees,
)
.ok_or(SwapError::ZeroTradingTokens)?;
@ -910,6 +918,7 @@ impl Processor {
swap_token_b_amount,
pool_mint_supply,
trade_direction,
RoundDirection::Ceiling,
&token_swap.fees,
)
.ok_or(SwapError::ZeroTradingTokens)?;
@ -4142,6 +4151,7 @@ mod tests {
pool_mint.supply.try_into().unwrap(),
swap_token_a.amount.try_into().unwrap(),
swap_token_b.amount.try_into().unwrap(),
RoundDirection::Floor,
)
.unwrap();
assert_eq!(
@ -4220,6 +4230,7 @@ mod tests {
pool_mint.supply.try_into().unwrap(),
swap_token_a.amount.try_into().unwrap(),
swap_token_b.amount.try_into().unwrap(),
RoundDirection::Floor,
)
.unwrap();
let token_a = spl_token::state::Account::unpack(&token_a_account.data).unwrap();
@ -5322,6 +5333,7 @@ mod tests {
swap_token_b.amount.try_into().unwrap(),
pool_mint.supply.try_into().unwrap(),
TradeDirection::AtoB,
RoundDirection::Ceiling,
&accounts.fees,
)
.unwrap();
@ -5494,6 +5506,7 @@ mod tests {
token_b_amount.try_into().unwrap(),
initial_supply.try_into().unwrap(),
TradeDirection::AtoB,
RoundDirection::Ceiling,
&fees,
)
.unwrap();
@ -5570,6 +5583,7 @@ mod tests {
token_b_amount.try_into().unwrap(),
initial_supply.try_into().unwrap(),
TradeDirection::BtoA,
RoundDirection::Ceiling,
&fees,
)
.unwrap();