diff --git a/api/src/state/market/buy_exact_in.rs b/api/src/state/market/buy_exact_in.rs index 38c08a0..9b25b03 100644 --- a/api/src/state/market/buy_exact_in.rs +++ b/api/src/state/market/buy_exact_in.rs @@ -24,7 +24,7 @@ impl Market { // 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); + 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. @@ -34,18 +34,18 @@ impl Market { SwapDirection::Buy, TokenType::Quote, ); - self.update_reserves(base_via_ask, quote_via_ask, SwapDirection::Buy); + 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); + 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); + self.update_reserves(base_via_curve, quote_via_curve, SwapDirection::Buy)?; (base_via_ask, quote_via_ask, base_via_curve, quote_via_curve) }; diff --git a/api/src/state/market/buy_exact_out.rs b/api/src/state/market/buy_exact_out.rs index da36500..4aee03d 100644 --- a/api/src/state/market/buy_exact_out.rs +++ b/api/src/state/market/buy_exact_out.rs @@ -25,7 +25,7 @@ impl Market { // 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); + 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. @@ -35,18 +35,18 @@ impl Market { SwapDirection::Buy, TokenType::Base, ); - self.update_reserves(base_via_ask, quote_via_ask, SwapDirection::Buy); + 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); + 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); + self.update_reserves(base_via_curve, quote_via_curve, SwapDirection::Buy)?; (base_via_ask, quote_via_ask, base_via_curve, quote_via_curve) }; diff --git a/api/src/state/market/curve.rs b/api/src/state/market/curve.rs index 010e5fa..8f6a1f6 100644 --- a/api/src/state/market/curve.rs +++ b/api/src/state/market/curve.rs @@ -7,46 +7,40 @@ use super::Market; 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) + (self.base.reserves() * self.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); + let base_out = self.base.reserves() + - (self.k() / (self.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); + let quote_out = + self.quote.reserves() - (self.k() / (self.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 { + if base_out >= self.base.reserves() { return Err(OreError::InsufficientVaultReserves.into()); } - let quote_in = (self.k() / (base_reserves - base_out)).saturating_add(1) - quote_reserves; + let quote_in = (self.k() / (self.base.reserves() - base_out)).saturating_add(1) + - self.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 { + if quote_out >= self.quote.reserves() { return Err(OreError::InsufficientVaultReserves.into()); } - let base_in = (self.k() / (quote_reserves - quote_out)).saturating_add(1) - base_reserves; + let base_in = (self.k() / (self.quote.reserves() - quote_out)).saturating_add(1) + - self.base.reserves(); Ok(base_in) } } diff --git a/api/src/state/market/market.rs b/api/src/state/market/market.rs index 71eddb0..062322a 100644 --- a/api/src/state/market/market.rs +++ b/api/src/state/market/market.rs @@ -48,8 +48,17 @@ pub struct TokenParams { /// Mint of the token. pub mint: Pubkey, - /// Amount of token held in liquidity. + /// Amount of tokens held in liquidity. pub balance: u64, + + /// Amount of virtual tokens held in liquidity. + pub balance_virtual: u64, +} + +impl TokenParams { + pub fn reserves(&self) -> u128 { + (self.balance + self.balance_virtual) as u128 + } } #[repr(C)] @@ -192,6 +201,8 @@ mod tests { #[test] fn test_fills() { let mut market = new_market(); + let mut clock = Clock::default(); + clock.slot = 10; // Small buy // Assert swap is filled via curve. @@ -201,7 +212,7 @@ mod tests { 100_000, SwapDirection::Buy, SwapPrecision::ExactIn, - Clock::default(), + clock.clone(), ) .unwrap(); assert!(swap_1.base_via_curve > 0 && swap_1.quote_via_curve > 0); @@ -215,7 +226,7 @@ mod tests { 1_000_000, SwapDirection::Sell, SwapPrecision::ExactIn, - Clock::default(), + clock.clone(), ) .unwrap(); assert!(swap_2.base_via_curve > 0 && swap_2.quote_via_curve > 0); @@ -229,7 +240,7 @@ mod tests { 1_000, SwapDirection::Buy, SwapPrecision::ExactIn, - Clock::default(), + clock.clone(), ) .unwrap(); assert!(swap_3.base_via_curve == 0 && swap_3.quote_via_curve == 0); @@ -243,7 +254,7 @@ mod tests { 1_000_000, SwapDirection::Buy, SwapPrecision::ExactIn, - Clock::default(), + clock.clone(), ) .unwrap(); assert!(swap_4.base_via_curve > 0 && swap_4.quote_via_curve > 0); @@ -261,13 +272,16 @@ mod tests { slot: 0, }; + let mut clock = Clock::default(); + clock.slot = 10; + // Open sandwich let swap_1 = market .swap( 100_000, SwapDirection::Buy, SwapPrecision::ExactIn, - Clock::default(), + clock.clone(), ) .unwrap(); let amount_base_1 = swap_1.base_to_transfer; @@ -280,7 +294,7 @@ mod tests { 100_000, SwapDirection::Buy, SwapPrecision::ExactIn, - Clock::default(), + clock.clone(), ) .unwrap(); assert!(swap_2.base_via_curve > 0 && swap_2.quote_via_curve > 0); @@ -292,7 +306,7 @@ mod tests { amount_base_1, SwapDirection::Sell, SwapPrecision::ExactIn, - Clock::default(), + clock.clone(), ) .unwrap(); assert!(swap_3.base_via_curve > 0 && swap_3.quote_via_curve > 0); @@ -307,13 +321,16 @@ mod tests { let mut market = new_market(); market.fee.rate = 0; + let mut clock = Clock::default(); + clock.slot = 10; + // Open sandwich let swap_1 = market .swap( 100_000, SwapDirection::Buy, SwapPrecision::ExactIn, - Clock::default(), + clock.clone(), ) .unwrap(); let amount_base_1 = swap_1.base_to_transfer; @@ -326,7 +343,7 @@ mod tests { 100_000, SwapDirection::Buy, SwapPrecision::ExactIn, - Clock::default(), + clock.clone(), ) .unwrap(); assert!(swap_2.base_via_curve > 0 && swap_2.quote_via_curve > 0); @@ -338,7 +355,7 @@ mod tests { amount_base_1, SwapDirection::Sell, SwapPrecision::ExactIn, - Clock::default(), + clock.clone(), ) .unwrap(); assert!(swap_3.base_via_curve == 0 && swap_3.quote_via_curve == 0); @@ -348,15 +365,78 @@ mod tests { assert!(swap_3.quote_to_transfer <= swap_1.quote_to_transfer); } + #[test] + fn test_virtual_liquidity() { + let mut market = new_market(); + market.fee.rate = 0; + market.quote.balance_virtual = 1_000_000_000; + market.quote.balance = 0; + + let mut clock = Clock::default(); + clock.slot = 10; + + // Sell + // Assert swap fails without real liquidity to satisfy order. + let swap = market.swap( + 100_000, + SwapDirection::Sell, + SwapPrecision::ExactIn, + clock.clone(), + ); + assert!(swap.is_err()); + + // Buy + // Assert buy succeeds adding liquidity. + let swap = market + .swap( + 100_000, + SwapDirection::Buy, + SwapPrecision::ExactIn, + clock.clone(), + ) + .unwrap(); + assert!(swap.base_via_curve > 0 && swap.quote_via_curve > 0); + assert!(swap.base_via_order == 0 && swap.quote_via_order == 0); + assert_eq!(market.quote.balance, 100_000); + assert_eq!(market.quote.balance_virtual, 1_000_000_000); + + // Sell + // Assert sell fails if there is insufficient liquidity. + let swap = market.swap( + 100_001, + SwapDirection::Sell, + SwapPrecision::ExactOut, + clock.clone(), + ); + assert!(swap.is_err()); + + // Sell + // Assert sell succeeds removing liquidity. + let swap = market + .swap( + 100_000, + SwapDirection::Sell, + SwapPrecision::ExactOut, + clock.clone(), + ) + .unwrap(); + assert!(swap.base_via_curve > 0 && swap.quote_via_curve > 0); + assert!(swap.base_via_order > 0 && swap.quote_via_order > 0); + assert_eq!(market.quote.balance, 0); + assert_eq!(market.quote.balance_virtual, 1_000_000_000); + } + fn new_market() -> Market { Market { base: TokenParams { mint: Pubkey::new_unique(), balance: 1_000_000_000, + balance_virtual: 0, }, quote: TokenParams { mint: Pubkey::new_unique(), balance: 1_000_000_000, + balance_virtual: 0, }, fee: FeeParams { cumulative: 0, @@ -365,8 +445,8 @@ mod tests { }, snapshot: Snapshot { enabled: 1, - base_balance: 1_000_000_000, - quote_balance: 1_000_000_000, + base_balance: 0, + quote_balance: 0, slot: 0, }, id: 0, diff --git a/api/src/state/market/sell_exact_in.rs b/api/src/state/market/sell_exact_in.rs index 32739f0..095b338 100644 --- a/api/src/state/market/sell_exact_in.rs +++ b/api/src/state/market/sell_exact_in.rs @@ -23,7 +23,7 @@ impl Market { // 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); + 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; @@ -37,7 +37,7 @@ impl Market { TokenType::Base, ); quote_fee += self.fee(quote_via_bid as u64); - self.update_reserves(base_via_bid, quote_via_bid, SwapDirection::Sell); + 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 { @@ -45,13 +45,13 @@ impl Market { 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); + 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); + 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; diff --git a/api/src/state/market/sell_exact_out.rs b/api/src/state/market/sell_exact_out.rs index 57bdaf6..4d756b8 100644 --- a/api/src/state/market/sell_exact_out.rs +++ b/api/src/state/market/sell_exact_out.rs @@ -29,7 +29,7 @@ impl Market { // 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); + 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. @@ -39,18 +39,18 @@ impl Market { SwapDirection::Sell, TokenType::Quote, ); - self.update_reserves(base_via_bid, quote_via_bid, SwapDirection::Sell); + 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); + 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); + self.update_reserves(base_via_curve, quote_via_curve, SwapDirection::Sell)?; (base_via_bid, quote_via_bid, base_via_curve, quote_via_curve) }; diff --git a/api/src/state/market/virtual_limit_order.rs b/api/src/state/market/virtual_limit_order.rs index 1a2f861..7274753 100644 --- a/api/src/state/market/virtual_limit_order.rs +++ b/api/src/state/market/virtual_limit_order.rs @@ -1,6 +1,6 @@ use steel::Clock; -use crate::consts::SLOT_WINDOW; +use crate::{consts::SLOT_WINDOW, error::OreError}; use super::{Market, SwapDirection, TokenType, VirtualLimitOrder}; @@ -41,8 +41,8 @@ impl Market { /// ``` 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_balance = self.base.reserves(); + let quote_balance = self.quote.reserves(); let base_snapshot = self.snapshot.base_balance as u128; let quote_snapshot = self.snapshot.quote_balance as u128; @@ -126,21 +126,33 @@ impl Market { 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; + self.snapshot.base_balance = self.base.reserves() as u64; + self.snapshot.quote_balance = self.quote.reserves() as u64; } } - pub(crate) fn update_reserves(&mut self, base: u128, quote: u128, direction: SwapDirection) { + pub(crate) fn update_reserves( + &mut self, + base: u128, + quote: u128, + direction: SwapDirection, + ) -> Result<(), OreError> { match direction { SwapDirection::Buy => { + if base > self.base.balance as u128 { + return Err(OreError::InsufficientVaultReserves.into()); + } self.base.balance -= base as u64; self.quote.balance += quote as u64; } SwapDirection::Sell => { + if quote > self.quote.balance as u128 { + return Err(OreError::InsufficientVaultReserves.into()); + } self.base.balance += base as u64; self.quote.balance -= quote as u64; } } + Ok(()) } } diff --git a/program/src/open.rs b/program/src/open.rs index 315b8b4..e6e80e2 100644 --- a/program/src/open.rs +++ b/program/src/open.rs @@ -58,6 +58,27 @@ pub fn process_open(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult &[MARKET, &id.to_le_bytes()], )?; let market = market_info.as_account_mut::(&ore_api::ID)?; + market.base = TokenParams { + mint: Pubkey::default(), + balance: 0, + balance_virtual: 0, + }; + market.quote = TokenParams { + mint: Pubkey::default(), + balance: 0, + balance_virtual: 0, + }; + market.fee = FeeParams { + rate: FEE_RATE_BPS, + uncollected: 0, + cumulative: 0, + }; + market.snapshot = Snapshot { + enabled: 1, + base_balance: 0, + quote_balance: 0, + slot: 0, + }; market.id = id; // Initialize hash token mint.