token-swap: Make single token withdrawal fair with better calcs (#1794)

* Separate deposit and withdraw single side calcs

* token-swap: Make single token withdrawal fair with better calcs

* Fix JS test calcs
This commit is contained in:
Jon Cinque 2021-05-25 22:05:06 +02:00 committed by GitHub
parent 6b3fbb8ff5
commit 5f692a0b61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 536 additions and 130 deletions

View File

@ -59,7 +59,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 ? 22273 : 22276;
const SWAP_FEE = SWAP_PROGRAM_OWNER_FEE_ADDRESS ? 22273 : 22277;
const HOST_SWAP_FEE = SWAP_PROGRAM_OWNER_FEE_ADDRESS
? Math.floor((SWAP_FEE * HOST_FEE_NUMERATOR) / HOST_FEE_DENOMINATOR)
: 0;

View File

@ -7,7 +7,7 @@ use spl_token_swap_fuzz::{
use spl_token_swap::{
curve::{
base::{CurveType, SwapCurve},
calculator::CurveCalculator,
calculator::{CurveCalculator, TradeDirection},
constant_product::ConstantProductCurve,
fees::Fees,
},
@ -60,13 +60,6 @@ enum FuzzInstruction {
},
}
/// Helper enum to tell which direction a swap is meant to go.
#[derive(Debug, Arbitrary, Clone)]
enum TradeDirection {
AtoB,
BtoA,
}
/// Use u8 as an account id to simplify the address space and re-use accounts
/// more often.
type AccountId = u8;

View File

@ -6,7 +6,7 @@ use solana_program::{
};
use crate::curve::{
calculator::{CurveCalculator, RoundDirection, SwapWithoutFeesResult, TradeDirection},
calculator::{CurveCalculator, SwapWithoutFeesResult, TradeDirection},
constant_price::ConstantPriceCurve,
constant_product::ConstantProductCurve,
fees::Fees,
@ -17,7 +17,11 @@ use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
#[cfg(feature = "fuzz")]
use arbitrary::Arbitrary;
/// Curve types supported by the token-swap program.
#[cfg_attr(feature = "fuzz", derive(Arbitrary))]
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CurveType {
@ -100,16 +104,14 @@ 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(
/// Get the amount of pool tokens for the deposited amount of token A or B
pub 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,
round_direction: RoundDirection,
fees: &Fees,
) -> Option<u128> {
if source_amount == 0 {
@ -121,13 +123,40 @@ impl SwapCurve {
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)?;
self.calculator.trading_tokens_to_pool_tokens(
self.calculator.deposit_single_token_type(
source_amount,
swap_token_a_amount,
swap_token_b_amount,
pool_supply,
trade_direction,
)
}
/// Get the amount of pool tokens for the withdrawn amount of token A or B
pub 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,
fees: &Fees,
) -> Option<u128> {
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)?;
self.calculator.withdraw_single_token_type_exact_out(
source_amount,
swap_token_a_amount,
swap_token_b_amount,
pool_supply,
trade_direction,
round_direction,
)
}
}

View File

@ -2,6 +2,9 @@
use {crate::error::SwapError, spl_math::precise_number::PreciseNumber, std::fmt::Debug};
#[cfg(feature = "fuzz")]
use arbitrary::Arbitrary;
/// Initial amount of pool tokens for swap contract, hard-coded to something
/// "sensible" given a maximum of u128.
/// Note that on Ethereum, Uniswap uses the geometric mean of all provided
@ -23,6 +26,7 @@ pub fn map_zero_to_none(x: u128) -> Option<u128> {
/// The direction of a trade, since curves can be specialized to treat each
/// token differently (by adding offsets or weights)
#[cfg_attr(feature = "fuzz", derive(Arbitrary))]
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TradeDirection {
@ -108,23 +112,39 @@ pub trait CurveCalculator: Debug + DynPack {
round_direction: RoundDirection,
) -> Option<TradingTokenResult>;
/// Get the amount of pool tokens for the given amount of token A or B.
/// Get the amount of pool tokens for the deposited amount of token A or B.
///
/// This is used for single-sided deposits or withdrawals and owner trade
/// fee calculation. It essentially performs a swap followed by a deposit,
/// or a withdrawal followed by a swap. Because a swap is implicitly
/// performed, this will change the spot price of the pool.
/// This is used for single-sided deposits. It essentially performs a swap
/// followed by a deposit. Because a swap is implicitly performed, this will
/// change the spot price of the pool.
///
/// See more background for the calculation at:
/// https://balancer.finance/whitepaper/#single-asset-deposit-withdrawal
fn trading_tokens_to_pool_tokens(
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>;
/// Get the amount of pool tokens for the withdrawn amount of token A or B.
///
/// This is used for single-sided withdrawals and owner trade fee
/// calculation. It essentially performs a withdrawal followed by a swap.
/// Because a swap is implicitly performed, this will change the spot price
/// of the pool.
///
/// See more background for the calculation at:
/// https://balancer.finance/whitepaper/#single-asset-deposit-withdrawal
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,
round_direction: RoundDirection,
) -> Option<u128>;
/// Validate that the given curve has no invalid parameters
@ -187,7 +207,7 @@ pub mod test {
/// We guarantee that the relative error between depositing one side and
/// performing a swap plus deposit will be at most some epsilon provided by
/// the curve. Most curves guarantee accuracy within 0.5%.
pub fn check_pool_token_conversion(
pub fn check_deposit_token_conversion(
curve: &dyn CurveCalculator,
source_token_amount: u128,
swap_source_amount: u128,
@ -213,13 +233,12 @@ pub mod test {
// base amount
let pool_tokens_from_one_side = curve
.trading_tokens_to_pool_tokens(
.deposit_single_token_type(
source_token_amount,
swap_token_a_amount,
swap_token_b_amount,
pool_supply,
trade_direction,
RoundDirection::Floor,
)
.unwrap();
@ -235,23 +254,21 @@ pub mod test {
),
};
let pool_tokens_from_source = curve
.trading_tokens_to_pool_tokens(
.deposit_single_token_type(
source_token_amount - results.source_amount_swapped,
swap_token_a_amount,
swap_token_b_amount,
pool_supply,
trade_direction,
RoundDirection::Floor,
)
.unwrap();
let pool_tokens_from_destination = curve
.trading_tokens_to_pool_tokens(
.deposit_single_token_type(
results.destination_amount_swapped,
swap_token_a_amount,
swap_token_b_amount,
pool_supply + pool_tokens_from_source,
opposite_direction,
RoundDirection::Floor,
)
.unwrap();
@ -275,6 +292,90 @@ pub mod test {
);
}
/// Test function to check that withdrawing token A is the same as withdrawing
/// both and swapping one side.
/// Since calculations use unsigned integers, there will be truncation at
/// some point, meaning we can't have perfect equality.
/// We guarantee that the relative error between withdrawing one side and
/// performing a withdraw plus a swap will be at most some epsilon provided by
/// the curve. Most curves guarantee accuracy within 0.5%.
pub fn check_withdraw_token_conversion(
curve: &dyn CurveCalculator,
pool_token_amount: u128,
pool_token_supply: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
trade_direction: TradeDirection,
epsilon_in_basis_points: u128,
) {
// withdraw the pool tokens
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;
// swap one side of them
let source_token_amount = match trade_direction {
TradeDirection::AtoB => {
let results = curve
.swap_without_fees(
withdraw_result.token_a_amount,
new_swap_token_a_amount,
new_swap_token_b_amount,
trade_direction,
)
.unwrap();
withdraw_result.token_b_amount + results.destination_amount_swapped
}
TradeDirection::BtoA => {
let results = curve
.swap_without_fees(
withdraw_result.token_b_amount,
new_swap_token_b_amount,
new_swap_token_a_amount,
trade_direction,
)
.unwrap();
withdraw_result.token_a_amount + results.destination_amount_swapped
}
};
// see how many pool tokens it would cost to withdraw one side for the
// total amount of tokens, should be close!
let opposite_direction = trade_direction.opposite();
let pool_token_amount_from_single_side_withdraw = curve
.withdraw_single_token_type_exact_out(
source_token_amount,
swap_token_a_amount,
swap_token_b_amount,
pool_token_supply,
opposite_direction,
)
.unwrap();
// slippage due to rounding or truncation errors
let epsilon = std::cmp::max(1, pool_token_amount * epsilon_in_basis_points / 10000);
let difference = if pool_token_amount >= pool_token_amount_from_single_side_withdraw {
pool_token_amount - pool_token_amount_from_single_side_withdraw
} else {
pool_token_amount_from_single_side_withdraw - pool_token_amount
};
assert!(
difference <= epsilon,
"difference expected to be less than {}, actually {}",
epsilon,
difference
);
}
/// Test function checking that a swap never reduces the overall value of
/// the pool.
///

View File

@ -15,6 +15,46 @@ use {
spl_math::{checked_ceil_div::CheckedCeilDiv, precise_number::PreciseNumber, uint::U256},
};
/// 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(
token_b_price: u64,
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 token_b_price = U256::from(token_b_price);
let given_value = match trade_direction {
TradeDirection::AtoB => U256::from(source_amount),
TradeDirection::BtoA => U256::from(source_amount).checked_mul(token_b_price)?,
};
let total_value = U256::from(swap_token_b_amount)
.checked_mul(token_b_price)?
.checked_add(U256::from(swap_token_a_amount))?;
let pool_supply = U256::from(pool_supply);
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(),
),
}
}
/// ConstantPriceCurve struct implementing CurveCalculator
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ConstantPriceCurve {
@ -107,39 +147,42 @@ impl CurveCalculator for ConstantPriceCurve {
/// Get the amount of pool tokens for the given amount of token A and B
/// For the constant price curve, the total value of the pool is weighted
/// by the price of token B.
fn trading_tokens_to_pool_tokens(
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,
round_direction: RoundDirection,
) -> Option<u128> {
let token_b_price = U256::from(self.token_b_price);
let given_value = match trade_direction {
TradeDirection::AtoB => U256::from(source_amount),
TradeDirection::BtoA => U256::from(source_amount).checked_mul(token_b_price)?,
};
let total_value = U256::from(swap_token_b_amount)
.checked_mul(token_b_price)?
.checked_add(U256::from(swap_token_a_amount))?;
let pool_supply = U256::from(pool_supply);
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(),
),
}
trading_tokens_to_pool_tokens(
self.token_b_price,
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> {
trading_tokens_to_pool_tokens(
self.token_b_price,
source_amount,
swap_token_a_amount,
swap_token_b_amount,
pool_supply,
trade_direction,
RoundDirection::Ceiling,
)
}
fn validate(&self) -> Result<(), SwapError> {
@ -221,7 +264,8 @@ mod tests {
use super::*;
use crate::curve::calculator::{
test::{
check_curve_value_from_swap, check_pool_token_conversion, total_and_intermediate,
check_curve_value_from_swap, check_deposit_token_conversion,
check_withdraw_token_conversion, total_and_intermediate,
CONVERSION_BASIS_POINTS_GUARANTEE,
},
INITIAL_SWAP_POOL_AMOUNT,
@ -343,7 +387,7 @@ mod tests {
proptest! {
#[test]
fn pool_token_conversion_a_to_b(
fn deposit_token_conversion_a_to_b(
// 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,
@ -361,7 +405,7 @@ mod tests {
let curve = ConstantPriceCurve {
token_b_price,
};
check_pool_token_conversion(
check_deposit_token_conversion(
&curve,
source_token_amount as u128,
swap_source_amount as u128,
@ -375,7 +419,7 @@ mod tests {
proptest! {
#[test]
fn pool_token_conversion_b_to_a(
fn deposit_token_conversion_b_to_a(
// 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..u32::MAX, // kept small to avoid proptest rejections
@ -395,7 +439,7 @@ mod tests {
// on the other side to complete the swap
prop_assume!(token_b_price * source_token_amount / 2 <= swap_destination_amount);
check_pool_token_conversion(
check_deposit_token_conversion(
&curve,
source_token_amount,
swap_source_amount,
@ -407,6 +451,61 @@ mod tests {
}
}
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..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 token_b_price = token_b_price as u128;
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 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);
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);
check_withdraw_token_conversion(
&curve,
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
TradeDirection::AtoB,
CONVERSION_BASIS_POINTS_GUARANTEE
);
check_withdraw_token_conversion(
&curve,
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
TradeDirection::BtoA,
CONVERSION_BASIS_POINTS_GUARANTEE
);
}
}
proptest! {
#[test]
fn curve_value_does_not_decrease_from_swap_a_to_b(

View File

@ -92,12 +92,12 @@ pub fn pool_tokens_to_trading_tokens(
})
}
/// Get the amount of pool tokens for the given amount of token A or B.
/// Get the amount of pool tokens for the deposited 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(
pub fn deposit_single_token_type(
source_amount: u128,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
@ -123,6 +123,37 @@ pub fn trading_tokens_to_pool_tokens(
}
}
/// Get the amount of pool tokens for the withdrawn amount of token A or B.
///
/// The constant product implementation uses the Balancer formulas found at
/// https://balancer.finance/whitepaper/#single-asset-withdrawal, specifically
/// in the case for 2 tokens, each weighted at 1/2.
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(),
}
}
/// Calculates the total normalized value of the curve given the liquidity
/// parameters.
///
@ -170,23 +201,40 @@ impl CurveCalculator for ConstantProductCurve {
)
}
/// Get the amount of pool tokens for the given amount of token A or B.
fn trading_tokens_to_pool_tokens(
/// Get the amount of pool tokens for the deposited amount of token A or B.
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,
round_direction: RoundDirection,
) -> Option<u128> {
trading_tokens_to_pool_tokens(
deposit_single_token_type(
source_amount,
swap_token_a_amount,
swap_token_b_amount,
pool_supply,
trade_direction,
round_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,
)
}
@ -230,8 +278,9 @@ mod tests {
use super::*;
use crate::curve::calculator::{
test::{
check_curve_value_from_swap, check_pool_token_conversion,
check_pool_value_from_deposit, check_pool_value_from_withdraw, total_and_intermediate,
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,
@ -367,7 +416,7 @@ mod tests {
proptest! {
#[test]
fn pool_token_conversion(
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,
@ -376,7 +425,7 @@ mod tests {
pool_supply in INITIAL_SWAP_POOL_AMOUNT..u64::MAX as u128,
) {
let curve = ConstantProductCurve {};
check_pool_token_conversion(
check_deposit_token_conversion(
&curve,
source_token_amount as u128,
swap_source_amount as u128,
@ -386,7 +435,7 @@ mod tests {
CONVERSION_BASIS_POINTS_GUARANTEE,
);
check_pool_token_conversion(
check_deposit_token_conversion(
&curve,
source_token_amount as u128,
swap_source_amount as u128,
@ -398,6 +447,35 @@ mod tests {
}
}
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(

View File

@ -8,8 +8,8 @@ use {
TradingTokenResult,
},
constant_product::{
normalized_value, pool_tokens_to_trading_tokens, swap,
trading_tokens_to_pool_tokens,
deposit_single_token_type, normalized_value, pool_tokens_to_trading_tokens, swap,
withdraw_single_token_type_exact_out,
},
},
error::SwapError,
@ -78,23 +78,41 @@ impl CurveCalculator for OffsetCurve {
/// Get the amount of pool tokens for the given amount of token A and B,
/// taking into account the offset
fn trading_tokens_to_pool_tokens(
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,
round_direction: RoundDirection,
) -> Option<u128> {
let token_b_offset = self.token_b_offset as u128;
trading_tokens_to_pool_tokens(
deposit_single_token_type(
source_amount,
swap_token_a_amount,
swap_token_b_amount.checked_add(token_b_offset)?,
pool_supply,
trade_direction,
round_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> {
let token_b_offset = self.token_b_offset as u128;
withdraw_single_token_type_exact_out(
source_amount,
swap_token_a_amount,
swap_token_b_amount.checked_add(token_b_offset)?,
pool_supply,
trade_direction,
RoundDirection::Ceiling,
)
}
@ -170,8 +188,9 @@ mod tests {
use super::*;
use crate::curve::calculator::{
test::{
check_curve_value_from_swap, check_pool_token_conversion,
check_pool_value_from_deposit, check_pool_value_from_withdraw, total_and_intermediate,
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,
},
INITIAL_SWAP_POOL_AMOUNT,
@ -297,7 +316,7 @@ mod tests {
proptest! {
#[test]
fn pool_token_conversion_a_to_b(
fn deposit_token_conversion_a_to_b(
// 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,
@ -326,7 +345,7 @@ mod tests {
// The invariant needs to fit in a u128.
// invariant = swap_source_amount * (swap_destination_amount + token_b_offset)
prop_assume!(!(swap_destination_amount + token_b_offset).overflowing_mul(swap_source_amount).1);
check_pool_token_conversion(
check_deposit_token_conversion(
&curve,
source_token_amount,
swap_source_amount,
@ -340,7 +359,7 @@ mod tests {
proptest! {
#[test]
fn pool_token_conversion_b_to_a(
fn deposit_token_conversion_b_to_a(
// 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,
@ -360,7 +379,7 @@ mod tests {
// The invariant needs to fit in a u128
// invariant = swap_destination_amount * (swap_source_amount + token_b_offset)
prop_assume!(!(swap_source_amount + token_b_offset).overflowing_mul(swap_destination_amount).1);
check_pool_token_conversion(
check_deposit_token_conversion(
&curve,
source_token_amount,
swap_source_amount,
@ -372,6 +391,59 @@ mod tests {
}
}
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, token_b_offset) in values_sum_within_u64(),
) {
let curve = OffsetCurve {
token_b_offset,
};
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;
let pool_token_amount = pool_token_amount as u128;
let pool_token_supply = pool_token_supply as u128;
// The invariant needs to fit in a u128
// invariant = swap_destination_amount * (swap_source_amount + token_b_offset)
prop_assume!(!(swap_token_b_amount + token_b_offset).overflowing_mul(swap_token_a_amount).1);
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_withdraw_token_conversion(
&curve,
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
TradeDirection::AtoB,
CONVERSION_BASIS_POINTS_GUARANTEE
);
check_withdraw_token_conversion(
&curve,
pool_token_amount,
pool_token_supply,
swap_token_a_amount,
swap_token_b_amount,
TradeDirection::BtoA,
CONVERSION_BASIS_POINTS_GUARANTEE
);
}
}
proptest! {
#[test]
fn curve_value_does_not_decrease_from_swap_a_to_b(

View File

@ -200,14 +200,13 @@ impl CurveCalculator for StableCurve {
/// 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 trading_tokens_to_pool_tokens(
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,
round_direction: RoundDirection,
) -> Option<u128> {
if source_amount == 0 {
return Some(0);
@ -231,10 +230,40 @@ impl CurveCalculator for StableCurve {
let diff = d1.checked_sub(&d0)?;
let final_amount =
(diff.checked_mul(&PreciseNumber::new(pool_supply)?))?.checked_div(&d0)?;
match round_direction {
RoundDirection::Floor => final_amount.floor()?.to_imprecise(),
RoundDirection::Ceiling => final_amount.ceiling()?.to_imprecise(),
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 = self.amp.checked_mul(N_COINS as u64)?;
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(
@ -315,8 +344,9 @@ mod tests {
use super::*;
use crate::curve::calculator::{
test::{
check_curve_value_from_swap, check_pool_token_conversion,
check_pool_value_from_deposit, check_pool_value_from_withdraw, total_and_intermediate,
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,
@ -502,7 +532,7 @@ mod tests {
proptest! {
#[test]
fn pool_token_conversion(
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,
@ -512,7 +542,7 @@ mod tests {
amp in 1..100u64,
) {
let curve = StableCurve { amp };
check_pool_token_conversion(
check_deposit_token_conversion(
&curve,
source_token_amount as u128,
swap_source_amount as u128,
@ -522,7 +552,7 @@ mod tests {
CONVERSION_BASIS_POINTS_GUARANTEE * 100,
);
check_pool_token_conversion(
check_deposit_token_conversion(
&curve,
source_token_amount as u128,
swap_source_amount as u128,
@ -533,4 +563,34 @@ mod tests {
);
}
}
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
);
}
}
}

View File

@ -427,13 +427,12 @@ impl Processor {
let mut pool_token_amount = token_swap
.swap_curve()
.trading_tokens_to_pool_tokens(
.withdraw_single_token_type_exact_out(
result.owner_fee,
swap_token_a_amount,
swap_token_b_amount,
to_u128(pool_mint.supply)?,
trade_direction,
RoundDirection::Ceiling,
token_swap.fees(),
)
.ok_or(SwapError::FeeCalculationFailure)?;
@ -782,13 +781,12 @@ impl Processor {
let pool_token_amount = if pool_mint_supply > 0 {
token_swap
.swap_curve()
.trading_tokens_to_pool_tokens(
.deposit_single_token_type(
to_u128(source_token_amount)?,
to_u128(swap_token_a.amount)?,
to_u128(swap_token_b.amount)?,
pool_mint_supply,
trade_direction,
RoundDirection::Floor,
token_swap.fees(),
)
.ok_or(SwapError::ZeroTradingTokens)?
@ -896,36 +894,17 @@ impl Processor {
let pool_mint = Self::unpack_mint(pool_mint_info, &token_swap.token_program_id())?;
let pool_mint_supply = to_u128(pool_mint.supply)?;
let (swap_token_a_amount, swap_token_b_amount) = match trade_direction {
TradeDirection::AtoB => (
to_u128(
swap_token_a
.amount
.checked_sub(destination_token_amount)
.ok_or(SwapError::CalculationFailure)?,
)?,
to_u128(swap_token_b.amount)?,
),
TradeDirection::BtoA => (
to_u128(swap_token_a.amount)?,
to_u128(
swap_token_b
.amount
.checked_sub(destination_token_amount)
.ok_or(SwapError::CalculationFailure)?,
)?,
),
};
let swap_token_a_amount = to_u128(swap_token_a.amount)?;
let swap_token_b_amount = to_u128(swap_token_b.amount)?;
let burn_pool_token_amount = token_swap
.swap_curve()
.trading_tokens_to_pool_tokens(
.withdraw_single_token_type_exact_out(
to_u128(destination_token_amount)?,
swap_token_a_amount,
swap_token_b_amount,
pool_mint_supply,
trade_direction,
RoundDirection::Ceiling,
token_swap.fees(),
)
.ok_or(SwapError::ZeroTradingTokens)?;
@ -5330,15 +5309,12 @@ mod tests {
let pool_token_amount = accounts
.swap_curve
.trading_tokens_to_pool_tokens(
.withdraw_single_token_type_exact_out(
destination_a_amount.try_into().unwrap(),
(swap_token_a.amount - destination_a_amount)
.try_into()
.unwrap(),
swap_token_a.amount.try_into().unwrap(),
swap_token_b.amount.try_into().unwrap(),
pool_mint.supply.try_into().unwrap(),
TradeDirection::AtoB,
RoundDirection::Ceiling,
&accounts.fees,
)
.unwrap();
@ -5505,13 +5481,12 @@ mod tests {
);
let first_fee = swap_curve
.trading_tokens_to_pool_tokens(
.withdraw_single_token_type_exact_out(
results.owner_fee,
token_a_amount.try_into().unwrap(),
token_b_amount.try_into().unwrap(),
initial_supply.try_into().unwrap(),
TradeDirection::AtoB,
RoundDirection::Ceiling,
&fees,
)
.unwrap();
@ -5582,13 +5557,12 @@ mod tests {
);
let second_fee = swap_curve
.trading_tokens_to_pool_tokens(
.withdraw_single_token_type_exact_out(
results.owner_fee,
token_a_amount.try_into().unwrap(),
token_b_amount.try_into().unwrap(),
initial_supply.try_into().unwrap(),
TradeDirection::BtoA,
RoundDirection::Ceiling,
&fees,
)
.unwrap();