Merge branch 'project-serum:master' into master

This commit is contained in:
galactus 2022-11-12 17:31:55 +00:00 committed by GitHub
commit 244f0d27bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 988 additions and 72 deletions

View File

@ -6,7 +6,7 @@ cache: cargo
env:
global:
- NODE_VERSION="v14.7.0"
- SOLANA_VERSION="v1.9.1"
- SOLANA_VERSION="v1.10.2"
_defaults: &defaults
language: rust
@ -26,10 +26,10 @@ jobs:
name: Dex tests
script:
- ./scripts/travis/dex-tests.sh
- <<: *defaults
name: Permissioned Dex tests
script:
- cd dex/tests/permissioned/ && yarn && yarn build && yarn test && cd ../../../
# - <<: *defaults
# name: Permissioned Dex tests
# script:
# - cd dex/tests/permissioned/ && yarn && yarn build && yarn test && cd ../../../
- <<: *defaults
name: Fmt and Common Tests
script:

4
Cargo.lock generated
View File

@ -1134,9 +1134,9 @@ dependencies = [
[[package]]
name = "event-listener"
version = "2.5.2"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "fastrand"

View File

@ -8,7 +8,7 @@ name = "crank"
path = "src/bin/main.rs"
[dependencies]
serum_dex = { path = "../", default-features = false, features = ["client"] }
serum_dex = { path = "../", default-features = false, features = ["client", "program"] }
serum-common = { path = "../../common", features = ["client"] }
spl-token = { version = "3.0.0-pre1", features = ["no-entrypoint"], default-features = false }
clap = { version = "3.1.6", features = ["derive"] }

View File

@ -113,6 +113,8 @@ pub enum DexErrorCode {
InvalidOpenOrdersAuthority,
OrderMaxTimestampExceeded,
MinAmountNotMet,
Unknown = 1000,
// This contains the line number in the lower 16 bits,

View File

@ -449,7 +449,8 @@ pub enum MarketInstruction {
/// 8. `[writable]` coin vault
/// 9. `[writable]` pc vault
/// 10. `[]` spl token program
/// 11. `[]` (optional) the (M)SRM account used for fee discounts
/// 11. `[]` vault signer
/// 12. `[]` (optional) the (M)SRM account used for fee discounts
SendTake(SendTakeInstruction),
/// 0. `[writable]` OpenOrders
/// 1. `[signer]` the OpenOrders owner
@ -1043,6 +1044,63 @@ pub fn sweep_fees(
})
}
pub fn send_take(
market: &Pubkey,
request_queue: &Pubkey,
event_queue: &Pubkey,
market_bids: &Pubkey,
market_asks: &Pubkey,
coin_wallet: &Pubkey,
pc_wallet: &Pubkey,
wallet_owner: &Pubkey,
coin_vault: &Pubkey,
pc_vault: &Pubkey,
spl_token_program_id: &Pubkey,
vault_signer: &Pubkey,
srm_account_referral: Option<&Pubkey>,
program_id: &Pubkey,
side: Side,
limit_price: NonZeroU64,
max_coin_qty: NonZeroU64,
max_native_pc_qty_including_fees: NonZeroU64,
min_coin_qty: u64,
min_native_pc_qty: u64,
limit: u16,
) -> Result<Instruction, DexError> {
let data = MarketInstruction::SendTake(SendTakeInstruction {
side,
limit_price,
max_coin_qty,
max_native_pc_qty_including_fees,
min_coin_qty,
min_native_pc_qty,
limit,
})
.pack();
let mut accounts = vec![
AccountMeta::new(*market, false),
AccountMeta::new(*request_queue, false),
AccountMeta::new(*event_queue, false),
AccountMeta::new(*market_bids, false),
AccountMeta::new(*market_asks, false),
AccountMeta::new(*coin_wallet, false),
AccountMeta::new(*pc_wallet, false),
AccountMeta::new_readonly(*wallet_owner, true),
AccountMeta::new(*coin_vault, false),
AccountMeta::new(*pc_vault, false),
AccountMeta::new_readonly(*spl_token_program_id, false),
AccountMeta::new_readonly(*vault_signer, false),
];
if let Some(key) = srm_account_referral {
accounts.push(AccountMeta::new_readonly(*key, false))
}
Ok(Instruction {
program_id: *program_id,
data,
accounts,
})
}
pub fn close_open_orders(
program_id: &Pubkey,
open_orders: &Pubkey,

View File

@ -6,7 +6,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
use proptest_derive::Arbitrary;
use serde::{Deserialize, Serialize};
#[cfg(feature = "program")]
use solana_program::msg;
use solana_program::{msg, pubkey::Pubkey, system_program};
use crate::critbit::SlabTreeError;
use crate::error::{DexErrorCode, DexResult, SourceFileId};
@ -16,12 +16,25 @@ use crate::{
state::{Event, EventQueue, EventView, MarketState, OpenOrders, RequestView},
};
use bytemuck::cast;
#[cfg(not(feature = "program"))]
macro_rules! msg {
($($i:expr),*) => { { ($($i),*) } };
}
declare_check_assert_macros!(SourceFileId::Matching);
pub trait ToAlignedBytes {
fn to_aligned_bytes(&self) -> [u64; 4];
}
impl ToAlignedBytes for Pubkey {
#[inline]
fn to_aligned_bytes(&self) -> [u64; 4] {
cast(self.to_bytes())
}
}
#[derive(
Eq, PartialEq, Copy, Clone, TryFromPrimitive, IntoPrimitive, Debug, Serialize, Deserialize,
)]
@ -243,7 +256,7 @@ impl<'ob> OrderBookState<'ob> {
client_order_id,
self_trade_behavior,
} = params;
let (mut post_only, mut post_allowed) = match order_type {
let (mut post_only, post_allowed) = match order_type {
OrderType::Limit => (false, true),
OrderType::ImmediateOrCancel => (false, false),
OrderType::PostOnly => (true, true),
@ -253,7 +266,9 @@ impl<'ob> OrderBookState<'ob> {
if *limit == 0 {
// Stop matching and release funds if we're out of cycles
post_only = true;
post_allowed = true;
// Remove this block of code as it can lead to undefined behavior where
// an ImmediateOrCancel order is allowed to place orders on the book
// post_allowed = true;
}
let remaining_order = match side {
@ -341,6 +356,8 @@ impl<'ob> OrderBookState<'ob> {
client_order_id,
self_trade_behavior,
} = params;
let is_send_take = system_program::ID.to_aligned_bytes() == owner;
let mut unfilled_qty = max_qty.get();
let mut accum_fill_price = 0;
@ -504,7 +521,7 @@ impl<'ob> OrderBookState<'ob> {
to_release.credit_native_pc(net_taker_pc_qty);
to_release.debit_coin(coin_lots_traded);
if native_taker_pc_qty > 0 {
if native_taker_pc_qty > 0 && !is_send_take {
let taker_fill = Event::new(EventView::Fill {
side: Side::Ask,
maker: false,
@ -531,6 +548,7 @@ impl<'ob> OrderBookState<'ob> {
self.market_state.pc_fees_accrued += net_fees;
self.market_state.pc_deposits_total -= net_fees_before_referrer_rebate;
if !done {
if let Some(coin_qty_remaining) = NonZeroU64::new(unfilled_qty) {
return Ok(Some(OrderRemaining {
@ -541,6 +559,7 @@ impl<'ob> OrderBookState<'ob> {
}
if post_allowed && !crossed && unfilled_qty > 0 {
check_assert!(!is_send_take)?;
let offers = self.orders_mut(Side::Ask);
let new_order = LeafNode::new(
owner_slot,
@ -572,7 +591,7 @@ impl<'ob> OrderBookState<'ob> {
} else {
insert_result.unwrap();
}
} else {
} else if !is_send_take {
to_release.unlock_coin(unfilled_qty);
let out = Event::new(EventView::Out {
side: Side::Ask,
@ -631,6 +650,8 @@ impl<'ob> OrderBookState<'ob> {
check_assert!(limit_price.is_some())?;
}
let is_send_take = system_program::ID.to_aligned_bytes() == owner;
let pc_lot_size = self.market_state.pc_lot_size;
let coin_lot_size = self.market_state.coin_lot_size;
@ -822,7 +843,7 @@ impl<'ob> OrderBookState<'ob> {
to_release.credit_coin(coin_lots_received);
to_release.debit_native_pc(native_pc_paid);
if native_accum_fill_price > 0 {
if native_accum_fill_price > 0 && !is_send_take {
let taker_fill = Event::new(EventView::Fill {
side: Side::Bid,
maker: false,
@ -869,26 +890,28 @@ impl<'ob> OrderBookState<'ob> {
_ => (0, 0),
};
let out = {
let native_qty_still_locked = pc_qty_to_keep_locked * pc_lot_size;
let native_qty_unlocked = native_pc_qty_remaining - native_qty_still_locked;
if !is_send_take {
let out = {
let native_qty_still_locked = pc_qty_to_keep_locked * pc_lot_size;
let native_qty_unlocked = native_pc_qty_remaining - native_qty_still_locked;
to_release.unlock_native_pc(native_qty_unlocked);
to_release.unlock_native_pc(native_qty_unlocked);
Event::new(EventView::Out {
side: Side::Bid,
release_funds: false,
native_qty_unlocked,
native_qty_still_locked,
order_id,
owner,
owner_slot,
client_order_id: NonZeroU64::new(client_order_id),
})
};
event_q
.push_back(out)
.map_err(|_| DexErrorCode::EventQueueFull)?;
Event::new(EventView::Out {
side: Side::Bid,
release_funds: false,
native_qty_unlocked,
native_qty_still_locked,
order_id,
owner,
owner_slot,
client_order_id: NonZeroU64::new(client_order_id),
})
};
event_q
.push_back(out)
.map_err(|_| DexErrorCode::EventQueueFull)?;
}
if pc_qty_to_keep_locked > 0 {
let bids = self.orders_mut(Side::Bid);

View File

@ -17,13 +17,13 @@ use safe_transmute::{self, to_bytes::transmute_to_bytes, trivial::TriviallyTrans
use solana_program::{
account_info::AccountInfo, program_error::ProgramError, program_pack::Pack, pubkey::Pubkey,
rent::Rent, sysvar::Sysvar,
rent::Rent, system_program, sysvar::Sysvar,
};
use spl_token::error::TokenError;
use crate::{
critbit::Slab,
error::{DexErrorCode, DexResult, SourceFileId},
error::{DexErrorCode, DexResult, SourceFileId, DexError},
fees::{self, FeeTier},
instruction::{
disable_authority, fee_sweeper, msrm_token, srm_token, CancelOrderInstructionV2,
@ -1360,7 +1360,6 @@ fn gen_vault_signer_seeds<'a>(nonce: &'a u64, market: &'a Pubkey) -> [&'a [u8];
[market.as_ref(), bytes_of(nonce)]
}
#[cfg(not(any(test, feature = "fuzz")))]
#[inline]
pub fn gen_vault_signer_key(
nonce: u64,
@ -1371,16 +1370,6 @@ pub fn gen_vault_signer_key(
Ok(Pubkey::create_program_address(&seeds, program_id)?)
}
#[cfg(any(test, feature = "fuzz"))]
pub fn gen_vault_signer_key(
nonce: u64,
market: &Pubkey,
_program_id: &Pubkey,
) -> Result<Pubkey, ProgramError> {
gen_vault_signer_seeds(&nonce, market);
Ok(Pubkey::default())
}
#[cfg(not(any(test, feature = "fuzz")))]
fn invoke_spl_token(
instruction: &solana_program::instruction::Instruction,
@ -1741,6 +1730,7 @@ pub(crate) mod account_parser {
pub pc_vault: PcVault<'a, 'b>,
pub spl_token_program: SplTokenProgram<'a, 'b>,
pub fee_tier: FeeTier,
pub vault_signer: VaultSigner<'a, 'b>,
}
impl<'a, 'b: 'a> SendTakeArgs<'a, 'b> {
pub fn with_parsed_args<T>(
@ -1749,7 +1739,7 @@ pub(crate) mod account_parser {
accounts: &'a [AccountInfo<'b>],
f: impl FnOnce(SendTakeArgs) -> DexResult<T>,
) -> DexResult<T> {
const MIN_ACCOUNTS: usize = 11;
const MIN_ACCOUNTS: usize = 12;
check_assert!(accounts.len() == MIN_ACCOUNTS || accounts.len() == MIN_ACCOUNTS + 1)?;
let (fixed_accounts, fee_discount_account): (
&'a [AccountInfo<'b>; MIN_ACCOUNTS],
@ -1767,6 +1757,7 @@ pub(crate) mod account_parser {
ref coin_vault_acc,
ref pc_vault_acc,
ref spl_token_program_acc,
ref vault_signer_acc,
]: &'a [AccountInfo<'b>; MIN_ACCOUNTS] = fixed_accounts;
let srm_or_msrm_account = match fee_discount_account {
&[] => None,
@ -1791,6 +1782,8 @@ pub(crate) mod account_parser {
let spl_token_program = SplTokenProgram::new(spl_token_program_acc)?;
let vault_signer = VaultSigner::new(vault_signer_acc, &market, program_id)?;
let mut bids = market.load_bids_mut(bids_acc).or(check_unreachable!())?;
let mut asks = market.load_asks_mut(asks_acc).or(check_unreachable!())?;
@ -1812,6 +1805,7 @@ pub(crate) mod account_parser {
pc_vault,
order_book_state,
spl_token_program,
vault_signer,
};
f(args)
}
@ -2686,8 +2680,168 @@ impl State {
}
#[cfg(feature = "program")]
fn process_send_take(_args: account_parser::SendTakeArgs) -> DexResult {
unimplemented!()
fn process_send_take(args: account_parser::SendTakeArgs) -> DexResult {
let account_parser::SendTakeArgs {
instruction,
signer,
mut event_q,
mut req_q,
mut order_book_state,
coin_wallet,
pc_wallet,
coin_vault,
pc_vault,
spl_token_program,
fee_tier,
vault_signer,
} = args;
let order_id = req_q.gen_order_id(instruction.limit_price.get(), instruction.side);
let native_pc_qty_locked = match instruction.side {
Side::Bid => {
let lock_qty_native = instruction.max_native_pc_qty_including_fees;
Some(lock_qty_native)
}
Side::Ask => None,
};
let request = RequestView::NewOrder {
side: instruction.side,
order_type: OrderType::ImmediateOrCancel,
order_id,
fee_tier,
self_trade_behavior: SelfTradeBehavior::AbortTransaction,
// Pass dummy account - system program pubkey
owner: system_program::ID.to_aligned_bytes(),
// Pass 0
owner_slot: 0,
max_coin_qty: instruction.max_coin_qty,
native_pc_qty_locked,
client_order_id: None,
};
let mut limit = instruction.limit;
let mut proceeds = RequestProceeds::zero();
let _unfilled_portion = order_book_state.process_orderbook_request(
&request,
&mut event_q,
&mut proceeds,
&mut limit,
)?;
let market_state = order_book_state.market_state;
let coin_lot_size = market_state.coin_lot_size;
let RequestProceeds {
coin_unlocked: _,
coin_credit,
native_pc_unlocked: _,
native_pc_credit,
coin_debit,
native_pc_debit,
} = proceeds;
let abort = match instruction.side {
Side::Bid if coin_credit < instruction.min_coin_qty => true,
Side::Ask if native_pc_credit < instruction.min_native_pc_qty => true,
_ => false,
};
if abort {
solana_program::msg!("Min amount requested not met! Aborting");
return Err(DexErrorCode::MinAmountNotMet.into());
};
// Amount that user deposits into the program
let deposit_amount;
// Amount that user receives after the exchange
let withdraw_amount;
// Token accounts for transfers
let deposit_vault;
let withdraw_vault;
let deposit_source;
let withdraw_destination;
match instruction.side {
Side::Bid => {
deposit_amount = native_pc_debit;
withdraw_amount = coin_credit.checked_mul(coin_lot_size).unwrap();
deposit_vault = pc_vault.token_account();
deposit_source = pc_wallet.token_account();
withdraw_destination = coin_wallet.token_account();
withdraw_vault = coin_vault.token_account();
market_state.pc_deposits_total += deposit_amount;
check_assert!(market_state.coin_deposits_total >= withdraw_amount)?;
market_state.coin_deposits_total -= withdraw_amount;
}
Side::Ask => {
deposit_amount = coin_debit.checked_mul(coin_lot_size).unwrap();
withdraw_amount = native_pc_credit;
deposit_vault = coin_vault.token_account();
deposit_source = coin_wallet.token_account();
withdraw_destination = pc_wallet.token_account();
withdraw_vault = pc_vault.token_account();
market_state.coin_deposits_total += deposit_amount;
check_assert!(market_state.pc_deposits_total >= withdraw_amount)?;
market_state.pc_deposits_total -= withdraw_amount;
}
};
if deposit_amount != 0 {
let balance_before = deposit_vault.balance()?;
let deposit_instruction = spl_token::instruction::transfer(
&spl_token::ID,
deposit_source.inner().key,
deposit_vault.inner().key,
signer.inner().key,
&[],
deposit_amount,
)
.unwrap();
invoke_spl_token(
&deposit_instruction,
&[
deposit_source.inner().clone(),
deposit_vault.inner().clone(),
signer.inner().clone(),
spl_token_program.inner().clone(),
],
&[],
)
.map_err(|err| match err {
ProgramError::Custom(i) => match TokenError::from_u32(i) {
Some(TokenError::InsufficientFunds) => DexErrorCode::InsufficientFunds,
_ => DexErrorCode::TransferFailed,
},
_ => DexErrorCode::TransferFailed,
})?;
let balance_after = deposit_vault.balance()?;
let balance_change = balance_after.checked_sub(balance_before);
check_assert_eq!(Some(deposit_amount), balance_change)?;
}
let nonce = market_state.vault_signer_nonce;
let market_pubkey = market_state.pubkey();
let vault_signer_seeds = gen_vault_signer_seeds(&nonce, &market_pubkey);
if withdraw_amount != 0 {
send_from_vault(
withdraw_amount,
withdraw_destination,
withdraw_vault,
spl_token_program,
vault_signer,
&vault_signer_seeds,
)?;
};
Ok(())
}
fn process_prune(args: account_parser::PruneArgs) -> DexResult {
@ -2892,13 +3046,29 @@ impl State {
}
for (order_id, side) in orders {
let _ = order_book_state.cancel_order_v2(
if let Err(err) = order_book_state.cancel_order_v2(
side,
open_orders_address,
open_orders,
order_id,
&mut event_q,
);
) {
// Cancel returns OrderNotFound when the order isn't in bids or
// asks. In this case, no Serum state is modified so it is safe
// to continue. Any other type of error could be accompanied by
// a partial state change, so we must error. For example, if the
// event queue is full, we should abort to prevent the cancelled
// order from being removed from bids/asks.
match err {
DexError::ErrorCode(code) => {
if code == DexErrorCode::OrderNotFound {
continue;
}
return Err(err);
}
_ => return Err(err)
}
}
}
Ok(())

View File

@ -18,11 +18,12 @@ use spl_token::state::{Account, AccountState, Mint};
use instruction::{initialize_market, MarketInstruction, NewOrderInstructionV3, SelfTradeBehavior};
use matching::{OrderType, Side};
use state::gen_vault_signer_key;
use state::{Market, MarketState, OpenOrders, State, ToAlignedBytes};
use crate::critbit::SlabView;
use crate::error::DexErrorCode;
use crate::state::account_parser::CancelOrderByClientIdV2Args;
use crate::instruction::SendTakeInstruction;
use crate::state::account_parser::TokenAccount;
use super::*;
@ -41,6 +42,7 @@ struct MarketAccounts<'bump> {
coin_mint: AccountInfo<'bump>,
pc_mint: AccountInfo<'bump>,
rent_sysvar: AccountInfo<'bump>,
vault_signer: AccountInfo<'bump>,
}
fn allocate_dex_owned_account(unpadded_size: usize, bump: &Bump) -> &mut [u8] {
@ -168,7 +170,44 @@ fn new_spl_token_program<'bump>(bump: &'bump Bump) -> AccountInfo<'bump> {
fn setup_market<'bump, R: Rng>(rng: &mut R, bump: &'bump Bump) -> MarketAccounts<'bump> {
let program_id = random_pubkey(rng, bump);
let market = new_dex_owned_account(rng, size_of::<MarketState>(), program_id, bump);
let mut i: u64 = 0;
let (market_key, vault_signer_nonce, vault_signer) = loop {
assert!(i < 100);
let market = Pubkey::new(transmute_to_bytes(&rand::random::<[u64; 4]>()));
new_dex_owned_account(rng, size_of::<MarketState>(), program_id, bump);
let seeds = [market.as_ref(), bytemuck::bytes_of(&i)];
let vault_signer_pk = match Pubkey::create_program_address(&seeds, program_id) {
Ok(pk) => pk,
Err(_) => {
i += 1;
continue;
}
};
let vault_signer = AccountInfo::new(
bump.alloc(vault_signer_pk),
true,
false,
bump.alloc(1000000),
&mut [],
&system_program::ID,
false,
Epoch::default(),
);
break (market, i, vault_signer);
};
let market = AccountInfo::new(
bump.alloc(market_key),
false,
true,
bump.alloc(60_000_000_000),
allocate_dex_owned_account(size_of::<MarketState>(), bump),
program_id,
false,
Epoch::default(),
);
let bids = new_dex_owned_account(rng, 1 << 23, program_id, bump);
let asks = new_dex_owned_account(rng, 1 << 23, program_id, bump);
let req_q = new_dex_owned_account(rng, 640, program_id, bump);
@ -178,18 +217,8 @@ fn setup_market<'bump, R: Rng>(rng: &mut R, bump: &'bump Bump) -> MarketAccounts
let pc_mint = new_token_mint(rng, bump);
let rent_sysvar = new_rent_sysvar_account(100000, Rent::default(), bump);
let mut i = 0;
let (vault_signer_nonce, vault_signer_pk) = loop {
assert!(i < 100);
if let Ok(pk) = gen_vault_signer_key(i, &market.key, program_id) {
break (i, bump.alloc(pk));
}
i += 1;
};
let coin_vault = new_token_account(rng, &coin_mint.key, vault_signer_pk, 0, bump);
let pc_vault = new_token_account(rng, &pc_mint.key, vault_signer_pk, 0, bump);
let coin_vault = new_token_account(rng, &coin_mint.key, vault_signer.key, 0, bump);
let pc_vault = new_token_account(rng, &pc_mint.key, vault_signer.key, 0, bump);
let coin_lot_size = 1_000;
let pc_lot_size = 1;
@ -245,6 +274,124 @@ fn setup_market<'bump, R: Rng>(rng: &mut R, bump: &'bump Bump) -> MarketAccounts
coin_mint,
pc_mint,
rent_sysvar,
vault_signer,
}
}
fn layer_orders(
dex_program_id: &Pubkey,
start_price: u64,
end_price: u64,
price_step: u64,
start_size: u64,
size_step: u64,
side: Side,
instruction_accounts: &[AccountInfo],
) {
assert!(price_step > 0 && size_step > 0);
let mut prices = vec![];
let mut sizes = vec![];
match side {
Side::Bid => {
assert!(start_price >= end_price);
let mut price = start_price;
let mut size = start_size;
while price >= end_price && price > 0 {
prices.push(price);
sizes.push(size);
price -= price_step;
size += size_step;
}
}
Side::Ask => {
assert!(start_price <= end_price);
let mut price = start_price;
let mut size = start_size;
while price <= end_price {
prices.push(price);
sizes.push(size);
price += price_step;
size += size_step;
}
}
}
for (i, (p, s)) in prices.iter().zip(sizes.iter()).enumerate() {
let new_order_instruction = MarketInstruction::NewOrderV3(NewOrderInstructionV3 {
side,
limit_price: NonZeroU64::new(*p).unwrap(),
max_coin_qty: NonZeroU64::new(*s).unwrap(),
max_native_pc_qty_including_fees: NonZeroU64::new(*s * *p).unwrap(),
client_order_id: i as u64,
order_type: OrderType::Limit,
self_trade_behavior: SelfTradeBehavior::AbortTransaction,
limit: 1,
max_ts: i64::MAX,
});
let starting_balance = TokenAccount::new(&instruction_accounts[6])
.unwrap()
.balance()
.unwrap();
State::process(
dex_program_id,
instruction_accounts,
&new_order_instruction.pack().clone(),
)
.unwrap();
let owner = instruction_accounts[7].key;
let ending_balance = TokenAccount::new(&instruction_accounts[6])
.unwrap()
.balance()
.unwrap();
let side_str = match side {
Side::Bid => "BUY",
Side::Ask => "SELL",
};
println!(
"{} placed {} LIMIT {} @ {}, balance {} -> {}",
owner, s, side_str, p, starting_balance, ending_balance
);
}
}
struct BBO {
bid: u64,
ask: u64,
nbid: u64,
nask: u64,
buyer: [u64; 4],
seller: [u64; 4],
}
fn get_bbo(
program_id: &Pubkey,
market: &AccountInfo,
bids_a: &AccountInfo,
asks_a: &AccountInfo,
) -> BBO {
let mkt = MarketState::load(market, program_id, false).unwrap();
let bids = mkt.load_bids_mut(bids_a).unwrap();
let asks = mkt.load_asks_mut(asks_a).unwrap();
let (ask, nask, seller) = match asks.find_min() {
None => (u64::MAX, 0, [0; 4]),
Some(h) => {
let bo = asks.get(h).unwrap().as_leaf().unwrap();
(bo.price().into(), bo.quantity(), bo.owner())
}
};
let (bid, nbid, buyer) = match bids.find_max() {
None => (0, 0, [0; 4]),
Some(h) => {
let bb = bids.get(h).unwrap().as_leaf().unwrap();
(bb.price().into(), bb.quantity(), bb.owner())
}
};
BBO {
bid,
ask,
nbid,
nask,
buyer,
seller,
}
}
@ -413,6 +560,528 @@ fn test_new_order() {
}
}
#[test]
fn test_ioc_new_order() {
let mut rng = StdRng::seed_from_u64(2);
let bump = Bump::new();
let accounts = setup_market(&mut rng, &bump);
let dex_program_id = accounts.market.owner;
let owner = new_sol_account(&mut rng, 1_000_000_000, &bump);
let orders_account =
new_dex_owned_account(&mut rng, size_of::<OpenOrders>(), dex_program_id, &bump);
// Account with 25 coin orders (coin lot size = 1000)
let coin_account =
new_token_account(&mut rng, accounts.coin_mint.key, owner.key, 25_000, &bump);
let pc_account = new_token_account(&mut rng, accounts.pc_mint.key, owner.key, 1_000_000, &bump);
let spl_token_program = new_spl_token_program(&bump);
let mut instruction_accounts = bump_vec![in &bump;
accounts.market.clone(),
orders_account.clone(),
accounts.req_q.clone(),
accounts.event_q.clone(),
accounts.bids.clone(),
accounts.asks.clone(),
pc_account.clone(),
owner.clone(),
accounts.coin_vault.clone(),
accounts.pc_vault.clone(),
spl_token_program.clone(),
accounts.rent_sysvar.clone(),
];
layer_orders(
dex_program_id,
10_000,
9_000,
200,
1,
2,
Side::Bid,
instruction_accounts.as_slice(),
);
instruction_accounts[6] = coin_account.clone();
layer_orders(
dex_program_id,
10_100,
11_100,
200,
1,
1,
Side::Ask,
instruction_accounts.as_slice(),
);
let taker = new_sol_account(&mut rng, 1_000_000_000, &bump);
let orders_account_taker =
new_dex_owned_account(&mut rng, size_of::<OpenOrders>(), dex_program_id, &bump);
let taker_coin_account =
new_token_account(&mut rng, accounts.coin_mint.key, taker.key, 6_000, &bump);
let taker_pc_account =
new_token_account(&mut rng, accounts.pc_mint.key, owner.key, 100_000, &bump);
// IOC take out the 10_000 level
let instruction_data = MarketInstruction::NewOrderV3(NewOrderInstructionV3 {
side: Side::Ask,
limit_price: NonZeroU64::new(10_000).unwrap(),
max_coin_qty: NonZeroU64::new(1).unwrap(),
max_native_pc_qty_including_fees: NonZeroU64::new(1).unwrap(),
order_type: OrderType::ImmediateOrCancel,
client_order_id: 0xface,
self_trade_behavior: SelfTradeBehavior::AbortTransaction,
limit: 1,
max_ts: i64::MAX,
})
.pack();
let instruction_accounts: &[AccountInfo] = bump_vec![in &bump;
accounts.market.clone(),
orders_account_taker.clone(),
accounts.req_q.clone(),
accounts.event_q.clone(),
accounts.bids.clone(),
accounts.asks.clone(),
taker_coin_account.clone(),
taker.clone(),
accounts.coin_vault.clone(),
accounts.pc_vault.clone(),
spl_token_program.clone(),
accounts.rent_sysvar.clone(),
]
.into_bump_slice();
State::process(dex_program_id, instruction_accounts, &instruction_data).unwrap();
let ta = TokenAccount::new(&taker_coin_account).unwrap();
assert_eq!(ta.balance().unwrap(), 5_000);
let ta = TokenAccount::new(&taker_pc_account).unwrap();
assert_eq!(ta.balance().unwrap(), 100_000);
let BBO {
bid,
ask,
nbid,
nask,
buyer,
seller,
} = get_bbo(
dex_program_id,
&accounts.market,
&accounts.bids,
&accounts.asks,
);
assert_eq!(bid, 9800);
assert_eq!(ask, 10100);
assert_eq!(nbid, 3);
assert_eq!(nask, 1);
assert_eq!(buyer, seller);
// IOC take out the 10_000 level
let instruction_data = MarketInstruction::NewOrderV3(NewOrderInstructionV3 {
side: Side::Ask,
limit_price: NonZeroU64::new(9_800).unwrap(),
max_coin_qty: NonZeroU64::new(5).unwrap(),
max_native_pc_qty_including_fees: NonZeroU64::new(1).unwrap(),
order_type: OrderType::ImmediateOrCancel,
client_order_id: 0xabcd,
self_trade_behavior: SelfTradeBehavior::AbortTransaction,
limit: 1,
max_ts: i64::MAX,
})
.pack();
let instruction_accounts: &[AccountInfo] = bump_vec![in &bump;
accounts.market.clone(),
orders_account_taker.clone(),
accounts.req_q.clone(),
accounts.event_q.clone(),
accounts.bids.clone(),
accounts.asks.clone(),
taker_coin_account.clone(),
taker.clone(),
accounts.coin_vault.clone(),
accounts.pc_vault.clone(),
spl_token_program.clone(),
accounts.rent_sysvar.clone(),
]
.into_bump_slice();
State::process(dex_program_id, instruction_accounts, &instruction_data).unwrap();
let BBO {
bid,
ask,
nbid: _,
nask: _,
buyer,
seller,
} = get_bbo(
dex_program_id,
&accounts.market,
&accounts.bids,
&accounts.asks,
);
assert_eq!(bid, 9600);
if ask == 9800 {
println!("UNDEFINED BEHAVIOR: Taker placed a limit order, but specific IOC");
}
// This check will fail until the bug for processing IOC orders is fixed
assert_eq!(ask, 10100);
assert_eq!(buyer, seller);
}
#[test]
fn test_send_take() {
let mut rng = StdRng::seed_from_u64(3);
let bump = Bump::new();
let accounts = setup_market(&mut rng, &bump);
let dex_program_id = accounts.market.owner;
let owner = new_sol_account(&mut rng, 1_000_000_000, &bump);
let orders_account =
new_dex_owned_account(&mut rng, size_of::<OpenOrders>(), dex_program_id, &bump);
// Account with 25 coin orders (coin lot size = 1000)
let coin_account =
new_token_account(&mut rng, accounts.coin_mint.key, owner.key, 25_000, &bump);
let pc_account = new_token_account(&mut rng, accounts.pc_mint.key, owner.key, 1_000_000, &bump);
let spl_token_program = new_spl_token_program(&bump);
let mut instruction_accounts = bump_vec![in &bump;
accounts.market.clone(),
orders_account.clone(),
accounts.req_q.clone(),
accounts.event_q.clone(),
accounts.bids.clone(),
accounts.asks.clone(),
pc_account.clone(),
owner.clone(),
accounts.coin_vault.clone(),
accounts.pc_vault.clone(),
spl_token_program.clone(),
accounts.rent_sysvar.clone(),
];
let market = Market::load(&accounts.market, &dex_program_id, false).unwrap();
let pc = market.pc_deposits_total;
let pcf = market.pc_fees_accrued;
let cdt = market.coin_deposits_total;
let cf = market.coin_fees_accrued;
println!(
"pc_deposits_total: {}, pc_fees_accrued: {}, coin_deposits_total: {}, coin_fees_accrued: {}",
pc, pcf, cdt, cf
);
drop(market);
layer_orders(
dex_program_id,
10_000,
9_000,
200,
1,
2,
Side::Bid,
instruction_accounts.as_slice(),
);
let market = Market::load(&accounts.market, &dex_program_id, false).unwrap();
let pc = market.pc_deposits_total;
let pcf = market.pc_fees_accrued;
let cdt = market.coin_deposits_total;
let cf = market.coin_fees_accrued;
println!(
"pc_deposits_total: {}, pc_fees_accrued: {}, coin_deposits_total: {}, coin_fees_accrued: {}",
pc, pcf, cdt, cf
);
drop(market);
instruction_accounts[6] = coin_account.clone();
layer_orders(
dex_program_id,
10_100,
11_100,
200,
1,
1,
Side::Ask,
instruction_accounts.as_slice(),
);
let market = Market::load(&accounts.market, &dex_program_id, false).unwrap();
let pc = market.pc_deposits_total;
let pcf = market.pc_fees_accrued;
let cdt = market.coin_deposits_total;
let cf = market.coin_fees_accrued;
println!(
"pc_deposits_total: {}, pc_fees_accrued: {}, coin_deposits_total: {}, coin_fees_accrued: {}",
pc, pcf, cdt, cf
);
drop(market);
let mut total_pc_on_book = 0;
let mut p = 10_000;
let mut s = 1;
while p >= 9_000 {
total_pc_on_book += p * s;
s += 2;
p -= 200
}
let taker = new_sol_account(&mut rng, 1_000_000_000, &bump);
let taker_coin_account =
new_token_account(&mut rng, accounts.coin_mint.key, taker.key, 6_000, &bump);
let taker_pc_account =
new_token_account(&mut rng, accounts.pc_mint.key, taker.key, 100_000, &bump);
let instruction_accounts = bump_vec![in &bump;
accounts.market.clone(),
accounts.req_q.clone(),
accounts.event_q.clone(),
accounts.bids.clone(),
accounts.asks.clone(),
taker_coin_account.clone(),
taker_pc_account.clone(),
taker.clone(),
accounts.coin_vault.clone(),
accounts.pc_vault.clone(),
spl_token_program.clone(),
accounts.vault_signer.clone(),
];
let starting_balance = TokenAccount::new(&taker_coin_account)
.unwrap()
.balance()
.unwrap();
let starting_pc = TokenAccount::new(&taker_pc_account)
.unwrap()
.balance()
.unwrap();
let max_coin_qty = 3;
let limit_price = 10_299;
let max_pc_qty = max_coin_qty * limit_price;
let send_take_ix = MarketInstruction::SendTake(SendTakeInstruction {
side: Side::Bid,
limit_price: NonZeroU64::new(limit_price).unwrap(),
max_coin_qty: NonZeroU64::new(max_coin_qty).unwrap(),
max_native_pc_qty_including_fees: NonZeroU64::new(max_pc_qty).unwrap(),
min_coin_qty: 0,
min_native_pc_qty: 0,
limit: 50,
});
State::process(dex_program_id, &instruction_accounts, &send_take_ix.pack()).unwrap();
let ending_balance = TokenAccount::new(&taker_coin_account)
.unwrap()
.balance()
.unwrap();
let ending_pc = TokenAccount::new(&taker_pc_account)
.unwrap()
.balance()
.unwrap();
println!(
"{} sends 3 MARKET BUY @ {}, matched {}, paid {}",
taker.key,
limit_price,
ending_balance - starting_balance,
starting_pc - ending_pc
);
let market = Market::load(&accounts.market, &dex_program_id, false).unwrap();
let pc = market.pc_deposits_total;
let pcf = market.pc_fees_accrued;
let cdt = market.coin_deposits_total;
let cf = market.coin_fees_accrued;
println!(
"pc_deposits_total: {}, pc_fees_accrued: {}, coin_deposits_total: {}, coin_fees_accrued: {}",
pc, pcf, cdt, cf
);
drop(market);
let tca = TokenAccount::new(&taker_coin_account).unwrap();
assert_eq!(tca.balance().unwrap(), 7_000);
let tpca = TokenAccount::new(&taker_pc_account).unwrap();
// There's a default 4bps fee applied, but the fee rounds up always
// See fees.rs:taker_fee (line 137)
assert_eq!(tpca.balance().unwrap(), 100_000 - 10_100 - 5);
let prev_pc_balance = tpca.balance().unwrap();
let starting_balance = TokenAccount::new(&taker_coin_account)
.unwrap()
.balance()
.unwrap();
let starting_pc = TokenAccount::new(&taker_pc_account)
.unwrap()
.balance()
.unwrap();
let max_coin_qty = 3;
let limit_price = 9999;
let max_pc_qty = max_coin_qty * limit_price;
let send_take_ix = MarketInstruction::SendTake(SendTakeInstruction {
side: Side::Ask,
limit_price: NonZeroU64::new(limit_price).unwrap(),
max_coin_qty: NonZeroU64::new(max_coin_qty).unwrap(),
max_native_pc_qty_including_fees: NonZeroU64::new(max_pc_qty).unwrap(),
min_coin_qty: 0,
min_native_pc_qty: 0,
limit: 1,
});
State::process(dex_program_id, &instruction_accounts, &send_take_ix.pack()).unwrap();
let ending_balance = TokenAccount::new(&taker_coin_account)
.unwrap()
.balance()
.unwrap();
let ending_pc = TokenAccount::new(&taker_pc_account)
.unwrap()
.balance()
.unwrap();
println!(
"{} sends 3 MARKET SELL @ {}, matched {}, received {}",
taker.key,
limit_price,
starting_balance - ending_balance,
ending_pc - starting_pc
);
let market = Market::load(&accounts.market, &dex_program_id, false).unwrap();
let pc = market.pc_deposits_total;
let pcf = market.pc_fees_accrued;
let cdt = market.coin_deposits_total;
let cf = market.coin_fees_accrued;
println!(
"pc_deposits_total: {}, pc_fees_accrued: {}, coin_deposits_total: {}, coin_fees_accrued: {}",
pc, pcf, cdt, cf
);
drop(market);
let tca = TokenAccount::new(&taker_coin_account).unwrap();
assert_eq!(tca.balance().unwrap(), 6_000);
let tpca = TokenAccount::new(&taker_pc_account).unwrap();
assert_eq!(
tpca.balance().unwrap(),
prev_pc_balance + 10_000 - 4 /* This time the fee is exactly 4 bps */
);
let prev_pc_balance = tpca.balance().unwrap();
let starting_balance = TokenAccount::new(&taker_coin_account)
.unwrap()
.balance()
.unwrap();
let starting_pc = TokenAccount::new(&taker_pc_account)
.unwrap()
.balance()
.unwrap();
let max_coin_qty = 3;
let limit_price = 9999;
let max_pc_qty = max_coin_qty * limit_price;
let mut send_take_ix = SendTakeInstruction {
side: Side::Ask,
limit_price: NonZeroU64::new(limit_price).unwrap(),
max_coin_qty: NonZeroU64::new(max_coin_qty).unwrap(),
max_native_pc_qty_including_fees: NonZeroU64::new(max_pc_qty).unwrap(),
min_coin_qty: max_coin_qty,
min_native_pc_qty: max_pc_qty,
limit: 2,
};
let send_take = MarketInstruction::SendTake(send_take_ix.clone());
assert!(State::process(dex_program_id, &instruction_accounts, &send_take.pack()).is_err());
let ending_balance = TokenAccount::new(&taker_coin_account)
.unwrap()
.balance()
.unwrap();
let ending_pc = TokenAccount::new(&taker_pc_account)
.unwrap()
.balance()
.unwrap();
println!(
"{} sends 3 MARKET SELL @ {}, matched {}, received {}",
taker.key,
limit_price,
starting_balance - ending_balance,
ending_pc - starting_pc
);
send_take_ix.limit_price = NonZeroU64::new(9800).unwrap();
send_take_ix.min_coin_qty = 1;
send_take_ix.min_native_pc_qty = 0;
let send_take = MarketInstruction::SendTake(send_take_ix.clone());
assert!(!State::process(dex_program_id, &instruction_accounts, &send_take.pack()).is_err());
let ending_balance = TokenAccount::new(&taker_coin_account)
.unwrap()
.balance()
.unwrap();
let ending_pc = TokenAccount::new(&taker_pc_account)
.unwrap()
.balance()
.unwrap();
println!(
"{} sends 3 MARKET SELL @ {}, matched {}, received {}",
taker.key,
send_take_ix.limit_price.get(),
starting_balance - ending_balance,
ending_pc - starting_pc
);
let market = Market::load(&accounts.market, &dex_program_id, false).unwrap();
let pc = market.pc_deposits_total;
let pcf = market.pc_fees_accrued;
let cdt = market.coin_deposits_total;
let cf = market.coin_fees_accrued;
println!(
"pc_deposits_total: {}, pc_fees_accrued: {}, coin_deposits_total: {}, coin_fees_accrued: {}",
pc, pcf, cdt, cf
);
drop(market);
let BBO {
bid,
ask,
nbid,
nask,
buyer,
seller,
} = get_bbo(
dex_program_id,
&accounts.market,
&accounts.bids,
&accounts.asks,
);
assert_eq!(bid, 9600);
assert_eq!(ask, 10300);
assert_eq!(nbid, 5);
assert_eq!(nask, 2);
assert_eq!(buyer, seller);
let tca = TokenAccount::new(&taker_coin_account).unwrap();
assert_eq!(tca.balance().unwrap(), 3_000);
let tpca = TokenAccount::new(&taker_pc_account).unwrap();
assert_eq!(tpca.balance().unwrap(), prev_pc_balance + (3 * 9800) - 12);
{
let crank_accounts = bump_vec![in &bump;
orders_account.clone(),
accounts.market.clone(),
accounts.event_q.clone(),
coin_account.clone(),
pc_account.clone(),
]
.into_bump_slice_mut();
let instruction_data = MarketInstruction::ConsumeEvents(200).pack();
State::process(dex_program_id, crank_accounts, &instruction_data).unwrap();
}
{
let open_orders = Market::load(&accounts.market, &dex_program_id, false)
.unwrap()
.load_orders_mut(&orders_account, None, &dex_program_id, None, None)
.unwrap();
// The taker sold a total of 4 coins
assert_eq!(identity(open_orders.native_coin_free), 4_000);
// The maker places 21 offers (1+2+...+6) and 1 was filled. Then there were 4 sells
assert_eq!(identity(open_orders.native_coin_total), 24_000);
assert_eq!(identity(open_orders.native_pc_free), 10100);
assert_eq!(
identity(open_orders.native_pc_total),
total_pc_on_book - 10000 - (9800 * 3) + 10100
);
}
let market = Market::load(&accounts.market, &dex_program_id, false).unwrap();
let pc = market.pc_deposits_total;
let pcf = market.pc_fees_accrued;
let cdt = market.coin_deposits_total;
let cf = market.coin_fees_accrued;
println!(
"pc_deposits_total: {}, pc_fees_accrued: {}, coin_deposits_total: {}, coin_fees_accrued: {}",
pc, pcf, cdt, cf
);
}
#[test]
fn test_cancel_orders() {
let mut rng = StdRng::seed_from_u64(1);
@ -425,8 +1094,6 @@ fn test_cancel_orders() {
let owner = new_sol_account(&mut rng, 1_000_000_000, &bump);
let orders_account =
new_dex_owned_account(&mut rng, size_of::<OpenOrders>(), dex_program_id, &bump);
let coin_account =
new_token_account(&mut rng, accounts.coin_mint.key, owner.key, 10_000, &bump);
let pc_account = new_token_account(&mut rng, accounts.pc_mint.key, owner.key, 1_000_000, &bump);
let spl_token_program = new_spl_token_program(&bump);
@ -520,8 +1187,6 @@ fn test_max_ts_order() {
let owner = new_sol_account(&mut rng, 1_000_000_000, &bump);
let orders_account =
new_dex_owned_account(&mut rng, size_of::<OpenOrders>(), dex_program_id, &bump);
let coin_account =
new_token_account(&mut rng, accounts.coin_mint.key, owner.key, 10_000, &bump);
let pc_account = new_token_account(&mut rng, accounts.pc_mint.key, owner.key, 1_000_000, &bump);
let spl_token_program = new_spl_token_program(&bump);
@ -608,8 +1273,6 @@ fn test_replace_orders() {
let owner = new_sol_account(&mut rng, 1_000_000_000, &bump);
let orders_account =
new_dex_owned_account(&mut rng, size_of::<OpenOrders>(), dex_program_id, &bump);
let coin_account =
new_token_account(&mut rng, accounts.coin_mint.key, owner.key, 10_000, &bump);
let pc_account = new_token_account(&mut rng, accounts.pc_mint.key, owner.key, 1_000_000, &bump);
let spl_token_program = new_spl_token_program(&bump);