use crate::math::amm::{
self, AmmError, AmmInput, BuyArgs, QuoteError, SellArgs,
};
use crate::nexus::EffectiveFeeArgs;
use crate::nexus::types::LaunchpadFees;
use crate::utils::{
Landing, MarketQuote, MarketQuoteError, QuoteRequest, TradeDirection,
TradeMode,
};
#[derive(Clone, Copy, Debug)]
pub struct CurveMarket<'a> {
pub virtual_base_reserves: u64,
pub virtual_quote_reserves: u64,
pub real_base_reserves: u64,
pub real_quote_reserves: u64,
pub created_at: i64,
pub fees: &'a LaunchpadFees,
pub landing: Landing,
pub creator_fee_bps: u16,
}
pub fn quote(
market: &CurveMarket<'_>,
request: QuoteRequest,
) -> Result<MarketQuote, MarketQuoteError> {
if market.real_base_reserves == 0 {
return Err(MarketQuoteError::SupplyExhausted);
}
let fee_bps = market
.fees
.effective_fee_bps(EffectiveFeeArgs {
creator_fee_bps: market.creator_fee_bps,
created_at: market.created_at,
now: market.landing.unix_timestamp,
})
.ok_or(MarketQuoteError::FeeOutOfRange)?;
let amm = AmmInput {
quote_reserves: market.virtual_quote_reserves,
base_reserves: market.virtual_base_reserves,
amount: request.amount,
fee_bps,
};
let Landing {
base_fee,
quote_fee,
..
} = market.landing;
match request.direction {
TradeDirection::Buy => {
let args = BuyArgs {
amm,
quote_fee,
base_fee,
base_reserve_cap: Some(market.real_base_reserves),
};
let bought = match request.mode {
TradeMode::ExactIn => amm::buy_exact_in_with_fees(args),
TradeMode::ExactOut => amm::buy_exact_out_with_fees(args),
}?;
Ok(MarketQuote::bought(&bought, request, fee_bps))
}
TradeDirection::Sell => {
let args = SellArgs {
amm,
quote_fee,
base_fee,
};
let sold = match request.mode {
TradeMode::ExactIn => amm::sell_exact_in_with_fees(args),
TradeMode::ExactOut => amm::sell_exact_out_with_fees(args),
}?;
let debit = sold
.quote_amount
.checked_add(sold.fee)
.ok_or(QuoteError::Amm(AmmError::Overflow))?;
if debit > market.real_quote_reserves {
return Err(MarketQuoteError::ExceedsQuoteHeld);
}
Ok(MarketQuote::sold(&sold, fee_bps))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::amm::MintFee;
const NOW: i64 = 1_000;
const ONE_SOL: u64 = 1_000_000_000;
const FEES: LaunchpadFees = LaunchpadFees {
creation_fee_cents: 0,
protocol_fee_bps: 100,
max_creator_fee_bps: 50,
fee_decay_seconds: 0,
fee_decay_start_bps: 0,
};
const LANDING: Landing = Landing {
base_fee: None,
quote_fee: None,
unix_timestamp: NOW,
};
const MARKET: CurveMarket<'static> = CurveMarket {
virtual_base_reserves: 1_000_000_000_000_000,
virtual_quote_reserves: 30_000_000_000,
real_base_reserves: 500_000_000_000_000,
real_quote_reserves: 0,
created_at: 0,
fees: &FEES,
landing: LANDING,
creator_fee_bps: 0,
};
const BASE_100_BPS: MintFee = MintFee {
bps: 100,
maximum_fee: u64::MAX,
};
const fn request(
direction: TradeDirection,
mode: TradeMode,
amount: u64,
) -> QuoteRequest {
QuoteRequest {
direction,
mode,
amount,
}
}
#[test]
fn a_buy_prices_off_the_virtual_reserves() {
let quote = quote(
&MARKET,
request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL),
)
.unwrap();
assert_eq!(quote.in_amount, ONE_SOL);
assert_eq!(quote.out_amount, 31_945_788_964_181);
assert_eq!(quote.fee, 10_000_000);
assert_eq!(quote.fee_bps, 100);
assert!(!quote.supply_capped);
}
#[test]
fn the_fee_decays_from_the_curves_creation() {
let decaying = LaunchpadFees {
fee_decay_seconds: 12,
fee_decay_start_bps: 5_000,
..FEES
};
let fresh = quote(
&CurveMarket {
created_at: NOW,
fees: &decaying,
..MARKET
},
request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL),
)
.unwrap();
assert_eq!(fresh.fee_bps, 5_000);
assert_eq!(fresh.fee, 500_000_000);
assert_eq!(fresh.out_amount, 16_393_442_622_950);
assert_eq!(
quote(
&CurveMarket {
created_at: -1,
..MARKET
},
request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL)
),
Err(MarketQuoteError::FeeOutOfRange)
);
}
#[test]
fn the_curves_creator_rate_prices_the_trade_not_the_partner_max() {
let decaying = LaunchpadFees {
fee_decay_seconds: 12,
fee_decay_start_bps: 5_000,
..FEES
};
let buy = request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL);
for (creator_fee_bps, standard, halfway) in [
(
0,
(100, 10_000_000, 31_945_788_964_181),
(1_325, 132_500_000, 28_103_992_872_762),
),
(
30,
(130, 13_000_000, 31_852_066_995_836),
(1_348, 134_800_000, 28_031_569_534_621),
),
(
80,
(180, 18_000_000, 31_695_823_381_318),
(1_385, 138_500_000, 27_915_039_774_476),
),
] {
let market = CurveMarket {
creator_fee_bps,
fees: &decaying,
..MARKET
};
let past = quote(&market, buy).unwrap();
assert_eq!((past.fee_bps, past.fee, past.out_amount), standard);
let inside = quote(
&CurveMarket {
created_at: NOW - 6,
..market
},
buy,
)
.unwrap();
assert_eq!(
(inside.fee_bps, inside.fee, inside.out_amount),
halfway
);
}
}
#[test]
fn an_exact_in_buy_past_the_supply_left_fills_the_cap() {
let quote = quote(
&CurveMarket {
real_base_reserves: 1_000_000_000,
..MARKET
},
request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL),
)
.unwrap();
assert_eq!(quote.out_amount, 1_000_000_000);
assert_eq!(quote.in_amount, 30_305);
assert!(quote.supply_capped);
}
#[test]
fn an_exact_out_buy_past_the_supply_left_is_flagged() {
let wanted = 10_000_000_000_000;
let landing = Landing {
base_fee: Some(BASE_100_BPS),
..LANDING
};
let holding = |real_base_reserves| CurveMarket {
real_base_reserves,
landing,
..MARKET
};
let just_enough = quote(
&holding(10_101_010_101_011),
request(TradeDirection::Buy, TradeMode::ExactOut, wanted),
)
.unwrap();
assert_eq!(just_enough.out_amount, wanted);
assert!(!just_enough.supply_capped);
let one_short = quote(
&holding(10_101_010_101_010),
request(TradeDirection::Buy, TradeMode::ExactOut, wanted),
)
.unwrap();
assert_eq!(one_short.out_amount, 9_999_999_999_999);
assert!(one_short.supply_capped);
}
#[test]
fn a_sell_is_bounded_by_the_quote_the_curve_holds() {
let sell = request(
TradeDirection::Sell,
TradeMode::ExactIn,
10_000_000_000_000,
);
let sold = amm::sell_exact_in_with_fees(SellArgs {
amm: AmmInput {
quote_reserves: 30_000_000_000,
base_reserves: 1_000_000_000_000_000,
amount: 10_000_000_000_000,
fee_bps: 100,
},
quote_fee: None,
base_fee: None,
})
.unwrap();
let debit = sold.quote_amount + sold.fee;
assert_eq!(debit, 297_029_702);
let holding = |real_quote_reserves| CurveMarket {
real_quote_reserves,
..MARKET
};
let covered = quote(&holding(debit), sell).unwrap();
assert_eq!(covered.in_amount, 10_000_000_000_000);
assert_eq!(covered.out_amount, sold.quote_to_user);
assert!(!covered.supply_capped);
assert_eq!(
quote(&holding(debit - 1), sell),
Err(MarketQuoteError::ExceedsQuoteHeld)
);
}
#[test]
fn a_curve_with_no_supply_left_quotes_nothing() {
let complete = CurveMarket {
real_base_reserves: 0,
real_quote_reserves: 85_000_000_000,
..MARKET
};
for direction in [TradeDirection::Buy, TradeDirection::Sell] {
for mode in [TradeMode::ExactIn, TradeMode::ExactOut] {
assert_eq!(
quote(&complete, request(direction, mode, 1_000)),
Err(MarketQuoteError::SupplyExhausted)
);
}
}
}
}