diff --git a/api/src/consts.rs b/api/src/consts.rs index bd88e6a..2958e37 100644 --- a/api/src/consts.rs +++ b/api/src/consts.rs @@ -107,3 +107,12 @@ pub const TREASURY_TOKENS_ADDRESS: Pubkey = Pubkey::new_from_array( ) .0, ); + +/// Denominator for protocol fee calculations. +pub const FEE_RATE_BPS: u64 = 100; + +/// Denominator for fee calculations. +pub const DENOMINATOR_BPS: u64 = 10_000; + +/// Slot window size, used for sandwich resistance. +pub const SLOT_WINDOW: u64 = 4; diff --git a/api/src/error.rs b/api/src/error.rs index ace6867..1350457 100644 --- a/api/src/error.rs +++ b/api/src/error.rs @@ -5,6 +5,15 @@ use steel::*; pub enum OreError { #[error("Placeholder error")] Dummy = 0, + + #[error("Insufficient vault reserves")] + InsufficientVaultReserves = 1, + + #[error("Invariant violation")] + InvariantViolation = 2, + + #[error("Insufficient liquidity")] + InsufficientLiquidity = 3, } error!(OreError); diff --git a/api/src/event.rs b/api/src/event.rs index 5718ef0..b782ba4 100644 --- a/api/src/event.rs +++ b/api/src/event.rs @@ -1,28 +1,39 @@ use steel::*; -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct BuryEvent { - pub amount: u64, - pub ts: u64, -} +use crate::state::SwapDirection; #[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct DeployEvent { - pub authority: Pubkey, - pub amount: u64, - pub ts: u64, +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +pub struct SwapEvent { + /// Swap direction. + pub direction: u64, + + /// Amount of base tokens to transfer. + pub base_to_transfer: u64, + + /// Amount of quote tokens to transfer. + pub quote_to_transfer: u64, + + /// Amount of base tokens swapped via virtual limit order. + pub base_via_order: u64, + + /// Amount of quote tokens swapped via virtual limit order. + pub quote_via_order: u64, + + /// Amount of base tokens swapped via curve. + pub base_via_curve: u64, + + /// Amount of quote tokens swapped via curve. + pub quote_via_curve: u64, + + /// Amount of quote tokens taken in fees. + pub quote_fee: u64, } -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct PayoutEvent { - pub authority: Pubkey, - pub amount: u64, - pub ts: u64, +impl SwapEvent { + pub fn direction(&self) -> SwapDirection { + SwapDirection::try_from(self.direction as u8).unwrap() + } } -event!(BuryEvent); -event!(DeployEvent); -event!(PayoutEvent); +event!(SwapEvent); diff --git a/api/src/state/market.rs b/api/src/state/market.rs deleted file mode 100644 index bfae8f6..0000000 --- a/api/src/state/market.rs +++ /dev/null @@ -1,17 +0,0 @@ -use steel::*; - -use super::OreAccount; - -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct Market { - /// The id of the block this market is associated with. - pub id: u64, - - /// Mint of the hash token. - pub mint: Pubkey, -} - -// TODO Bonding curve stuff - -account!(OreAccount, Market); diff --git a/api/src/state/market/buy_exact_in.rs b/api/src/state/market/buy_exact_in.rs new file mode 100644 index 0000000..38c08a0 --- /dev/null +++ b/api/src/state/market/buy_exact_in.rs @@ -0,0 +1,77 @@ +use crate::error::OreError; + +use super::{Market, SwapDirection, TokenType, VirtualLimitOrder}; +use crate::event::SwapEvent; + +impl Market { + pub fn buy_exact_in(&mut self, quote_in: u64) -> Result { + // Get fee from quote side. + let quote_fee = self.fee(quote_in); + let quote_in_post_fee = quote_in - quote_fee; + + // Upcast data. + let quote_in_post_fee = quote_in_post_fee as u128; + + // Get virtual limit order. + let VirtualLimitOrder { + size_in_base: ask_size_in_base, + size_in_quote: ask_size_in_quote, + } = self.get_virtual_limit_order(SwapDirection::Buy); + + // Execute swap. + let (base_via_ask, quote_via_ask, base_via_curve, quote_via_curve) = + if !self.sandwich_resistance_enabled() { + // Fill entire swap via curve. + let quote_via_curve = quote_in_post_fee; + let base_via_curve = self.get_base_out(quote_via_curve); + self.update_reserves(base_via_curve, quote_via_curve, SwapDirection::Buy); + (0, 0, base_via_curve, quote_via_curve) + } else if ask_size_in_quote >= quote_in_post_fee { + // Fill entire swap via virtual limit order. + let quote_via_ask = quote_in_post_fee; + let base_via_ask = self.get_complementary_limit_order_size( + quote_in_post_fee, + SwapDirection::Buy, + TokenType::Quote, + ); + self.update_reserves(base_via_ask, quote_via_ask, SwapDirection::Buy); + (base_via_ask, quote_via_ask, 0, 0) + } else { + // Partially fill swap via virtual limit order. + let base_via_ask = ask_size_in_base; + let quote_via_ask = ask_size_in_quote; + self.update_reserves(base_via_ask, quote_via_ask, SwapDirection::Buy); + + // Fill remaining swap amount via curve. + let quote_via_curve = quote_in_post_fee - ask_size_in_quote; + let base_via_curve = self.get_base_out(quote_via_curve); + self.update_reserves(base_via_curve, quote_via_curve, SwapDirection::Buy); + (base_via_ask, quote_via_ask, base_via_curve, quote_via_curve) + }; + + // Produce swap result. + let base_out = base_via_ask + base_via_curve; + let swap_event = SwapEvent { + direction: SwapDirection::Buy as u64, + base_to_transfer: base_out as u64, + quote_to_transfer: quote_in, + base_via_order: base_via_ask as u64, + quote_via_order: quote_via_ask as u64, + base_via_curve: base_via_curve as u64, + quote_via_curve: quote_via_curve as u64, + quote_fee: quote_fee as u64, + }; + + // Sanity check swap event. + assert!( + swap_event.base_to_transfer == swap_event.base_via_order + swap_event.base_via_curve + ); + assert!( + swap_event.quote_to_transfer + == swap_event.quote_via_order + swap_event.quote_via_curve + swap_event.quote_fee + ); + + // Return + Ok(swap_event) + } +} diff --git a/api/src/state/market/buy_exact_out.rs b/api/src/state/market/buy_exact_out.rs new file mode 100644 index 0000000..da36500 --- /dev/null +++ b/api/src/state/market/buy_exact_out.rs @@ -0,0 +1,82 @@ +use crate::error::OreError; + +use super::{Market, SwapDirection, TokenType, VirtualLimitOrder}; +use crate::event::SwapEvent; + +impl Market { + pub fn buy_exact_out(&mut self, base_out: u64) -> Result { + // Check if there is enough liquidity. + if self.base.balance < base_out { + return Err(OreError::InsufficientLiquidity); + } + + // Upcast data. + let base_out = base_out as u128; + + // Get virtual limit order. + let VirtualLimitOrder { + size_in_base: ask_size_in_base, + size_in_quote: ask_size_in_quote, + } = self.get_virtual_limit_order(SwapDirection::Buy); + + // Execute swap. + let (base_via_ask, quote_via_ask, base_via_curve, quote_via_curve) = + if !self.sandwich_resistance_enabled() { + // Fill entire swap via curve. + let base_via_curve = base_out; + let quote_via_curve = self.get_quote_in(base_via_curve)?; + self.update_reserves(base_via_curve, quote_via_curve, SwapDirection::Buy); + (0, 0, base_via_curve, quote_via_curve) + } else if ask_size_in_base >= base_out { + // Fill entire swap through virtual limit order. + let base_via_ask = base_out; + let quote_via_ask = self.get_complementary_limit_order_size( + base_via_ask, + SwapDirection::Buy, + TokenType::Base, + ); + self.update_reserves(base_via_ask, quote_via_ask, SwapDirection::Buy); + (base_via_ask, quote_via_ask, 0, 0) + } else { + // Partially fill swap through virtual limit order. + let base_via_ask = ask_size_in_base; + let quote_via_ask = ask_size_in_quote; + self.update_reserves(base_via_ask, quote_via_ask, SwapDirection::Buy); + + // Fill remaining swap amount through pool. + let base_via_curve = base_out - base_via_ask; + let quote_via_curve = self.get_quote_in(base_via_curve)?; + self.update_reserves(base_via_curve, quote_via_curve, SwapDirection::Buy); + (base_via_ask, quote_via_ask, base_via_curve, quote_via_curve) + }; + + // Calculate fee. + let quote_post_fee = quote_via_ask + quote_via_curve; + let quote_in = self.pre_fee(quote_post_fee as u64) as u128; + let quote_fee = quote_in - quote_post_fee; + + // Produce swap result. + let swap_event = SwapEvent { + direction: SwapDirection::Buy as u64, + base_to_transfer: base_out as u64, + quote_to_transfer: quote_in as u64, + base_via_order: base_via_ask as u64, + quote_via_order: quote_via_ask as u64, + base_via_curve: base_via_curve as u64, + quote_via_curve: quote_via_curve as u64, + quote_fee: quote_fee as u64, + }; + + // Sanity check swap result. + assert!( + swap_event.base_to_transfer == swap_event.base_via_order + swap_event.base_via_curve + ); + assert!( + swap_event.quote_to_transfer + == swap_event.quote_via_order + swap_event.quote_via_curve + swap_event.quote_fee + ); + + // Return. + Ok(swap_event) + } +} diff --git a/api/src/state/market/curve.rs b/api/src/state/market/curve.rs new file mode 100644 index 0000000..010e5fa --- /dev/null +++ b/api/src/state/market/curve.rs @@ -0,0 +1,52 @@ +use crate::error::OreError; + +use super::Market; + +// TODO Add weights. + +impl Market { + /// Returns the constant product invariant. + pub(crate) fn k(&self) -> u128 { + let base_reserves = self.base.balance as u128; + let quote_reserves = self.quote.balance as u128; + (base_reserves * quote_reserves).saturating_sub(1) + } + + /// Returns the amount of base tokens that can be bought from a given amount of quote tokens. + pub fn get_base_out(&self, quote_in: u128) -> u128 { + let base_reserves = self.base.balance as u128; + let quote_reserves = self.quote.balance as u128; + let base_out = base_reserves - (self.k() / (quote_reserves + quote_in)).saturating_add(1); + base_out + } + + /// Returns the amount of quote tokens received from selling a given amount of base tokens. + pub fn get_quote_out(&self, base_in: u128) -> u128 { + let base_reserves = self.base.balance as u128; + let quote_reserves = self.quote.balance as u128; + let quote_out = quote_reserves - (self.k() / (base_reserves + base_in)).saturating_add(1); + quote_out + } + + /// Returns the amount of quote tokens needed to buy a given amount of base tokens. + pub fn get_quote_in(&self, base_out: u128) -> Result { + let base_reserves = self.base.balance as u128; + let quote_reserves = self.quote.balance as u128; + if base_out >= base_reserves { + return Err(OreError::InsufficientVaultReserves.into()); + } + let quote_in = (self.k() / (base_reserves - base_out)).saturating_add(1) - quote_reserves; + Ok(quote_in) + } + + /// Returns the amount of base tokens which must be sold to receive a given amount of quote tokens. + pub fn get_base_in(&self, quote_out: u128) -> Result { + let base_reserves = self.base.balance as u128; + let quote_reserves = self.quote.balance as u128; + if quote_out >= quote_reserves { + return Err(OreError::InsufficientVaultReserves.into()); + } + let base_in = (self.k() / (quote_reserves - quote_out)).saturating_add(1) - base_reserves; + Ok(base_in) + } +} diff --git a/api/src/state/market/fees.rs b/api/src/state/market/fees.rs new file mode 100644 index 0000000..23c5986 --- /dev/null +++ b/api/src/state/market/fees.rs @@ -0,0 +1,24 @@ +use crate::consts::*; + +use super::Market; + +impl Market { + pub(crate) fn apply_fees(&mut self, quote_fee: u64) { + // Process protocol fees. + self.fee.cumulative += quote_fee; + self.fee.uncollected += quote_fee; + } + + /// Calculates the fee from a quote amount. + pub(crate) fn fee(&self, quote_size: u64) -> u64 { + quote_size * self.fee.rate / DENOMINATOR_BPS + } + + /// Calculates the pre-fee quote from a post-fee quote amount. + pub(crate) fn pre_fee(&self, quote_post_fee: u64) -> u64 { + // x * 10000 / (10000 - fee) is approximately equivalent to x * (1 - fee / 10000) + let numerator = quote_post_fee * DENOMINATOR_BPS; + let denominator = DENOMINATOR_BPS - self.fee.rate; + return numerator / denominator; + } +} diff --git a/api/src/state/market/market.rs b/api/src/state/market/market.rs new file mode 100644 index 0000000..71eddb0 --- /dev/null +++ b/api/src/state/market/market.rs @@ -0,0 +1,375 @@ +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, + } + } +} diff --git a/api/src/state/market/mod.rs b/api/src/state/market/mod.rs new file mode 100644 index 0000000..6624648 --- /dev/null +++ b/api/src/state/market/mod.rs @@ -0,0 +1,12 @@ +mod buy_exact_in; +mod buy_exact_out; +mod curve; +mod fees; +mod market; +mod sell_exact_in; +mod sell_exact_out; +mod swap; +mod vaults; +mod virtual_limit_order; + +pub use market::*; diff --git a/api/src/state/market/sell_exact_in.rs b/api/src/state/market/sell_exact_in.rs new file mode 100644 index 0000000..32739f0 --- /dev/null +++ b/api/src/state/market/sell_exact_in.rs @@ -0,0 +1,85 @@ +use crate::error::OreError; + +use super::{Market, SwapDirection, TokenType, VirtualLimitOrder}; +use crate::event::SwapEvent; + +impl Market { + pub fn sell_exact_in(&mut self, base_in: u64) -> Result { + // Get fee from quote side. + let mut quote_fee = 0; + + // Upcast data. + let base_in = base_in as u128; + + // Get virtual limit order. + let VirtualLimitOrder { + size_in_base: bid_size_in_base, + size_in_quote: bid_size_in_quote, + } = self.get_virtual_limit_order(SwapDirection::Sell); + + // Execute swap. + let (base_via_bid, quote_via_bid, base_via_curve, quote_via_curve) = + if !self.sandwich_resistance_enabled() { + // Fill entire swap via curve. + let base_via_curve = base_in; + let mut quote_via_curve = self.get_quote_out(base_via_curve); + self.update_reserves(base_via_curve, quote_via_curve, SwapDirection::Sell); + let swap_fee = self.fee(quote_via_curve as u64); + quote_fee += swap_fee; + quote_via_curve -= swap_fee as u128; + (0, 0, base_via_curve, quote_via_curve) + } else if bid_size_in_base >= base_in { + // Fill entire swap through virtual limit order. + let base_via_bid = base_in; + let mut quote_via_bid = self.get_complementary_limit_order_size( + base_in, + SwapDirection::Sell, + TokenType::Base, + ); + quote_fee += self.fee(quote_via_bid as u64); + self.update_reserves(base_via_bid, quote_via_bid, SwapDirection::Sell); + quote_via_bid -= quote_fee as u128; + (base_via_bid, quote_via_bid, 0, 0) + } else { + // Partially fill swap through virtual limit order. + let base_via_bid = bid_size_in_base; + let mut quote_via_bid = bid_size_in_quote; + quote_fee += self.fee(quote_via_bid as u64); + self.update_reserves(base_via_bid, quote_via_bid, SwapDirection::Sell); + quote_via_bid -= quote_fee as u128; + + // Fill remaining swap through pool. + let base_via_curve = base_in - base_via_bid; + let mut quote_via_curve = self.get_quote_out(base_via_curve); + self.update_reserves(base_via_curve, quote_via_curve, SwapDirection::Sell); + let swap_fee = self.fee(quote_via_curve as u64); + quote_fee += swap_fee; + quote_via_curve -= swap_fee as u128; + (base_via_bid, quote_via_bid, base_via_curve, quote_via_curve) + }; + + // Produce swap result. + let quote_out = quote_via_bid + quote_via_curve; + let swap_event = SwapEvent { + direction: SwapDirection::Sell as u64, + base_to_transfer: base_in as u64, + quote_to_transfer: quote_out as u64, + base_via_order: base_via_bid as u64, + quote_via_order: quote_via_bid as u64, + base_via_curve: base_via_curve as u64, + quote_via_curve: quote_via_curve as u64, + quote_fee: quote_fee as u64, + }; + + // Sanity check swap result. + assert!( + swap_event.base_to_transfer == swap_event.base_via_order + swap_event.base_via_curve + ); + assert!( + swap_event.quote_to_transfer == swap_event.quote_via_order + swap_event.quote_via_curve + ); + + // Return. + Ok(swap_event) + } +} diff --git a/api/src/state/market/sell_exact_out.rs b/api/src/state/market/sell_exact_out.rs new file mode 100644 index 0000000..57bdaf6 --- /dev/null +++ b/api/src/state/market/sell_exact_out.rs @@ -0,0 +1,84 @@ +use crate::error::OreError; + +use super::{Market, SwapDirection, TokenType, VirtualLimitOrder}; +use crate::event::SwapEvent; + +impl Market { + pub fn sell_exact_out(&mut self, quote_out: u64) -> Result { + // Check if there is enough liquidity. + if self.quote.balance < quote_out { + return Err(OreError::InsufficientLiquidity); + } + + // Calculate fee. + let quote_out_pre_fee = self.pre_fee(quote_out) as u128; + let quote_fee = quote_out_pre_fee - quote_out as u128; + + // Upcast data. + let quote_out = quote_out as u128; + + // Get virtual limit order. + let VirtualLimitOrder { + size_in_base: bid_size_in_base, + size_in_quote: bid_size_in_quote, + } = self.get_virtual_limit_order(SwapDirection::Sell); + + // Execute swap. + let (base_via_bid, quote_via_bid, base_via_curve, quote_via_curve) = + if !self.sandwich_resistance_enabled() { + // Fill entire swap via curve. + let quote_via_curve = quote_out_pre_fee; + let base_via_curve = self.get_base_in(quote_via_curve)?; + self.update_reserves(base_via_curve, quote_via_curve, SwapDirection::Sell); + (0, 0, base_via_curve, quote_via_curve) + } else if bid_size_in_quote >= quote_out { + // Fill entire swap through virtual limit order. + let quote_via_bid = quote_out_pre_fee; + let base_via_bid = self.get_complementary_limit_order_size( + quote_via_bid, + SwapDirection::Sell, + TokenType::Quote, + ); + self.update_reserves(base_via_bid, quote_via_bid, SwapDirection::Sell); + (base_via_bid, quote_via_bid, 0, 0) + } else { + // Partially fill swap through virtual limit order. + let base_via_bid = bid_size_in_base; + let quote_via_bid = bid_size_in_quote; + self.update_reserves(base_via_bid, quote_via_bid, SwapDirection::Sell); + + // Fill remaining swap amount through pool. + let quote_via_curve = quote_out_pre_fee - quote_via_bid; + let base_via_curve = self.get_base_in(quote_via_curve)?; + self.update_reserves(base_via_curve, quote_via_curve, SwapDirection::Sell); + (base_via_bid, quote_via_bid, base_via_curve, quote_via_curve) + }; + + // Calculate fee. + let base_in = base_via_bid + base_via_curve; + + // Produce swap result. + let swap_event = SwapEvent { + direction: SwapDirection::Sell as u64, + base_to_transfer: base_in as u64, + quote_to_transfer: quote_out as u64, + base_via_order: base_via_bid as u64, + quote_via_order: quote_via_bid as u64, + base_via_curve: base_via_curve as u64, + quote_via_curve: quote_via_curve as u64, + quote_fee: quote_fee as u64, + }; + + // Sanity check swap result. + assert!( + swap_event.base_to_transfer == swap_event.base_via_order + swap_event.base_via_curve + ); + assert!( + swap_event.quote_to_transfer + == swap_event.quote_via_order + swap_event.quote_via_curve - swap_event.quote_fee + ); + + // Return. + Ok(swap_event) + } +} diff --git a/api/src/state/market/swap.rs b/api/src/state/market/swap.rs new file mode 100644 index 0000000..0959d9d --- /dev/null +++ b/api/src/state/market/swap.rs @@ -0,0 +1,42 @@ +use steel::Clock; + +use crate::error::OreError; + +use super::{Market, SwapDirection, SwapPrecision}; +use crate::event::SwapEvent; + +impl Market { + pub fn swap( + &mut self, + amount: u64, + direction: SwapDirection, + precision: SwapPrecision, + clock: Clock, + ) -> Result { + // Update snapshot. + self.update_snapshot(clock); + + // Get invariant. + let k_pre = self.k(); + + // Execute swap. + let swap_event = match (direction, precision) { + (SwapDirection::Buy, SwapPrecision::ExactIn) => self.buy_exact_in(amount)?, + (SwapDirection::Buy, SwapPrecision::ExactOut) => self.buy_exact_out(amount)?, + (SwapDirection::Sell, SwapPrecision::ExactIn) => self.sell_exact_in(amount)?, + (SwapDirection::Sell, SwapPrecision::ExactOut) => self.sell_exact_out(amount)?, + }; + + // Check invariant. + let k_post = self.k(); + if k_pre > k_post { + return Err(OreError::InvariantViolation.into()); + } + + // Apply fees. + self.apply_fees(swap_event.quote_fee); + + // Return. + Ok(swap_event) + } +} diff --git a/api/src/state/market/vaults.rs b/api/src/state/market/vaults.rs new file mode 100644 index 0000000..2df5151 --- /dev/null +++ b/api/src/state/market/vaults.rs @@ -0,0 +1,43 @@ +use solana_program::log::sol_log; +use steel::*; + +use crate::error::OreError; + +use super::Market; + +/// Vault reserve checks. +impl Market { + /// Sanity check that vaults have reserves for all market debts. + /// Assumes the token accounts have already been validated as the market's base and quote vaults. + pub fn check_vaults( + &self, + base_vault: &TokenAccount, + quote_vault: &TokenAccount, + ) -> Result<(), OreError> { + self.check_base_vault(base_vault)?; + self.check_quote_vault(quote_vault)?; + Ok(()) + } + + /// Sanity check that base vault has reserves for all market debts. + /// Assumes the token account has already been validated as the market's base vault. + pub fn check_base_vault(&self, base_vault: &TokenAccount) -> Result<(), OreError> { + if base_vault.amount() < self.base.balance { + sol_log(&format!("A base_vault.amount: {}", base_vault.amount())); + sol_log(&format!("A self.base.balance: {}", self.base.balance)); + sol_log("Insufficient base vault reserves"); + return Err(OreError::InsufficientVaultReserves.into()); + } + Ok(()) + } + + /// Sanity check that quote vault has reserves for all market debts. + /// Assumes the token account has already been validated as the market's quote vault. + pub fn check_quote_vault(&self, quote_vault: &TokenAccount) -> Result<(), OreError> { + if quote_vault.amount() < self.quote.balance + self.fee.uncollected { + sol_log("Insufficient quote vault reserves"); + return Err(OreError::InsufficientVaultReserves.into()); + } + Ok(()) + } +} diff --git a/api/src/state/market/virtual_limit_order.rs b/api/src/state/market/virtual_limit_order.rs new file mode 100644 index 0000000..1a2f861 --- /dev/null +++ b/api/src/state/market/virtual_limit_order.rs @@ -0,0 +1,146 @@ +use steel::Clock; + +use crate::consts::SLOT_WINDOW; + +use super::{Market, SwapDirection, TokenType, VirtualLimitOrder}; + +impl Market { + /// This function solves the closed-form solution for the size of the virtual limit order + /// in the pool. The virutal limit order is always priced at the snapshot price. + /// + /// The size of the limit order is determined by the following constraint: + /// + /// ```no_run + /// (quote_snapshot / base_snapshot) = (quote_reserves + ∆_quote) / (base_reserves + ∆_base) + /// ``` + /// + /// Note that the signs of ∆_quote and ∆_base are always flipped. + /// + /// This means that the size of the limit order is set such that the new pool price + /// after the swap is equal to the price at the snapshot. + /// + /// Because we know the limit order is priced at the snapshot price, we can derive + /// the following equations: + /// - ∆_base = -∆_quote * base_snapshot / quote_snapshot + /// - ∆_quote = -∆_base * quote_snapshot / base_snapshot + /// + /// + /// We can then solve for ∆_base and ∆_quote after substituting the above equations. There are separate cases + /// for buy and sell + /// + /// - Limit order on the buy side (bid) + /// ```no_run + /// ∆_base = (base_snapshot * quote_reserves - quote_snapshot * base_reserves) / (2 * quote_snapshot) + /// ∆_quote = (base_snapshot * quote_reserves - quote_snapshot * base_reserves) / (2 * base_snapshot) + /// ``` + /// + /// - Limit order on the sell side (ask) + /// ```no_run + /// ∆_base = (quote_snapshot * base_reserves - base_snapshot * quote_reserves) / (2 * quote_snapshot) + /// ∆_quote = (quote_snapshot * base_reserves - base_snapshot * quote_reserves) / (2 * base_snapshot) + /// ``` + pub fn get_virtual_limit_order(&self, direction: SwapDirection) -> VirtualLimitOrder { + // Upcast data. + let base_balance = self.base.balance as u128; + let quote_balance = self.quote.balance as u128; + let base_snapshot = self.snapshot.base_balance as u128; + let quote_snapshot = self.snapshot.quote_balance as u128; + + // Get virtual limit order. + match direction { + SwapDirection::Buy => { + let ask = if quote_snapshot * base_balance > base_snapshot * quote_balance { + let size_in_quote = (quote_snapshot * base_balance + - base_snapshot * quote_balance) + / (2 * base_snapshot); + let size_in_base = size_in_quote * base_snapshot / quote_snapshot; + VirtualLimitOrder { + size_in_base, + size_in_quote, + } + } else { + VirtualLimitOrder::default() + }; + ask + } + SwapDirection::Sell => { + let bid = if base_snapshot * quote_balance > quote_snapshot * base_balance { + let size_in_base = (base_snapshot * quote_balance + - quote_snapshot * base_balance) + / (2 * quote_snapshot); + let size_in_quote = size_in_base * quote_snapshot / base_snapshot; + VirtualLimitOrder { + size_in_base, + size_in_quote, + } + } else { + VirtualLimitOrder::default() + }; + bid + } + } + } + + /// This function returns the size of the virtual limit order in the complementary token type + /// given an `amount` and the `input_token_type`. + /// - If the `input_token_type` is Base, then the size of the limit order in Quote is computed. + /// - If the `input_token_type` is Quote, then the size of the limit order in Base is computed. + pub(crate) fn get_complementary_limit_order_size( + &self, + amount: u128, + direction: SwapDirection, + token_type: TokenType, + ) -> u128 { + if amount == 0 { + return 0; + } + let quote_snapshot = self.snapshot.quote_balance as u128; + let base_snapshot = self.snapshot.base_balance as u128; + + match direction { + SwapDirection::Buy => { + match token_type { + // If `amount` is in base, then the size of the limit order in quote is computed and rounded up + TokenType::Base => ((amount * quote_snapshot).saturating_sub(1) + / base_snapshot) + .saturating_add(1), + // If `amount` is in quote, then the size of the limit order in base is computed + TokenType::Quote => amount * base_snapshot / quote_snapshot, + } + } + SwapDirection::Sell => { + match token_type { + // If `amount` is in base, then the size of the limit order in quote is computed + TokenType::Base => amount * quote_snapshot / base_snapshot, + // If `amount` is in quote, then the size of the limit order in base is computed and rounded up + TokenType::Quote => ((amount * base_snapshot).saturating_sub(1) + / quote_snapshot) + .saturating_add(1), + } + } + } + } + + pub(crate) fn update_snapshot(&mut self, clock: Clock) { + let slot = clock.slot; + let snapshot_slot = (slot / SLOT_WINDOW) * SLOT_WINDOW; + if snapshot_slot != self.snapshot.slot { + self.snapshot.slot = snapshot_slot; + self.snapshot.base_balance = self.base.balance; + self.snapshot.quote_balance = self.quote.balance; + } + } + + pub(crate) fn update_reserves(&mut self, base: u128, quote: u128, direction: SwapDirection) { + match direction { + SwapDirection::Buy => { + self.base.balance -= base as u64; + self.quote.balance += quote as u64; + } + SwapDirection::Sell => { + self.base.balance += base as u64; + self.quote.balance -= quote as u64; + } + } + } +} diff --git a/program/src/close.rs b/program/src/close.rs index 22e140b..f6f64d6 100644 --- a/program/src/close.rs +++ b/program/src/close.rs @@ -21,8 +21,8 @@ pub fn process_close(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul market_hash_info.as_associated_token_account(market_info.key, mint_hash_info.key)?; let market_ore = market_ore_info.as_associated_token_account(market_info.key, mint_ore_info.key)?; - mint_hash_info.has_address(&market.mint)?; - mint_ore_info.has_address(&MINT_ADDRESS)?; + mint_hash_info.has_address(&market.base.mint)?.as_mint(); + mint_ore_info.has_address(&market.quote.mint)?.as_mint(); system_program.is_program(&system_program::ID)?; token_program.is_program(&spl_token::ID)?;