From 4713d2f338d27b41ff09cf769e52d6a9f6c1cf4e Mon Sep 17 00:00:00 2001 From: emil <60591313+a-creation@users.noreply.github.com> Date: Sun, 23 Oct 2022 14:22:28 -0400 Subject: [PATCH 1/2] process_send_take function implemented (#251) * send-take implemented * send take * remove comments, return err * Replace 2 more checks with is_send_take flag * attempted cargo fix * Variable cleanup and code modification to handle fee updates in matching.rs * Remove unused variable and add assert * revert Cargo.lock * Anchor version * revert anchor version to 0.18.2 * update CI script * cargo.lock * Added test cases to more throroughly stress test SendTake * Remove unnecessary variable in test * Revert PR changes related to build * Skip permissioned markets test * Fix indentation * cargo fmt the state.rs file * Add overflow checks * variable rename * open orders accounting seems correct * Added print statements to understand the SendTake test case * get rid of overflow checks and calculate fees in matching Co-authored-by: jarry-xiao --- .travis.yml | 10 +- Cargo.lock | 4 +- dex/src/error.rs | 2 + dex/src/instruction.rs | 3 +- dex/src/matching.rs | 71 +++-- dex/src/state.rs | 184 ++++++++++- dex/src/tests.rs | 705 +++++++++++++++++++++++++++++++++++++++-- 7 files changed, 911 insertions(+), 68 deletions(-) diff --git a/.travis.yml b/.travis.yml index c875e04..223eca5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: diff --git a/Cargo.lock b/Cargo.lock index 60a12d8..3302771 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/dex/src/error.rs b/dex/src/error.rs index aa76f18..2b42e4c 100644 --- a/dex/src/error.rs +++ b/dex/src/error.rs @@ -113,6 +113,8 @@ pub enum DexErrorCode { InvalidOpenOrdersAuthority, OrderMaxTimestampExceeded, + MinAmountNotMet, + Unknown = 1000, // This contains the line number in the lower 16 bits, diff --git a/dex/src/instruction.rs b/dex/src/instruction.rs index 529f175..c8b3dbb 100644 --- a/dex/src/instruction.rs +++ b/dex/src/instruction.rs @@ -443,7 +443,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 diff --git a/dex/src/matching.rs b/dex/src/matching.rs index eb078c9..1918dd5 100644 --- a/dex/src/matching.rs +++ b/dex/src/matching.rs @@ -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); diff --git a/dex/src/state.rs b/dex/src/state.rs index a0a017c..d42f800 100644 --- a/dex/src/state.rs +++ b/dex/src/state.rs @@ -17,7 +17,7 @@ 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; @@ -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 { - 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( @@ -1749,7 +1739,7 @@ pub(crate) mod account_parser { accounts: &'a [AccountInfo<'b>], f: impl FnOnce(SendTakeArgs) -> DexResult, ) -> DexResult { - 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 { diff --git a/dex/src/tests.rs b/dex/src/tests.rs index 2e363e8..72a33b7 100644 --- a/dex/src/tests.rs +++ b/dex/src/tests.rs @@ -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::(), 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::(), 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::(), 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::(), 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 ≎ + 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::(), 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 ≎ + 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 ≎ + 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::(), 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 ≎ + 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 ≎ + 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 ≎ + 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::(), 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::(), 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::(), 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); From d678cead9548ad507f29ba5d2cde5383bee608fa Mon Sep 17 00:00:00 2001 From: kootsZhin <69758390+kootsZhin@users.noreply.github.com> Date: Sun, 23 Oct 2022 15:05:22 -0400 Subject: [PATCH 2/2] feat: add `send_take` to `instruction.rs` to facilitate CPI (#257) --- dex/src/instruction.rs | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/dex/src/instruction.rs b/dex/src/instruction.rs index c8b3dbb..0ce2f36 100644 --- a/dex/src/instruction.rs +++ b/dex/src/instruction.rs @@ -1038,6 +1038,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 { + 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,