use std::fmt::{self, Display, Formatter};
use crate::accounts::Pool;
use crate::errors::TensorAmmError;
use crate::types::{
CurveType, Direction, EditPoolConfig, PoolConfig, PoolStats, PoolType, TakerSide,
};
use crate::HUNDRED_PCT_BPS;
use spl_math::precise_number::PreciseNumber;
#[allow(clippy::derivable_impls)]
impl Default for PoolStats {
fn default() -> Self {
Self {
taker_sell_count: 0,
taker_buy_count: 0,
accumulated_mm_profit: 0,
}
}
}
impl Pool {
pub fn shift_price(&self, price_offset: i32, side: TakerSide) -> Result<u64, TensorAmmError> {
let direction = if price_offset > 0 {
Direction::Up
} else {
Direction::Down
};
let offset = price_offset.unsigned_abs();
let current_price = match self.config.curve_type {
CurveType::Linear => {
let base = self.config.starting_price;
let delta = self.config.delta;
match direction {
Direction::Up => base
.checked_add(
delta
.checked_mul(offset as u64)
.ok_or(TensorAmmError::ArithmeticError)?,
)
.ok_or(TensorAmmError::ArithmeticError)?,
Direction::Down => base
.checked_sub(
delta
.checked_mul(offset as u64)
.ok_or(TensorAmmError::ArithmeticError)?,
)
.ok_or(TensorAmmError::ArithmeticError)?,
}
}
CurveType::Exponential => {
let hundred_pct = PreciseNumber::new(HUNDRED_PCT_BPS.into())
.ok_or(TensorAmmError::ArithmeticError)?;
let base = PreciseNumber::new(self.config.starting_price.into())
.ok_or(TensorAmmError::ArithmeticError)?;
let factor = PreciseNumber::new(
(HUNDRED_PCT_BPS)
.checked_add(self.config.delta)
.ok_or(TensorAmmError::ArithmeticError)?
.into(),
)
.ok_or(TensorAmmError::ArithmeticError)?
.checked_div(&hundred_pct)
.ok_or(TensorAmmError::ArithmeticError)?
.checked_pow(offset.into())
.ok_or(TensorAmmError::ArithmeticError)?;
let result = match direction {
Direction::Up => base.checked_mul(&factor),
Direction::Down => base.checked_div(&factor),
};
let rounded_result = match side {
TakerSide::Buy => result.ok_or(TensorAmmError::ArithmeticError)?.ceiling(),
TakerSide::Sell => result.ok_or(TensorAmmError::ArithmeticError)?.floor(),
};
let imprecise = rounded_result
.ok_or(TensorAmmError::ArithmeticError)?
.to_imprecise()
.ok_or(TensorAmmError::ArithmeticError)?;
u64::try_from(imprecise)
.ok()
.ok_or(TensorAmmError::ArithmeticError)?
}
};
Ok(current_price)
}
pub fn current_price(&self, side: TakerSide) -> Result<u64, TensorAmmError> {
match (self.config.pool_type, side) {
(PoolType::Trade, TakerSide::Buy)
| (PoolType::Token, TakerSide::Sell)
| (PoolType::NFT, TakerSide::Buy) => self.shift_price(self.price_offset, side),
(PoolType::Trade, TakerSide::Sell) => self.shift_price(self.price_offset - 1, side),
_ => Err(TensorAmmError::WrongPoolType),
}
}
pub fn calc_mm_fee(&self, current_price: u64) -> Result<u64, TensorAmmError> {
let fee = match self.config.pool_type {
PoolType::Trade => (self.config.mm_fee_bps.into_base() as u64)
.checked_mul(current_price)
.ok_or(TensorAmmError::ArithmeticError)?
.checked_div(HUNDRED_PCT_BPS)
.ok_or(TensorAmmError::ArithmeticError)?,
PoolType::NFT | PoolType::Token => 0, };
Ok(fee)
}
}
impl Display for PoolType {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
PoolType::Trade => write!(f, "Trade"),
PoolType::Token => write!(f, "Token"),
PoolType::NFT => write!(f, "NFT"),
}
}
}
impl Display for CurveType {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
CurveType::Linear => write!(f, "Linear"),
CurveType::Exponential => write!(f, "Exponential"),
}
}
}
impl EditPoolConfig {
pub fn into_pool_config(self, pool_type: PoolType) -> PoolConfig {
PoolConfig {
pool_type,
curve_type: self.curve_type,
starting_price: self.starting_price,
delta: self.delta,
mm_compound_fees: self.mm_compound_fees,
mm_fee_bps: self.mm_fee_bps,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use solana_program::pubkey::Pubkey;
use crate::{
types::{PoolConfig, PoolStats},
Currency, NullableAddress, NullableU16, LAMPORTS_PER_SOL,
};
impl Pool {
pub fn new_test_pool(
pool_type: PoolType,
curve_type: CurveType,
starting_price: u64,
delta: u64,
price_offset: i32,
mm_fee_bps: NullableU16,
) -> Self {
Self {
discriminator: [0; 8],
version: 1,
bump: [1],
created_at: 1234,
updated_at: 0,
expiry: 0,
owner: Pubkey::default(),
cosigner: NullableAddress::none(),
maker_broker: NullableAddress::none(),
rent_payer: Pubkey::default(),
whitelist: Pubkey::default(),
pool_id: [0; 32],
config: PoolConfig {
pool_type,
curve_type,
starting_price,
delta,
mm_compound_fees: true,
mm_fee_bps,
},
price_offset,
nfts_held: 0,
stats: PoolStats::default(),
currency: Currency::sol(),
amount: 0,
shared_escrow: NullableAddress::none(),
max_taker_sell_count: 10,
reserved: [0; 100],
}
}
}
#[test]
fn test_linear_token_pool() {
let delta = LAMPORTS_PER_SOL / 10;
let mut p = Pool::new_test_pool(
PoolType::Token,
CurveType::Linear,
LAMPORTS_PER_SOL,
delta,
0,
NullableU16::none(),
);
assert_eq!(p.current_price(TakerSide::Sell).unwrap(), LAMPORTS_PER_SOL);
p.price_offset -= 1;
assert_eq!(
p.current_price(TakerSide::Sell).unwrap(),
LAMPORTS_PER_SOL - delta
);
p.price_offset -= 2;
assert_eq!(
p.current_price(TakerSide::Sell).unwrap(),
LAMPORTS_PER_SOL - delta * 3
);
p.price_offset -= 7;
assert_eq!(
p.current_price(TakerSide::Sell).unwrap(),
LAMPORTS_PER_SOL - delta * 10
);
assert_eq!(p.current_price(TakerSide::Sell).unwrap(), 0);
}
#[test]
#[should_panic(expected = "ArithmeticError")]
fn test_linear_token_pool_panic_overflow() {
let delta = LAMPORTS_PER_SOL / 10;
let p = Pool::new_test_pool(
PoolType::Token,
CurveType::Linear,
LAMPORTS_PER_SOL,
delta,
-11,
NullableU16::none(),
);
p.current_price(TakerSide::Sell).unwrap();
}
#[test]
#[should_panic(expected = "WrongPoolType")]
fn test_linear_token_pool_panic_on_buy() {
let delta = LAMPORTS_PER_SOL / 10;
let p = Pool::new_test_pool(
PoolType::Token,
CurveType::Linear,
LAMPORTS_PER_SOL,
delta,
0,
NullableU16::none(),
);
p.current_price(TakerSide::Buy).unwrap();
}
#[test]
fn test_linear_nft_pool() {
let delta = LAMPORTS_PER_SOL / 10;
let mut p = Pool::new_test_pool(
PoolType::NFT,
CurveType::Linear,
LAMPORTS_PER_SOL,
delta,
0,
NullableU16::none(),
);
assert_eq!(p.current_price(TakerSide::Buy).unwrap(), LAMPORTS_PER_SOL);
p.price_offset += 1;
assert_eq!(
p.current_price(TakerSide::Buy).unwrap(),
LAMPORTS_PER_SOL + delta
);
p.price_offset += 2;
assert_eq!(
p.current_price(TakerSide::Buy).unwrap(),
LAMPORTS_PER_SOL + delta * 3
);
p.price_offset += 9999996;
assert_eq!(
p.current_price(TakerSide::Buy).unwrap(),
LAMPORTS_PER_SOL + delta * 9999999
);
}
#[test]
#[should_panic(expected = "ArithmeticError")]
fn test_linear_nft_pool_panic_overflow() {
let delta = LAMPORTS_PER_SOL / 10 * 100;
let p = Pool::new_test_pool(
PoolType::NFT,
CurveType::Linear,
LAMPORTS_PER_SOL * 100,
delta,
i32::MAX - 1, NullableU16::none(),
);
p.current_price(TakerSide::Buy).unwrap();
}
#[test]
#[should_panic(expected = "WrongPoolType")]
fn test_linear_nft_pool_panic_on_sell() {
let delta = LAMPORTS_PER_SOL / 10 * 100;
let p = Pool::new_test_pool(
PoolType::NFT,
CurveType::Linear,
LAMPORTS_PER_SOL * 100,
delta,
0,
NullableU16::none(),
);
p.current_price(TakerSide::Sell).unwrap();
}
#[test]
fn test_linear_trade_pool() {
let delta = LAMPORTS_PER_SOL / 10;
let mut p = Pool::new_test_pool(
PoolType::Trade,
CurveType::Linear,
LAMPORTS_PER_SOL,
delta,
0,
NullableU16::none(),
);
assert_eq!(p.current_price(TakerSide::Buy).unwrap(), LAMPORTS_PER_SOL);
assert_eq!(
p.current_price(TakerSide::Sell).unwrap(),
LAMPORTS_PER_SOL - delta
);
p.price_offset -= 1;
assert_eq!(
p.current_price(TakerSide::Buy).unwrap(),
LAMPORTS_PER_SOL - delta
);
assert_eq!(
p.current_price(TakerSide::Sell).unwrap(),
LAMPORTS_PER_SOL - delta * 2
);
p.price_offset -= 2;
assert_eq!(
p.current_price(TakerSide::Buy).unwrap(),
LAMPORTS_PER_SOL - delta * 3
);
assert_eq!(
p.current_price(TakerSide::Sell).unwrap(),
LAMPORTS_PER_SOL - delta * 4
);
p.price_offset -= 7;
assert_eq!(
p.current_price(TakerSide::Buy).unwrap(),
LAMPORTS_PER_SOL - delta * 10
);
p.price_offset += 10;
assert_eq!(p.current_price(TakerSide::Buy).unwrap(), LAMPORTS_PER_SOL);
assert_eq!(
p.current_price(TakerSide::Sell).unwrap(),
LAMPORTS_PER_SOL - delta
);
p.price_offset += 1;
assert_eq!(
p.current_price(TakerSide::Buy).unwrap(),
LAMPORTS_PER_SOL + delta
);
assert_eq!(p.current_price(TakerSide::Sell).unwrap(), LAMPORTS_PER_SOL);
p.price_offset += 2;
assert_eq!(
p.current_price(TakerSide::Buy).unwrap(),
LAMPORTS_PER_SOL + delta * 3
);
assert_eq!(
p.current_price(TakerSide::Sell).unwrap(),
LAMPORTS_PER_SOL + delta * 2
);
p.price_offset += 9999996;
assert_eq!(
p.current_price(TakerSide::Buy).unwrap(),
LAMPORTS_PER_SOL + delta * 9999999
);
assert_eq!(
p.current_price(TakerSide::Sell).unwrap(),
LAMPORTS_PER_SOL + delta * 9999998
);
}
#[test]
#[should_panic(expected = "ArithmeticError")]
fn test_linear_trade_pool_panic_lower() {
let delta = LAMPORTS_PER_SOL / 10;
let p = Pool::new_test_pool(
PoolType::Trade,
CurveType::Linear,
LAMPORTS_PER_SOL,
delta,
-11,
NullableU16::none(),
);
p.current_price(TakerSide::Buy).unwrap();
}
#[test]
#[should_panic(expected = "ArithmeticError")]
fn test_linear_trade_pool_panic_sell_side_lower() {
let delta = LAMPORTS_PER_SOL / 10;
let p = Pool::new_test_pool(
PoolType::Trade,
CurveType::Linear,
LAMPORTS_PER_SOL,
delta,
-10, NullableU16::none(),
);
p.current_price(TakerSide::Sell).unwrap();
}
#[test]
#[should_panic(expected = "ArithmeticError")]
fn test_linear_trade_pool_panic_upper() {
let delta = LAMPORTS_PER_SOL * 10_000_000_000;
let p = Pool::new_test_pool(
PoolType::Trade,
CurveType::Linear,
delta,
delta,
1, NullableU16::none(),
);
p.current_price(TakerSide::Buy).unwrap();
}
#[test]
fn test_linear_trade_pool_sell_side_upper() {
let delta = LAMPORTS_PER_SOL * 10_000_000_000;
let p = Pool::new_test_pool(
PoolType::Trade,
CurveType::Linear,
delta,
delta,
1,
NullableU16::none(),
);
assert_eq!(p.current_price(TakerSide::Sell).unwrap(), delta);
}
}