virtual liquidity

This commit is contained in:
Hardhat Chad
2025-06-05 17:28:27 -07:00
parent 8f6a40ea17
commit fda52dd452
8 changed files with 159 additions and 52 deletions

View File

@@ -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)
};

View File

@@ -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)
};

View File

@@ -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<u128, OreError> {
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<u128, OreError> {
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)
}
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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)
};

View File

@@ -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(())
}
}

View File

@@ -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::<Market>(&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.