diff --git a/.gitignore b/.gitignore index d641e76d..2f93dffe 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ target/ *.swo node_modules yarn.lock -.anchor \ No newline at end of file +.anchor +.DS_Store +**/*.rs.bk +close-markets/Cargo.lock +close-markets/package-lock.json diff --git a/.travis.yml b/.travis.yml index c875e049..6693cf4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,10 @@ jobs: name: Permissioned Dex tests script: - cd dex/tests/permissioned/ && yarn && yarn build && yarn test && cd ../../../ + - <<: *defaults + name: Closed Markets tests + script: + - cd dex/tests/close-markets/ && yarn && yarn build && yarn test && cd ../../../ - <<: *defaults name: Fmt and Common Tests script: diff --git a/Cargo.lock b/Cargo.lock index 94d5fdbf..7aab8870 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,7 +195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6d6c8fbc834319618581a4e19807a30e76326b9981abd069addb55acf0647db" dependencies = [ "anchor-lang", - "serum_dex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serum_dex 0.4.0", "solana-program", "spl-associated-token-account", "spl-token 3.2.0", @@ -771,7 +771,7 @@ dependencies = [ "rand 0.7.3", "safe-transmute", "serum-common", - "serum_dex 0.4.0", + "serum_dex 0.5.1", "slog-scope", "slog-stdlog", "sloggers", @@ -3157,11 +3157,11 @@ dependencies = [ [[package]] name = "serum-dex-permissioned" -version = "0.1.0" +version = "0.5.1" dependencies = [ "anchor-lang", "anchor-spl", - "serum_dex 0.4.0", + "serum_dex 0.5.1", "spl-token 3.2.0", ] @@ -3219,6 +3219,8 @@ dependencies = [ [[package]] name = "serum_dex" version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02705854bae4622e552346c8edd43ab90c7425da35d63d2c689f39238f8d8b25" dependencies = [ "arrayref", "bincode", @@ -3240,9 +3242,7 @@ dependencies = [ [[package]] name = "serum_dex" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02705854bae4622e552346c8edd43ab90c7425da35d63d2c689f39238f8d8b25" +version = "0.5.1" dependencies = [ "arrayref", "bincode", diff --git a/dex/src/critbit.rs b/dex/src/critbit.rs index 6dfdcb2c..0074b839 100644 --- a/dex/src/critbit.rs +++ b/dex/src/critbit.rs @@ -252,13 +252,13 @@ const_assert_eq!(_NODE_ALIGN, align_of::()); #[derive(Copy, Clone)] #[repr(packed)] -struct SlabHeader { +pub struct SlabHeader { bump_index: u64, free_list_len: u64, free_list_head: u32, root_node: u32, - leaf_count: u64, + pub leaf_count: u64, } unsafe impl Zeroable for SlabHeader {} unsafe impl Pod for SlabHeader {} @@ -347,7 +347,7 @@ impl Slab { (header, nodes) } - fn header(&self) -> &SlabHeader { + pub fn header(&self) -> &SlabHeader { self.parts().0 } diff --git a/dex/src/error.rs b/dex/src/error.rs index 47097c9e..662ce711 100644 --- a/dex/src/error.rs +++ b/dex/src/error.rs @@ -112,6 +112,11 @@ pub enum DexErrorCode { WouldSelfTrade, InvalidOpenOrdersAuthority, + BidQueueNotEmpty, + AskQueueNotEmpty, + RequestQueueNotEmpty, + EventQueueNotEmpty, + 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 f240b851..f2a8c8cc 100644 --- a/dex/src/instruction.rs +++ b/dex/src/instruction.rs @@ -467,6 +467,16 @@ pub enum MarketInstruction { /// accounts.len() - 2 `[writable]` event queue /// accounts.len() - 1 `[signer]` crank authority ConsumeEventsPermissioned(u16), + /// Closes a market and retrieves the rent from the bids, asks, event and request queues + /// + /// 0. `[writable]` market + /// 1. `[writable]` request queue + /// 2. `[writable]` event queue + /// 3. `[writable]` bids + /// 4. `[writable]` asks + /// 5. `[signer]` prune authority + /// 6. `[writable]` the destination account to send the rent exemption SOL to. + CloseMarket, } impl MarketInstruction { @@ -568,6 +578,7 @@ impl MarketInstruction { let limit = array_ref![data, 0, 2]; MarketInstruction::ConsumeEventsPermissioned(u16::from_le_bytes(*limit)) } + (18, 0) => MarketInstruction::CloseMarket, _ => return None, }) } @@ -1003,6 +1014,33 @@ pub fn prune( }) } +pub fn close_market( + program_id: &Pubkey, + market: &Pubkey, + request_queue: &Pubkey, + event_queue: &Pubkey, + bids: &Pubkey, + asks: &Pubkey, + prune_authority: &Pubkey, + destination: &Pubkey, +) -> Result { + let data = MarketInstruction::CloseMarket.pack(); + let accounts: Vec = vec![ + AccountMeta::new(*market, false), + AccountMeta::new(*request_queue, false), + AccountMeta::new(*event_queue, false), + AccountMeta::new(*bids, false), + AccountMeta::new(*asks, false), + AccountMeta::new_readonly(*prune_authority, true), + AccountMeta::new(*destination, false), + ]; + Ok(Instruction { + program_id: *program_id, + data, + accounts, + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/dex/src/state.rs b/dex/src/state.rs index abc1a769..58889d52 100644 --- a/dex/src/state.rs +++ b/dex/src/state.rs @@ -752,7 +752,7 @@ pub trait QueueHeader: Pod { } pub struct Queue<'a, H: QueueHeader> { - header: RefMut<'a, H>, + pub header: RefMut<'a, H>, buf: RefMut<'a, [H::Item]>, } @@ -2422,6 +2422,72 @@ pub(crate) mod account_parser { f(args) } } + + pub struct CloseMarketArgs<'a, 'b: 'a> { + pub market: &'a mut MarketState, + pub request_q_acc: &'a AccountInfo<'b>, + pub event_q_acc: &'a AccountInfo<'b>, + pub bids_acc: &'a AccountInfo<'b>, + pub asks_acc: &'a AccountInfo<'b>, + pub dest_acc: &'a AccountInfo<'b>, + } + + impl<'a, 'b: 'a> CloseMarketArgs<'a, 'b> { + pub fn with_parsed_args( + program_id: &'a Pubkey, + accounts: &'a [AccountInfo<'b>], + f: impl FnOnce(CloseMarketArgs) -> DexResult, + ) -> DexResult { + // Parse accounts. + check_assert_eq!(accounts.len(), 7)?; + #[rustfmt::skip] + let &[ + ref market_acc, + ref request_q_acc, + ref event_q_acc, + ref bids_acc, + ref asks_acc, + ref prune_auth_acc, + ref dest_acc, + ] = array_ref![accounts, 0, 7]; + + // Validate prune authority. + let _prune_authority = SignerAccount::new(prune_auth_acc)?; + let mut market = Market::load(market_acc, program_id, false)?; + check_assert!(market.prune_authority() == Some(prune_auth_acc.key))?; + + // Check that request q, event q, bids and asks are completely cleared and empty + let bids = market.load_bids_mut(bids_acc).or(check_unreachable!())?; + let asks = market.load_asks_mut(asks_acc).or(check_unreachable!())?; + let req_q = market.load_request_queue_mut(request_q_acc)?; + let event_q = market.load_event_queue_mut(event_q_acc)?; + + let bids_header = bids.header(); + if bids_header.leaf_count != 0 { + return Err(DexErrorCode::BidQueueNotEmpty.into()); + } + let asks_header = asks.header(); + if asks_header.leaf_count != 0 { + return Err(DexErrorCode::AskQueueNotEmpty.into()); + } + if req_q.header.count != 0 { + return Err(DexErrorCode::RequestQueueNotEmpty.into()); + } + if event_q.header.count != 0 { + return Err(DexErrorCode::EventQueueNotEmpty.into()); + } + + // Invoke Processor + f(CloseMarketArgs { + market: market.deref_mut(), + request_q_acc, + event_q_acc, + bids_acc, + asks_acc, + dest_acc, + }) + } + } } #[inline] @@ -2545,6 +2611,11 @@ impl State { limit, Self::process_prune, )?, + MarketInstruction::CloseMarket => account_parser::CloseMarketArgs::with_parsed_args( + program_id, + accounts, + Self::process_close_market, + )?, }; Ok(()) } @@ -2554,6 +2625,37 @@ impl State { unimplemented!() } + fn process_close_market(args: account_parser::CloseMarketArgs) -> DexResult { + let account_parser::CloseMarketArgs { + market, + request_q_acc, + event_q_acc, + bids_acc, + asks_acc, + dest_acc, + } = args; + + // Transfer all lamports to the desintation. + let dest_starting_lamports = dest_acc.lamports(); + **dest_acc.lamports.borrow_mut() = dest_starting_lamports + .checked_add(request_q_acc.lamports()) + .unwrap() + .checked_add(event_q_acc.lamports()) + .unwrap() + .checked_add(bids_acc.lamports()) + .unwrap() + .checked_add(asks_acc.lamports()) + .unwrap(); + **request_q_acc.lamports.borrow_mut() = 0; + **event_q_acc.lamports.borrow_mut() = 0; + **bids_acc.lamports.borrow_mut() = 0; + **asks_acc.lamports.borrow_mut() = 0; + + market.account_flags = market.account_flags | (AccountFlag::Disabled as u64); + + Ok(()) + } + fn process_prune(args: account_parser::PruneArgs) -> DexResult { let account_parser::PruneArgs { mut order_book_state, diff --git a/dex/tests/close-markets/Anchor.toml b/dex/tests/close-markets/Anchor.toml new file mode 100644 index 00000000..b3fd167d --- /dev/null +++ b/dex/tests/close-markets/Anchor.toml @@ -0,0 +1,19 @@ +[programs.localnet] +close_markets = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" + +[test] +startup_wait = 20000 + +[registry] +url = "https://anchor.projectserum.com" + +[provider] +cluster = "localnet" +wallet = "~/.config/solana/id.json" + +[[test.genesis]] +address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" +program = "../../target/deploy/serum_dex.so" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" diff --git a/dex/tests/close-markets/Cargo.toml b/dex/tests/close-markets/Cargo.toml new file mode 100644 index 00000000..a60de986 --- /dev/null +++ b/dex/tests/close-markets/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "programs/*" +] diff --git a/dex/tests/close-markets/migrations/deploy.ts b/dex/tests/close-markets/migrations/deploy.ts new file mode 100644 index 00000000..325cf3d0 --- /dev/null +++ b/dex/tests/close-markets/migrations/deploy.ts @@ -0,0 +1,12 @@ +// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@project-serum/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +} diff --git a/dex/tests/close-markets/package.json b/dex/tests/close-markets/package.json new file mode 100644 index 00000000..44bec7b0 --- /dev/null +++ b/dex/tests/close-markets/package.json @@ -0,0 +1,26 @@ +{ + "name": "close-markets", + "version": "1.0.0", + "description": "This repo demonstrates how to create, utilize and then close a permissioned serum market", + "main": "index.js", + "directories": { + "test": "tests" + }, + "dependencies": {}, + "devDependencies": { + "@project-serum/anchor": "^0.20.0", + "@project-serum/anchor-cli": "^0.20.0", + "@project-serum/serum": "^0.13.61", + "@solana/spl-token": "^0.1.8", + "mocha": "^9.1.3", + "ts-mocha": "^8.0.0", + "typescript": "^4.5.4" + }, + "scripts": { + "test": "anchor test", + "build": "yarn build:dex && anchor build", + "build:dex": "cd ../../ && cargo build-bpf && cd tests/permissioned/" + }, + "author": "", + "license": "ISC" +} diff --git a/dex/tests/close-markets/programs/close-markets/Cargo.toml b/dex/tests/close-markets/programs/close-markets/Cargo.toml new file mode 100644 index 00000000..cce0c51a --- /dev/null +++ b/dex/tests/close-markets/programs/close-markets/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "close-markets" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "close_markets" + +[features] +no-entrypoint = [] +no-idl = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = "0.19.0" +anchor-spl = {version = "0.19.0", features = ["dex"]} +serum_dex = { path = "../../../..", features = ["no-entrypoint"]} +solana-program = "1.8.0" \ No newline at end of file diff --git a/dex/tests/close-markets/programs/close-markets/Xargo.toml b/dex/tests/close-markets/programs/close-markets/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/dex/tests/close-markets/programs/close-markets/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/dex/tests/close-markets/programs/close-markets/src/lib.rs b/dex/tests/close-markets/programs/close-markets/src/lib.rs new file mode 100644 index 00000000..eedef2c9 --- /dev/null +++ b/dex/tests/close-markets/programs/close-markets/src/lib.rs @@ -0,0 +1,341 @@ +use anchor_lang::prelude::*; +use anchor_spl::dex; +use anchor_spl::token::{Mint, Token, TokenAccount}; + +pub use serum_dex; + +declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); + +pub const SERUM_MARKET_SEED: &[u8; 12] = b"serum_market"; +pub const REQUEST_QUEUE_SEED: &[u8; 13] = b"request_queue"; +pub const COIN_VAULT_SEED: &[u8; 10] = b"coin_vault"; +pub const PC_VAULT_SEED: &[u8; 8] = b"pc_vault"; +pub const OPEN_ORDERS_SEED: &[u8; 11] = b"open_orders"; + +// 388 for marketstate + 32 * 3 = 96 for pubkey + 992 for padding = 1476 +pub const SERUM_MARKET_SPACE: usize = 1476; +pub const REQUEST_QUEUE_SPACE: usize = 5120 + 12; +pub const OPEN_ORDERS_SPACE: usize = 3228; + +pub const COIN_LOT_SIZE: u64 = 10_000; +pub const PC_LOT_SIZE: u64 = 10; +pub const PC_DUST_THRESHOLD: u64 = 100; + +#[program] +pub mod close_markets { + use super::*; + pub fn initialize_market(ctx: Context, bumps: Bumps) -> ProgramResult { + let vault_signer_nonce = bumps.vault_signer as u64; + + let prune_auth = &mut *ctx.accounts.prune_auth; + prune_auth.payer = ctx.accounts.payer.key(); + prune_auth.signer_bump = [bumps.prune_auth]; + prune_auth.bumps = bumps; + + dex::initialize_market( + ctx.accounts.initialize_serum_market_ctx(), + COIN_LOT_SIZE, + PC_LOT_SIZE, + vault_signer_nonce, + PC_DUST_THRESHOLD, + )?; + + Ok(()) + } + + pub fn init_open_orders(ctx: Context) -> ProgramResult { + dex::init_open_orders( + ctx.accounts + .init_open_orders_ctx() + .with_signer(&[&ctx.accounts.prune_auth.signer_seeds()]), + )?; + + Ok(()) + } + + pub fn prune_open_orders(ctx: Context) -> ProgramResult { + let ix = serum_dex::instruction::prune( + &ctx.accounts.dex_program.key(), + &ctx.accounts.serum_market.key(), + &ctx.accounts.bids.key(), + &ctx.accounts.asks.key(), + &ctx.accounts.prune_auth.key(), + &ctx.accounts.open_orders.key(), + &ctx.accounts.payer.key(), + &ctx.accounts.event_queue.key(), + u16::MAX, + )?; + + solana_program::program::invoke_signed( + &ix, + &ToAccountInfos::to_account_infos(ctx.accounts), + &[&ctx.accounts.prune_auth.signer_seeds()], + )?; + + Ok(()) + } + + pub fn close_market(ctx: Context) -> ProgramResult { + let ix = serum_dex::instruction::close_market( + &ctx.accounts.dex_program.key(), + &ctx.accounts.serum_market.key(), + &ctx.accounts.request_queue.key(), + &ctx.accounts.event_queue.key(), + &ctx.accounts.bids.key(), + &ctx.accounts.asks.key(), + &ctx.accounts.prune_auth.key(), + &ctx.accounts.payer.key(), + )?; + + let seeds = &[ + b"prune_auth".as_ref(), + &[ctx.accounts.prune_auth.bumps.prune_auth], + ]; + let signer = &[&seeds[..]]; + + solana_program::program::invoke_signed( + &ix, + &ToAccountInfos::to_account_infos(ctx.accounts), + signer, + )?; + + Ok(()) + } +} + +#[derive(Accounts)] +#[instruction(bumps: Bumps)] +pub struct InitializeMarket<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + init, + seeds = [b"prune_auth".as_ref()], + bump = bumps.prune_auth, + payer = payer + )] + pub prune_auth: Box>, + #[account( + init, + mint::decimals = 6, + mint::authority = payer, + seeds = [b"usdc_mint".as_ref()], + bump = bumps.usdc_mint, + payer = payer + )] + pub usdc_mint: Box>, + #[account( + init, + mint::decimals = 6, + mint::authority = payer, + seeds = [b"serum_mint".as_ref()], + bump = bumps.serum_mint, + payer = payer + )] + pub serum_mint: Box>, + #[account( + init, + seeds = [SERUM_MARKET_SEED.as_ref()], + bump = bumps.serum_market, + space = SERUM_MARKET_SPACE, + payer = payer, + owner = dex_program.key( + ))] + pub serum_market: UncheckedAccount<'info>, + #[account( + init, + seeds = [REQUEST_QUEUE_SEED.as_ref()], + bump = bumps.request_queue, + space = REQUEST_QUEUE_SPACE, + payer = payer, + owner = dex_program.key( + ))] + pub request_queue: UncheckedAccount<'info>, + #[account( + init, + token::mint = serum_mint, + token::authority = vault_signer, + seeds = [COIN_VAULT_SEED.as_ref()], + bump = bumps.coin_vault, + payer = payer + )] + pub coin_vault: Box>, + #[account( + init, + token::mint = usdc_mint, + token::authority = vault_signer, + seeds = [PC_VAULT_SEED.as_ref()], + bump = bumps.pc_vault, + payer = payer + )] + pub pc_vault: Box>, + // TODO probably no way to verify these seeds since it uses a different heuristic + pub vault_signer: UncheckedAccount<'info>, + // These shouldn't need to be signers + #[account(mut)] + pub event_queue: UncheckedAccount<'info>, + #[account(mut)] + pub bids: UncheckedAccount<'info>, + #[account(mut)] + pub asks: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, + // TODO check this via anchor-spl? + pub dex_program: UncheckedAccount<'info>, + pub rent: Sysvar<'info, Rent>, +} + +impl<'info> InitializeMarket<'info> { + pub fn initialize_serum_market_ctx( + &self, + ) -> CpiContext<'_, '_, '_, 'info, dex::InitializeMarket<'info>> { + let cpi_accounts = dex::InitializeMarket { + market: self.serum_market.to_account_info(), + req_q: self.request_queue.to_account_info(), + event_q: self.event_queue.to_account_info(), + bids: self.bids.to_account_info(), + asks: self.asks.to_account_info(), + coin_vault: self.coin_vault.to_account_info(), + pc_vault: self.pc_vault.to_account_info(), + coin_mint: self.serum_mint.to_account_info(), + pc_mint: self.usdc_mint.to_account_info(), + rent: self.rent.to_account_info(), + }; + let cpi_ctx = CpiContext::new(self.dex_program.to_account_info(), cpi_accounts); + // prune_auth will be the open orders auth and prune auth + cpi_ctx.with_remaining_accounts(vec![ + self.prune_auth.to_account_info(), + self.prune_auth.to_account_info(), + ]) + } +} + +#[derive(Accounts)] +pub struct InitOpenOrders<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + seeds = [b"prune_auth".as_ref()], + bump = prune_auth.bumps.prune_auth, + )] + pub prune_auth: Box>, + #[account( + mut, + seeds = [SERUM_MARKET_SEED.as_ref()], + bump = prune_auth.bumps.serum_market, + )] + pub serum_market: UncheckedAccount<'info>, + #[account( + init, + seeds = [payer.key().as_ref(), b"open_orders".as_ref()], + bump, + space = OPEN_ORDERS_SPACE, + payer = payer, + owner = dex_program.key() + )] + pub open_orders: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, + // TODO switch this to a Program<'info, Dex> check, except our ProgramIDs will be different + pub dex_program: UncheckedAccount<'info>, + pub rent: Sysvar<'info, Rent>, +} + +impl<'info> InitOpenOrders<'info> { + pub fn init_open_orders_ctx( + &self, + ) -> CpiContext<'_, '_, '_, 'info, dex::InitOpenOrders<'info>> { + let cpi_accounts = dex::InitOpenOrders { + open_orders: self.open_orders.to_account_info(), + authority: self.payer.to_account_info(), + market: self.serum_market.to_account_info(), + rent: self.rent.to_account_info(), + }; + let cpi_ctx = CpiContext::new(self.dex_program.to_account_info(), cpi_accounts); + cpi_ctx.with_remaining_accounts(vec![self.prune_auth.to_account_info()]) + } +} + +#[derive(Accounts)] +pub struct PruneOpenOrders<'info> { + #[account(mut)] + pub payer: UncheckedAccount<'info>, + #[account( + seeds = [b"prune_auth".as_ref()], + bump = prune_auth.bumps.prune_auth, + )] + pub prune_auth: Box>, + #[account( + mut, + seeds = [SERUM_MARKET_SEED.as_ref()], + bump = prune_auth.bumps.serum_market, + )] + pub serum_market: UncheckedAccount<'info>, + #[account(mut)] + pub event_queue: UncheckedAccount<'info>, + #[account(mut)] + pub bids: UncheckedAccount<'info>, + #[account(mut)] + pub asks: UncheckedAccount<'info>, + #[account( + mut, + seeds = [payer.key().as_ref() ,b"open_orders".as_ref()], + bump, + )] + pub open_orders: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, + // TODO switch this to a Program<'info, Dex> check, except our ProgramIDs will be different + pub dex_program: UncheckedAccount<'info>, + pub rent: Sysvar<'info, Rent>, +} + +#[derive(Accounts)] +pub struct CloseMarket<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + seeds = [b"prune_auth".as_ref()], + bump = prune_auth.bumps.prune_auth, + )] + pub prune_auth: Box>, + #[account( + mut, + seeds = [SERUM_MARKET_SEED.as_ref()], + bump = prune_auth.bumps.serum_market, + )] + pub serum_market: UncheckedAccount<'info>, + #[account(mut)] + pub request_queue: UncheckedAccount<'info>, + #[account(mut)] + pub event_queue: UncheckedAccount<'info>, + #[account(mut)] + pub bids: UncheckedAccount<'info>, + #[account(mut)] + pub asks: UncheckedAccount<'info>, + pub dex_program: UncheckedAccount<'info>, +} + +#[account] +#[derive(Default)] +pub struct PruneAuth { + pub payer: Pubkey, + pub signer_bump: [u8; 1], + pub bumps: Bumps, +} + +impl PruneAuth { + pub fn signer_seeds(&self) -> [&[u8]; 2] { + [b"prune_auth".as_ref(), &self.signer_bump] + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Default, Clone)] +pub struct Bumps { + pub prune_auth: u8, + pub usdc_mint: u8, + pub serum_mint: u8, + pub serum_market: u8, + pub request_queue: u8, + pub coin_vault: u8, + pub pc_vault: u8, + pub vault_signer: u8, // This one follows a different bump discovery formula +} diff --git a/dex/tests/close-markets/readme.md b/dex/tests/close-markets/readme.md new file mode 100644 index 00000000..682f09ed --- /dev/null +++ b/dex/tests/close-markets/readme.md @@ -0,0 +1,4 @@ +Run the following command to ensure there's a built serum dex program for the tests to interact with +``` +cd ../..; cargo build-bpf +``` diff --git a/dex/tests/close-markets/tests/close-markets.ts b/dex/tests/close-markets/tests/close-markets.ts new file mode 100644 index 00000000..b657efbe --- /dev/null +++ b/dex/tests/close-markets/tests/close-markets.ts @@ -0,0 +1,591 @@ +import * as anchor from "@project-serum/anchor"; +import { Program, BN } from "@project-serum/anchor"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + Token, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { PublicKey, Keypair, Transaction } from "@solana/web3.js"; + +import { Market, OpenOrders, DexInstructions } from "@project-serum/serum"; +import { crankEventQueue, mintToAccount, sleep } from "./utils"; + +const DEX_PID = new anchor.web3.PublicKey( + "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", +); + +describe("close-markets", () => { + // Configure the client to use the local cluster. + anchor.setProvider(anchor.Provider.env()); + + const program = anchor.workspace.CloseMarkets as Program; + + let secondarySigner = new Keypair(); + let eventQueueKeypair; + let bidsKeypair; + let asksKeypair; + let secondarySignerUsdcPubkey; + let signerUsdcPubkey; + let signerSerumPubkey; + + let secondarySignerSerumPubkey; + + let pruneAuth; + let serumMint; + let usdcMint; + let serumMarket; + + let serumMarketBump; + let pruneAuthBump; + let usdcMintBump; + let serumMintBump; + + let requestQueue; + let requestQueueBump; + let coinVault; + let coinVaultBump; + + let pcVault; + let pcVaultBump; + + let vaultSigner; + let vaultSignerNonce; + + let openOrders; + let openOrdersProvider; + + it("Initialize Market!", async () => { + [pruneAuth, pruneAuthBump] = await anchor.web3.PublicKey.findProgramAddress( + [Buffer.from("prune_auth")], + program.programId, + ); + + [serumMint, serumMintBump] = await anchor.web3.PublicKey.findProgramAddress( + [Buffer.from("serum_mint")], + program.programId, + ); + [usdcMint, usdcMintBump] = await anchor.web3.PublicKey.findProgramAddress( + [Buffer.from("usdc_mint")], + program.programId, + ); + + [serumMarket, serumMarketBump] = + await anchor.web3.PublicKey.findProgramAddress( + [Buffer.from("serum_market")], + program.programId, + ); + [requestQueue, requestQueueBump] = + await anchor.web3.PublicKey.findProgramAddress( + [Buffer.from("request_queue")], + program.programId, + ); + [coinVault, coinVaultBump] = await anchor.web3.PublicKey.findProgramAddress( + [Buffer.from("coin_vault")], + program.programId, + ); + [pcVault, pcVaultBump] = await anchor.web3.PublicKey.findProgramAddress( + [Buffer.from("pc_vault")], + program.programId, + ); + + [vaultSigner, vaultSignerNonce] = await getVaultSignerAndNonce(serumMarket); + eventQueueKeypair = anchor.web3.Keypair.generate(); + bidsKeypair = anchor.web3.Keypair.generate(); + asksKeypair = anchor.web3.Keypair.generate(); + + let bumps = new Bumps(); + bumps.pruneAuth = pruneAuthBump; + bumps.usdcMint = usdcMintBump; + bumps.serumMint = serumMintBump; + bumps.serumMarket = serumMarketBump; + bumps.requestQueue = requestQueueBump; + bumps.coinVault = coinVaultBump; + bumps.pcVault = pcVaultBump; + bumps.vaultSigner = vaultSignerNonce; + + await program.rpc.initializeMarket(bumps, { + accounts: { + payer: program.provider.wallet.publicKey, + pruneAuth, + usdcMint, + serumMint, + serumMarket, + requestQueue, + coinVault, + pcVault, + vaultSigner, + eventQueue: eventQueueKeypair.publicKey, + bids: bidsKeypair.publicKey, + asks: asksKeypair.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + dexProgram: DEX_PID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + instructions: [ + anchor.web3.SystemProgram.createAccount({ + fromPubkey: program.provider.wallet.publicKey, + newAccountPubkey: eventQueueKeypair.publicKey, + lamports: + await program.provider.connection.getMinimumBalanceForRentExemption( + 262144 + 12, + ), + space: 262144 + 12, + programId: DEX_PID, + }), + anchor.web3.SystemProgram.createAccount({ + fromPubkey: program.provider.wallet.publicKey, + newAccountPubkey: bidsKeypair.publicKey, + lamports: + await program.provider.connection.getMinimumBalanceForRentExemption( + 65536 + 12, + ), + space: 65536 + 12, + programId: DEX_PID, + }), + anchor.web3.SystemProgram.createAccount({ + fromPubkey: program.provider.wallet.publicKey, + newAccountPubkey: asksKeypair.publicKey, + lamports: + await program.provider.connection.getMinimumBalanceForRentExemption( + 65536 + 12, + ), + space: 65536 + 12, + programId: DEX_PID, + }), + ], + signers: [eventQueueKeypair, bidsKeypair, asksKeypair], + }); + }); + + it("Mints Tokens for all the accounts", async () => { + await program.provider.connection.requestAirdrop( + secondarySigner.publicKey, + 1_000_000_000, + ); + + secondarySignerUsdcPubkey = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + usdcMint, + secondarySigner.publicKey, + ); + + let createUserUsdcInstr = Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + usdcMint, + secondarySignerUsdcPubkey, + secondarySigner.publicKey, + + program.provider.wallet.publicKey, + ); + let createUserUsdcTrns = new anchor.web3.Transaction().add( + createUserUsdcInstr, + ); + await program.provider.send(createUserUsdcTrns); + + await mintToAccount( + program.provider, + usdcMint, + secondarySignerUsdcPubkey, + new anchor.BN(500000000), + program.provider.wallet.publicKey, + ); + + signerUsdcPubkey = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + usdcMint, + program.provider.wallet.publicKey, + ); + + let createSignerUsdcInstr = Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + usdcMint, + signerUsdcPubkey, + program.provider.wallet.publicKey, + + program.provider.wallet.publicKey, + ); + let createSignerUsdcTrns = new anchor.web3.Transaction().add( + createSignerUsdcInstr, + ); + await program.provider.send(createSignerUsdcTrns); + + await mintToAccount( + program.provider, + usdcMint, + signerUsdcPubkey, + new anchor.BN(500000000), + program.provider.wallet.publicKey, + ); + + secondarySignerSerumPubkey = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + serumMint, + secondarySigner.publicKey, + ); + + let createUserSerumInstr = Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + serumMint, + secondarySignerSerumPubkey, + secondarySigner.publicKey, + + program.provider.wallet.publicKey, + ); + let createUserSerumTrns = new anchor.web3.Transaction().add( + createUserSerumInstr, + ); + await program.provider.send(createUserSerumTrns); + + await mintToAccount( + program.provider, + serumMint, + secondarySignerSerumPubkey, + new anchor.BN(500000000), + program.provider.wallet.publicKey, + ); + + signerSerumPubkey = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + serumMint, + program.provider.wallet.publicKey, + ); + + let createSignerSerumInstr = Token.createAssociatedTokenAccountInstruction( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + serumMint, + signerSerumPubkey, + program.provider.wallet.publicKey, + program.provider.wallet.publicKey, + ); + + let createSignerSerum = new anchor.web3.Transaction().add( + createSignerSerumInstr, + ); + + await program.provider.send(createSignerSerum); + + await mintToAccount( + program.provider, + serumMint, + signerSerumPubkey, + new anchor.BN(500000000), + program.provider.wallet.publicKey, + ); + }); + + it("Fails to close a market with an outstanding order", async () => { + [openOrders] = await anchor.web3.PublicKey.findProgramAddress( + [secondarySigner.publicKey.toBuffer(), Buffer.from("open_orders")], + program.programId, + ); + + [openOrdersProvider] = await anchor.web3.PublicKey.findProgramAddress( + [ + program.provider.wallet.publicKey.toBuffer(), + Buffer.from("open_orders"), + ], + program.programId, + ); + + await program.rpc.initOpenOrders({ + accounts: { + payer: secondarySigner.publicKey, + pruneAuth, + serumMarket, + openOrders, + systemProgram: anchor.web3.SystemProgram.programId, + dexProgram: DEX_PID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [secondarySigner], + }); + + await program.rpc.initOpenOrders({ + accounts: { + payer: program.provider.wallet.publicKey, + pruneAuth, + serumMarket, + openOrders: openOrdersProvider, + systemProgram: anchor.web3.SystemProgram.programId, + dexProgram: DEX_PID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + }); + + let market = await Market.load( + program.provider.connection, + serumMarket, + undefined, + DEX_PID, + ); + + const newTransaction = new Transaction(); + + await program.provider.connection.getTokenAccountBalance( + secondarySignerUsdcPubkey, + ); + + const getInstr = await market.makePlaceOrderInstruction( + program.provider.connection, + { + owner: secondarySigner, + payer: secondarySignerUsdcPubkey, + side: "buy", + price: 50, + size: 2, + orderType: "limit", + clientId: new BN(0), + openOrdersAddressKey: openOrders, + }, + ); + + newTransaction.add(getInstr); + + await market._sendTransaction(program.provider.connection, newTransaction, [ + secondarySigner, + ]); + + try { + await program.rpc.closeMarket({ + accounts: { + payer: program.provider.wallet.publicKey, + pruneAuth, + serumMarket, + requestQueue, + eventQueue: eventQueueKeypair.publicKey, + bids: bidsKeypair.publicKey, + asks: asksKeypair.publicKey, + dexProgram: DEX_PID, + }, + }); + } catch (e) { + console.log("close market failed!"); + } + }); + + it("Matches an order and still fails", async () => { + let market = await Market.load( + program.provider.connection, + serumMarket, + undefined, + DEX_PID, + ); + + const newTransaction = new Transaction(); + + const getInstr = await market.makePlaceOrderInstruction( + program.provider.connection, + { + owner: secondarySigner, + payer: secondarySignerSerumPubkey, + side: "sell", + price: 50, + size: 10, + orderType: "limit", + clientId: new BN(0), + openOrdersAddressKey: openOrders, + }, + ); + + newTransaction.add(getInstr); + + await market._sendTransaction(program.provider.connection, newTransaction, [ + secondarySigner, + ]); + + await crankEventQueue(program.provider, market); + await sleep(1000); + + try { + await program.rpc.closeMarket({ + accounts: { + payer: program.provider.wallet.publicKey, + pruneAuth, + serumMarket, + requestQueue, + eventQueue: eventQueueKeypair.publicKey, + bids: bidsKeypair.publicKey, + asks: asksKeypair.publicKey, + dexProgram: DEX_PID, + }, + }); + } catch (e) { + console.log("close market failed!"); + } + }); + + it("Closes a Market with no outstanding orders, no uncranked events, but an open orders account that hasn't been settled and then settles it after the market has been closed", async () => { + let market = await Market.load( + program.provider.connection, + serumMarket, + undefined, + DEX_PID, + ); + + await program.rpc.pruneOpenOrders({ + accounts: { + payer: secondarySigner.publicKey, + pruneAuth, + serumMarket, + requestQueue, + eventQueue: eventQueueKeypair.publicKey, + bids: bidsKeypair.publicKey, + asks: asksKeypair.publicKey, + openOrders: openOrders, + systemProgram: anchor.web3.SystemProgram.programId, + dexProgram: DEX_PID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + }); + + await crankEventQueue(program.provider, market); + + await program.rpc.closeMarket({ + accounts: { + payer: program.provider.wallet.publicKey, + pruneAuth, + serumMarket, + requestQueue, + eventQueue: eventQueueKeypair.publicKey, + bids: bidsKeypair.publicKey, + asks: asksKeypair.publicKey, + dexProgram: DEX_PID, + }, + }); + }); + + it("Allows retrieval and settling of rent after the market closes", async () => { + let market = await Market.load( + program.provider.connection, + serumMarket, + undefined, + DEX_PID, + ); + + let { baseVault, quoteVault } = market.decoded; + + await OpenOrders.load(program.provider.connection, openOrders, DEX_PID); + + const recentBlockhash = await ( + await program.provider.connection.getRecentBlockhash() + ).blockhash; + + const settleTransaction = new Transaction({ + recentBlockhash, + }); + + const getSettle = DexInstructions.settleFunds({ + market: serumMarket, + openOrders, + owner: secondarySigner.publicKey, + baseVault, + quoteVault, + vaultSigner, + quoteWallet: secondarySignerUsdcPubkey, + baseWallet: secondarySignerSerumPubkey, + programId: DEX_PID, + }); + + settleTransaction.add(getSettle); + + let tokenBalanceBeforeSettling = + await program.provider.connection.getTokenAccountBalance( + secondarySignerUsdcPubkey, + ); + + console.log( + "tokenBalanceBeforeSettling", + tokenBalanceBeforeSettling.value.uiAmount, + ); + + const txnsig = await program.provider.connection.sendTransaction( + settleTransaction, + [secondarySigner], + ); + + await sleep(5000); + + let tokenBalanceAfterSettling = + await program.provider.connection.getTokenAccountBalance( + secondarySignerUsdcPubkey, + ); + + console.log( + "tokenBalanceAfterSettling", + tokenBalanceAfterSettling.value.uiAmount, + ); + + console.log(txnsig); + + const nextBlockhash = + await program.provider.connection.getRecentBlockhash(); + + const secondTxn = new Transaction({ + recentBlockhash: nextBlockhash.blockhash, + }); + + const getInstr = DexInstructions.closeOpenOrders({ + market: serumMarket, + openOrders, + solWallet: secondarySigner.publicKey, + owner: secondarySigner.publicKey, + programId: DEX_PID, + }); + + secondTxn.add(getInstr); + + console.log( + "payer account balance after open orders close: ", + ( + await program.provider.connection.getBalance(secondarySigner.publicKey) + ).toString(), + ); + await program.provider.connection.sendTransaction(secondTxn, [ + secondarySigner, + ]); + + await sleep(5000); + + console.log( + "payer account balance after open orders close: ", + ( + await program.provider.connection.getBalance(secondarySigner.publicKey) + ).toString(), + ); + }); + + function Bumps() { + this.pruneAuth; + this.usdcMint; + this.serumMint; + this.serumMarket; + this.requestQueue; + this.coinVault; + this.pcVault; + this.vaultSigner; + } + + async function getVaultSignerAndNonce(market: PublicKey) { + const nonce = new BN(0); + while (true) { + try { + const vaultSigner = await anchor.web3.PublicKey.createProgramAddress( + [market.toBuffer(), nonce.toArrayLike(Buffer, "le", 8)], + DEX_PID, + ); + return [vaultSigner, nonce]; + } catch (e) { + nonce.iaddn(1); + } + } + } +}); diff --git a/dex/tests/close-markets/tests/utils.ts b/dex/tests/close-markets/tests/utils.ts new file mode 100644 index 00000000..ac7cb44a --- /dev/null +++ b/dex/tests/close-markets/tests/utils.ts @@ -0,0 +1,78 @@ +import * as anchor from "@project-serum/anchor"; + +import { PublicKey, Transaction } from "@solana/web3.js"; + +import { TokenInstructions, OpenOrders } from "@project-serum/serum"; + +const DEX_PID = new anchor.web3.PublicKey( + "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", +); + +export function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export async function mintToAccount( + provider, + mint, + destination, + amount, + mintAuthority, +) { + const tx = new Transaction(); + tx.add( + ...(await createMintToAccountInstrs( + mint, + destination, + amount, + mintAuthority, + )), + ); + await provider.send(tx, []); + return; +} + +export async function crankEventQueue(provider, marketClient) { + let eq = await marketClient.loadEventQueue(provider.connection); + let count = 0; + while (eq.length > 0) { + const accounts = new Set(); + for (const event of eq) { + accounts.add(event.openOrders.toBase58()); + } + let orderedAccounts = Array.from(accounts) + .map((s) => new PublicKey(s)) + .sort((a, b) => a.toBuffer().swap64().compare(b.toBuffer().swap64())); + + let openOrdersRaw = await provider.connection.getAccountInfo( + orderedAccounts[0], + ); + OpenOrders.fromAccountInfo(orderedAccounts[0], openOrdersRaw, DEX_PID); + + const tx = new anchor.web3.Transaction(); + tx.add(marketClient.makeConsumeEventsInstruction(orderedAccounts, 20)); + await provider.send(tx); + eq = await marketClient.loadEventQueue(provider.connection); + console.log(eq.length); + count += 1; + if (count > 4) { + break; + } + } +} + +export async function createMintToAccountInstrs( + mint, + destination, + amount, + mintAuthority, +) { + return [ + TokenInstructions.mintTo({ + mint, + destination, + amount, + mintAuthority, + }), + ]; +} diff --git a/dex/tests/close-markets/tsconfig.json b/dex/tests/close-markets/tsconfig.json new file mode 100644 index 00000000..cd5d2e3d --- /dev/null +++ b/dex/tests/close-markets/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +}