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

229 lines
7.5 KiB
Rust

//! Swap calculations
use crate::{curve::math::PreciseNumber, error::SwapError};
use std::fmt::Debug;
/// 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
/// input amounts, and Balancer uses 100 * 10 ^ 18.
pub const INITIAL_SWAP_POOL_AMOUNT: u128 = 1_000_000_000;
/// Hardcode the number of token types in a pool, used to calculate the
/// equivalent pool tokens for the owner trading fee.
pub const TOKENS_IN_POOL: u128 = 2;
/// Helper function for mapping to SwapError::CalculationFailure
pub fn map_zero_to_none(x: u128) -> Option<u128> {
if x == 0 {
None
} else {
Some(x)
}
}
/// The direction of a trade, since curves can be specialized to treat each
/// token differently (by adding offsets or weights)
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TradeDirection {
/// Input token A, output token B
AtoB,
/// Input token B, output token A
BtoA,
}
/// Encodes all results of swapping from a source token to a destination token
#[derive(Debug, PartialEq)]
pub struct SwapWithoutFeesResult {
/// Amount of source token swapped
pub source_amount_swapped: u128,
/// Amount of destination token swapped
pub destination_amount_swapped: u128,
}
/// Encodes results of depositing both sides at once
#[derive(Debug, PartialEq)]
pub struct TradingTokenResult {
/// Amount of token
pub token_a_amount: u128,
/// Amount of destination token swapped
pub token_b_amount: u128,
}
/// Trait for packing of trait objects, required because structs that implement
/// `Pack` cannot be used as trait objects (as `dyn Pack`).
pub trait DynPack {
/// Only required function is to pack given a trait object
fn pack_into_slice(&self, dst: &mut [u8]);
}
/// Trait representing operations required on a swap curve
pub trait CurveCalculator: Debug + DynPack {
/// Calculate how much destination token will be provided given an amount
/// of source token.
fn swap_without_fees(
&self,
source_amount: u128,
swap_source_amount: u128,
swap_destination_amount: u128,
trade_direction: TradeDirection,
) -> Option<SwapWithoutFeesResult>;
/// Get the supply for a new pool
/// The default implementation is a Balancer-style fixed initial supply
fn new_pool_supply(&self) -> u128 {
INITIAL_SWAP_POOL_AMOUNT
}
/// 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 = pool_tokens
.checked_mul(swap_token_a_amount)?
.checked_div(pool_token_supply)?;
let token_b_amount = pool_tokens
.checked_mul(swap_token_b_amount)?
.checked_div(pool_token_supply)?;
Some(TradingTokenResult {
token_a_amount,
token_b_amount,
})
}
/// Get the amount of pool tokens for the given amount of token A or B
/// See the concept for the calculation at:
/// https://balancer.finance/whitepaper/#single-asset-deposit
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,
) -> 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()
}
/// Validate that the given curve has no bad parameters
fn validate(&self) -> Result<(), SwapError>;
/// Validate the given supply on init, helpful for curves that do or don't
/// allow zero supply on one side
fn validate_supply(&self, token_a_amount: u64, token_b_amount: u64) -> Result<(), SwapError> {
if token_a_amount == 0 {
return Err(SwapError::EmptySupply);
}
if token_b_amount == 0 {
return Err(SwapError::EmptySupply);
}
Ok(())
}
/// Some curves will function best and prevent attacks if we prevent
/// deposits after initialization
fn allows_deposits(&self) -> bool {
true
}
}
#[cfg(test)]
pub mod test {
use super::*;
/// Check that two numbers are within 1 of each other
fn almost_equal(a: u128, b: u128) {
if a >= b {
assert!(a - b <= 1);
} else {
assert!(b - a <= 1);
}
}
pub fn check_pool_token_conversion(
curve: &dyn CurveCalculator,
swap_token_a_amount: u128,
swap_token_b_amount: u128,
token_a_amount: u128,
) {
// check that depositing token A is the same as swapping for token B
// and depositing the result
let swap_results = curve
.swap_without_fees(
token_a_amount,
swap_token_a_amount,
swap_token_b_amount,
TradeDirection::AtoB,
)
.unwrap();
let token_a_amount = swap_results.source_amount_swapped;
let token_b_amount = swap_results.destination_amount_swapped;
let pool_supply = curve.new_pool_supply();
let pool_tokens_from_a = curve
.trading_tokens_to_pool_tokens(
token_a_amount,
swap_token_a_amount + token_a_amount,
swap_token_b_amount,
pool_supply,
TradeDirection::AtoB,
)
.unwrap();
let pool_tokens_from_b = curve
.trading_tokens_to_pool_tokens(
token_b_amount,
swap_token_a_amount + token_a_amount,
swap_token_b_amount,
pool_supply,
TradeDirection::BtoA,
)
.unwrap();
let deposit_token_a = curve
.pool_tokens_to_trading_tokens(
pool_tokens_from_a,
pool_supply + pool_tokens_from_a,
swap_token_a_amount,
swap_token_b_amount,
)
.unwrap();
let deposit_token_b = curve
.pool_tokens_to_trading_tokens(
pool_tokens_from_b,
pool_supply + pool_tokens_from_b,
swap_token_a_amount,
swap_token_b_amount,
)
.unwrap();
// They should be within 1 token because truncation
almost_equal(
deposit_token_b.token_a_amount,
deposit_token_a.token_a_amount,
);
almost_equal(
deposit_token_b.token_b_amount,
deposit_token_b.token_b_amount,
);
}
}