Files
ore/api/src/state/market/market.rs
Hardhat Chad 8f6a40ea17 market
2025-06-05 16:56:03 -07:00

376 lines
10 KiB
Rust

use spl_associated_token_account::get_associated_token_address;
use steel::*;
use crate::state::{market_pda, OreAccount};
// TODO Bonding curve stuff
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct Market {
/// Base token parameters.
pub base: TokenParams,
/// Quote token parameters.
pub quote: TokenParams,
/// Fee parameters.
pub fee: FeeParams,
/// Snapshot of the market state at the time of the last swap.
pub snapshot: Snapshot,
/// The id of the block this market is associated with.
pub id: u64,
}
impl Market {
pub fn pda(&self) -> (Pubkey, u8) {
market_pda(self.id)
}
pub fn base_vault(&self) -> Pubkey {
get_associated_token_address(&self.pda().0, &self.base.mint)
}
pub fn quote_vault(&self) -> Pubkey {
get_associated_token_address(&self.pda().0, &self.quote.mint)
}
pub fn sandwich_resistance_enabled(&self) -> bool {
self.snapshot.enabled == 1
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct TokenParams {
/// Mint of the token.
pub mint: Pubkey,
/// Amount of token held in liquidity.
pub balance: u64,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct FeeParams {
/// Cumulative protocol fees.
pub cumulative: u64,
/// Fee rate in basis points.
pub rate: u64,
/// Current uncollected protocol fees.
pub uncollected: u64,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct Snapshot {
/// Whether sandwich resistance is enabled.
pub enabled: u64,
/// Base token balance at the time of the snapshot.
pub base_balance: u64,
/// Quote token balance at the time of the snapshot.
pub quote_balance: u64,
/// Slot at which the snapshot was taken.
pub slot: u64,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
pub struct VirtualLimitOrder {
/// Size of the virtual limit order in base tokens.
pub size_in_base: u128,
/// Size of the virtual limit order in quote tokens.
pub size_in_quote: u128,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum SwapDirection {
/// Swap quote tokens for base tokens.
Buy = 0,
/// Swap base tokens for quote tokens.
Sell = 1,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum SwapPrecision {
/// Swap with precision exact in amount.
ExactIn = 0,
/// Swap with precision exact out amount.
ExactOut = 1,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum TokenType {
/// Base token.
Base = 0,
/// Quote token.
Quote = 1,
}
account!(OreAccount, Market);
#[cfg(test)]
mod tests {
use crate::consts::FEE_RATE_BPS;
use super::*;
#[test]
fn test_fees_buy_exact_in() {
let mut market = new_market();
let swap = market
.swap(
100_000,
SwapDirection::Buy,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
assert_eq!(swap.quote_via_curve, 99_000);
assert_eq!(market.fee.uncollected, 1000); // Protocol fee is 1%
}
#[test]
fn test_fees_sell_exact_in() {
let mut market = new_market();
let swap = market
.swap(
100_000,
SwapDirection::Sell,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
assert_eq!(swap.quote_via_curve, 98_991);
assert_eq!(market.fee.uncollected, 999);
}
#[test]
fn test_fees_buy_exact_out() {
let mut market = new_market();
let swap = market
.swap(
100_000,
SwapDirection::Buy,
SwapPrecision::ExactOut,
Clock::default(),
)
.unwrap();
assert_eq!(swap.quote_via_curve, 100_011);
assert_eq!(market.fee.uncollected, 1010);
}
#[test]
fn test_fees_sell_exact_out() {
let mut market = new_market();
let swap = market
.swap(
100_000,
SwapDirection::Sell,
SwapPrecision::ExactOut,
Clock::default(),
)
.unwrap();
assert_eq!(swap.quote_via_curve, 101_010);
assert_eq!(market.fee.uncollected, 1010);
}
#[test]
fn test_fills() {
let mut market = new_market();
// Small buy
// Assert swap is filled via curve.
// Post swap, price is above snapshot.
let swap_1 = market
.swap(
100_000,
SwapDirection::Buy,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
assert!(swap_1.base_via_curve > 0 && swap_1.quote_via_curve > 0);
assert!(swap_1.base_via_order == 0 && swap_1.quote_via_order == 0);
// Large sell
// Assert swap is partially filled via order and partially filled via curve.
// Post swap, price is below snapshot.
let swap_2 = market
.swap(
1_000_000,
SwapDirection::Sell,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
assert!(swap_2.base_via_curve > 0 && swap_2.quote_via_curve > 0);
assert!(swap_2.base_via_order > 0 && swap_2.quote_via_order > 0);
// Small buy
// Assert swap is filled via order
// Post swap, price is still below snapshot.
let swap_3 = market
.swap(
1_000,
SwapDirection::Buy,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
assert!(swap_3.base_via_curve == 0 && swap_3.quote_via_curve == 0);
assert!(swap_3.base_via_order > 0 && swap_3.quote_via_order > 0);
// Large buy
// Assert swap is partially filled via order and partially filled via curve.
// Post swap, price is above snapshot.
let swap_4 = market
.swap(
1_000_000,
SwapDirection::Buy,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
assert!(swap_4.base_via_curve > 0 && swap_4.quote_via_curve > 0);
assert!(swap_4.base_via_order > 0 && swap_4.quote_via_order > 0);
}
#[test]
fn test_sandwich() {
let mut market = new_market();
market.fee.rate = 0;
market.snapshot = Snapshot {
enabled: 0,
base_balance: 0,
quote_balance: 0,
slot: 0,
};
// Open sandwich
let swap_1 = market
.swap(
100_000,
SwapDirection::Buy,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
let amount_base_1 = swap_1.base_to_transfer;
assert!(swap_1.base_via_curve > 0 && swap_1.quote_via_curve > 0);
assert!(swap_1.base_via_order == 0 && swap_1.quote_via_order == 0);
// Victim buys
let swap_2 = market
.swap(
100_000,
SwapDirection::Buy,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
assert!(swap_2.base_via_curve > 0 && swap_2.quote_via_curve > 0);
assert!(swap_2.base_via_order == 0 && swap_2.quote_via_order == 0);
// Close sandwich
let swap_3 = market
.swap(
amount_base_1,
SwapDirection::Sell,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
assert!(swap_3.base_via_curve > 0 && swap_3.quote_via_curve > 0);
assert!(swap_3.base_via_order == 0 && swap_3.quote_via_order == 0);
// Assert sandwich attack succeeded
assert!(swap_3.quote_to_transfer > swap_1.quote_to_transfer);
}
#[test]
fn test_sandwich_resistance() {
let mut market = new_market();
market.fee.rate = 0;
// Open sandwich
let swap_1 = market
.swap(
100_000,
SwapDirection::Buy,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
let amount_base_1 = swap_1.base_to_transfer;
assert!(swap_1.base_via_curve > 0 && swap_1.quote_via_curve > 0);
assert!(swap_1.base_via_order == 0 && swap_1.quote_via_order == 0);
// Victim buys
let swap_2 = market
.swap(
100_000,
SwapDirection::Buy,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
assert!(swap_2.base_via_curve > 0 && swap_2.quote_via_curve > 0);
assert!(swap_2.base_via_order == 0 && swap_2.quote_via_order == 0);
// Close sandwich
let swap_3 = market
.swap(
amount_base_1,
SwapDirection::Sell,
SwapPrecision::ExactIn,
Clock::default(),
)
.unwrap();
assert!(swap_3.base_via_curve == 0 && swap_3.quote_via_curve == 0);
assert!(swap_3.base_via_order > 0 && swap_3.quote_via_order > 0);
// Assert sandwich attack failed
assert!(swap_3.quote_to_transfer <= swap_1.quote_to_transfer);
}
fn new_market() -> Market {
Market {
base: TokenParams {
mint: Pubkey::new_unique(),
balance: 1_000_000_000,
},
quote: TokenParams {
mint: Pubkey::new_unique(),
balance: 1_000_000_000,
},
fee: FeeParams {
cumulative: 0,
uncollected: 0,
rate: FEE_RATE_BPS,
},
snapshot: Snapshot {
enabled: 1,
base_balance: 1_000_000_000,
quote_balance: 1_000_000_000,
slot: 0,
},
id: 0,
}
}
}