From 70316fb927f2e1d6fc1f8fa96a461f8f00010a89 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Fri, 18 Mar 2022 15:58:39 +0100 Subject: [PATCH] Serum: Settle funds instruction Also move serum3 cpi helpers to a separate file, to allow reuse of calls like settle_funds from multiple mango instructions. --- programs/mango-v4/src/instructions/mod.rs | 2 + .../src/instructions/serum3_place_order.rs | 123 +++++-------- .../src/instructions/serum3_settle_funds.rs | 169 ++++++++++++++++++ programs/mango-v4/src/lib.rs | 5 + programs/mango-v4/src/serum3_cpi.rs | 119 ++++++++++++ .../tests/program_test/mango_client.rs | 82 ++++++++- programs/mango-v4/tests/test_serum.rs | 12 ++ 7 files changed, 432 insertions(+), 80 deletions(-) create mode 100644 programs/mango-v4/src/instructions/serum3_settle_funds.rs create mode 100644 programs/mango-v4/src/serum3_cpi.rs diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index 25da4a0cb..781cec075 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -8,6 +8,7 @@ pub use register_token::*; pub use serum3_create_open_orders::*; pub use serum3_place_order::*; pub use serum3_register_market::*; +pub use serum3_settle_funds::*; pub use set_stub_oracle::*; pub use withdraw::*; @@ -21,5 +22,6 @@ mod register_token; mod serum3_create_open_orders; mod serum3_place_order; mod serum3_register_market; +mod serum3_settle_funds; mod set_stub_oracle; mod withdraw; diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index c8590f304..47c06eb33 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -160,7 +160,6 @@ pub struct Serum3PlaceOrder<'info> { pub base_vault: Box>, pub token_program: Program<'info, Token>, - pub rent: Sysvar<'info, Rent>, } pub fn serum3_place_order( @@ -219,8 +218,8 @@ pub fn serum3_place_order( // Apply the order to serum. Also immediately settle, in case the order // matched against an existing other order. // - cpi_place_order(&ctx, order.0)?; - cpi_settle_funds(&ctx)?; + cpi_place_order(&ctx.accounts, order.0)?; + cpi_settle_funds(&ctx.accounts)?; // // After-order tracking @@ -257,90 +256,56 @@ pub fn serum3_place_order( Ok(()) } -fn cpi_place_order(ctx: &Context, order: NewOrderInstructionV3) -> Result<()> { +fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> { + use crate::serum3_cpi; + let order_payer_token_account = match order.side { - Side::Bid => &ctx.accounts.quote_vault, - Side::Ask => &ctx.accounts.base_vault, + Side::Bid => &ctx.quote_vault, + Side::Ask => &ctx.base_vault, }; - let data = serum_dex::instruction::MarketInstruction::NewOrderV3(order).pack(); - let instruction = solana_program::instruction::Instruction { - program_id: *ctx.accounts.serum_program.key, - data, - accounts: vec![ - AccountMeta::new(*ctx.accounts.serum_market_external.key, false), - AccountMeta::new(*ctx.accounts.open_orders.key, false), - AccountMeta::new(*ctx.accounts.market_request_queue.key, false), - AccountMeta::new(*ctx.accounts.market_event_queue.key, false), - AccountMeta::new(*ctx.accounts.market_bids.key, false), - AccountMeta::new(*ctx.accounts.market_asks.key, false), - AccountMeta::new(order_payer_token_account.key(), false), - AccountMeta::new_readonly(ctx.accounts.group.key(), true), - AccountMeta::new(*ctx.accounts.market_base_vault.key, false), - AccountMeta::new(*ctx.accounts.market_quote_vault.key, false), - AccountMeta::new_readonly(*ctx.accounts.token_program.key, false), - AccountMeta::new_readonly(ctx.accounts.group.key(), false), - ], - }; - let account_infos = [ - ctx.accounts.serum_program.to_account_info(), // Have to add account of the program id - ctx.accounts.serum_market_external.to_account_info(), - ctx.accounts.open_orders.to_account_info(), - ctx.accounts.market_request_queue.to_account_info(), - ctx.accounts.market_event_queue.to_account_info(), - ctx.accounts.market_bids.to_account_info(), - ctx.accounts.market_asks.to_account_info(), - order_payer_token_account.to_account_info(), - ctx.accounts.group.to_account_info(), - ctx.accounts.market_base_vault.to_account_info(), - ctx.accounts.market_quote_vault.to_account_info(), - ctx.accounts.token_program.to_account_info(), - ctx.accounts.group.to_account_info(), - ]; + let group = ctx.group.load()?; + serum3_cpi::place_order( + &group, + serum3_cpi::PlaceOrder { + program: ctx.serum_program.to_account_info(), + market: ctx.serum_market_external.to_account_info(), + request_queue: ctx.market_request_queue.to_account_info(), + event_queue: ctx.market_event_queue.to_account_info(), + bids: ctx.market_bids.to_account_info(), + asks: ctx.market_asks.to_account_info(), + base_vault: ctx.market_base_vault.to_account_info(), + quote_vault: ctx.market_quote_vault.to_account_info(), + token_program: ctx.token_program.to_account_info(), - let group = ctx.accounts.group.load()?; - let seeds = group_seeds!(group); - solana_program::program::invoke_signed_unchecked(&instruction, &account_infos, &[seeds])?; + open_orders: ctx.open_orders.to_account_info(), + order_payer_token_account: order_payer_token_account.to_account_info(), + user_authority: ctx.group.to_account_info(), + }, + order, + )?; Ok(()) } -fn cpi_settle_funds(ctx: &Context) -> Result<()> { - let data = serum_dex::instruction::MarketInstruction::SettleFunds.pack(); - let instruction = solana_program::instruction::Instruction { - program_id: *ctx.accounts.serum_program.key, - data, - accounts: vec![ - AccountMeta::new(*ctx.accounts.serum_market_external.key, false), - AccountMeta::new(*ctx.accounts.open_orders.key, false), - AccountMeta::new_readonly(ctx.accounts.group.key(), true), - AccountMeta::new(*ctx.accounts.market_base_vault.key, false), - AccountMeta::new(*ctx.accounts.market_quote_vault.key, false), - AccountMeta::new(ctx.accounts.base_vault.key(), false), - AccountMeta::new(ctx.accounts.quote_vault.key(), false), - AccountMeta::new_readonly(*ctx.accounts.market_vault_signer.key, false), - AccountMeta::new_readonly(*ctx.accounts.token_program.key, false), - AccountMeta::new(ctx.accounts.quote_vault.key(), false), - ], - }; - - let account_infos = [ - ctx.accounts.serum_market_external.to_account_info(), - ctx.accounts.serum_market_external.to_account_info(), - ctx.accounts.open_orders.to_account_info(), - ctx.accounts.group.to_account_info(), - ctx.accounts.market_base_vault.to_account_info(), - ctx.accounts.market_quote_vault.to_account_info(), - ctx.accounts.base_vault.to_account_info(), - ctx.accounts.quote_vault.to_account_info(), - ctx.accounts.market_vault_signer.to_account_info(), - ctx.accounts.token_program.to_account_info(), - ctx.accounts.quote_vault.to_account_info(), - ]; - - let group = ctx.accounts.group.load()?; - let seeds = group_seeds!(group); - solana_program::program::invoke_signed_unchecked(&instruction, &account_infos, &[seeds])?; +fn cpi_settle_funds(ctx: &Serum3PlaceOrder) -> Result<()> { + use crate::serum3_cpi; + let group = ctx.group.load()?; + serum3_cpi::settle_funds( + &group, + serum3_cpi::SettleFunds { + program: ctx.serum_program.to_account_info(), + market: ctx.serum_market_external.to_account_info(), + open_orders: ctx.open_orders.to_account_info(), + open_orders_authority: ctx.group.to_account_info(), + base_vault: ctx.market_base_vault.to_account_info(), + quote_vault: ctx.market_quote_vault.to_account_info(), + user_base_wallet: ctx.base_vault.to_account_info(), + user_quote_wallet: ctx.quote_vault.to_account_info(), + vault_signer: ctx.market_vault_signer.to_account_info(), + token_program: ctx.token_program.to_account_info(), + }, + )?; Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs new file mode 100644 index 000000000..4a9d9b412 --- /dev/null +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -0,0 +1,169 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Token, TokenAccount}; + +use crate::error::*; +use crate::state::*; + +#[derive(Accounts)] +pub struct Serum3SettleFunds<'info> { + pub group: AccountLoader<'info, Group>, + + #[account( + mut, + has_one = group, + has_one = owner, + )] + pub account: AccountLoader<'info, MangoAccount>, + pub owner: Signer<'info>, + + // Validated inline + #[account(mut)] + pub open_orders: UncheckedAccount<'info>, + + #[account( + has_one = group, + has_one = serum_program, + has_one = serum_market_external, + )] + pub serum_market: AccountLoader<'info, Serum3Market>, + pub serum_program: UncheckedAccount<'info>, + #[account(mut)] + pub serum_market_external: UncheckedAccount<'info>, + + // These accounts are forwarded directly to the serum cpi call + // and are validated there. + #[account(mut)] + pub market_base_vault: UncheckedAccount<'info>, + #[account(mut)] + pub market_quote_vault: UncheckedAccount<'info>, + // needed for the automatic settle_funds call + pub market_vault_signer: UncheckedAccount<'info>, + + // TODO: do we need to pass both, or just payer? + // TODO: if we potentially settle immediately, they all need to be mut? + // TODO: Can we reduce the number of accounts by requiring the banks + // to be in the remainingAccounts (where they need to be anyway, for + // health checks - but they need to be mut) + // Validated inline + #[account(mut)] + pub quote_bank: AccountLoader<'info, Bank>, + #[account(mut)] + pub quote_vault: Box>, + #[account(mut)] + pub base_bank: AccountLoader<'info, Bank>, + #[account(mut)] + pub base_vault: Box>, + + pub token_program: Program<'info, Token>, +} + +pub fn serum3_settle_funds(ctx: Context) -> Result<()> { + // + // Validation + // + { + let account = ctx.accounts.account.load()?; + let serum_market = ctx.accounts.serum_market.load()?; + + // Validate open_orders + require!( + account + .serum3_account_map + .find(serum_market.market_index) + .ok_or(error!(MangoError::SomeError))? + .open_orders + == ctx.accounts.open_orders.key(), + MangoError::SomeError + ); + + // Validate banks and vaults + let quote_bank = ctx.accounts.quote_bank.load()?; + require!( + quote_bank.vault == ctx.accounts.quote_vault.key(), + MangoError::SomeError + ); + require!( + quote_bank.token_index == serum_market.quote_token_index, + MangoError::SomeError + ); + let base_bank = ctx.accounts.base_bank.load()?; + require!( + base_bank.vault == ctx.accounts.base_vault.key(), + MangoError::SomeError + ); + require!( + base_bank.token_index == serum_market.base_token_index, + MangoError::SomeError + ); + } + + // + // Before-order tracking + // + + let before_base_vault = ctx.accounts.base_vault.amount; + let before_quote_vault = ctx.accounts.quote_vault.amount; + + // TODO: pre-health check + + // + // Settle + // + cpi_settle_funds(&ctx.accounts)?; + + // + // After-order tracking + // + ctx.accounts.base_vault.reload()?; + ctx.accounts.quote_vault.reload()?; + let after_base_vault = ctx.accounts.base_vault.amount; + let after_quote_vault = ctx.accounts.quote_vault.amount; + + // Charge the difference in vault balances to the user's account + { + let mut account = ctx.accounts.account.load_mut()?; + + let mut base_bank = ctx.accounts.base_bank.load_mut()?; + let base_position = account.token_account_map.get_mut(base_bank.token_index)?; + base_bank.change(base_position, (after_base_vault - before_base_vault) as i64)?; + + let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; + let quote_position = account.token_account_map.get_mut(quote_bank.token_index)?; + quote_bank.change( + quote_position, + (after_quote_vault - before_quote_vault) as i64, + )?; + } + + // + // Health check + // + let account = ctx.accounts.account.load()?; + let health = compute_health(&account, &ctx.remaining_accounts)?; + msg!("health: {}", health); + require!(health >= 0, MangoError::SomeError); + + Ok(()) +} + +fn cpi_settle_funds(ctx: &Serum3SettleFunds) -> Result<()> { + use crate::serum3_cpi; + let group = ctx.group.load()?; + serum3_cpi::settle_funds( + &group, + serum3_cpi::SettleFunds { + program: ctx.serum_program.to_account_info(), + market: ctx.serum_market_external.to_account_info(), + open_orders: ctx.open_orders.to_account_info(), + open_orders_authority: ctx.group.to_account_info(), + base_vault: ctx.market_base_vault.to_account_info(), + quote_vault: ctx.market_quote_vault.to_account_info(), + user_base_wallet: ctx.base_vault.to_account_info(), + user_quote_wallet: ctx.quote_vault.to_account_info(), + vault_signer: ctx.market_vault_signer.to_account_info(), + token_program: ctx.token_program.to_account_info(), + }, + )?; + + Ok(()) +} diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 094bac37c..37ae14ff8 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -12,6 +12,7 @@ use instructions::*; pub mod address_lookup_table; pub mod error; pub mod instructions; +mod serum3_cpi; pub mod state; use state::{Serum3MarketIndex, TokenIndex}; @@ -97,6 +98,10 @@ pub mod mango_v4 { instructions::serum3_place_order(ctx, order) } + pub fn serum3_settle_funds(ctx: Context) -> Result<()> { + instructions::serum3_settle_funds(ctx) + } + pub fn create_perp_market( ctx: Context, quote_lot_size: i64, diff --git a/programs/mango-v4/src/serum3_cpi.rs b/programs/mango-v4/src/serum3_cpi.rs new file mode 100644 index 000000000..88a0661f1 --- /dev/null +++ b/programs/mango-v4/src/serum3_cpi.rs @@ -0,0 +1,119 @@ +use anchor_lang::prelude::*; +use anchor_spl::dex::serum_dex; + +use crate::state::*; + +pub struct SettleFunds<'info> { + pub program: AccountInfo<'info>, + pub market: AccountInfo<'info>, + pub open_orders: AccountInfo<'info>, + pub open_orders_authority: AccountInfo<'info>, + pub base_vault: AccountInfo<'info>, + pub quote_vault: AccountInfo<'info>, + pub user_base_wallet: AccountInfo<'info>, + pub user_quote_wallet: AccountInfo<'info>, + pub vault_signer: AccountInfo<'info>, + pub token_program: AccountInfo<'info>, +} + +pub fn settle_funds(group: &Group, ctx: SettleFunds) -> Result<()> { + let data = serum_dex::instruction::MarketInstruction::SettleFunds.pack(); + let instruction = solana_program::instruction::Instruction { + program_id: *ctx.program.key, + data, + accounts: vec![ + AccountMeta::new(*ctx.market.key, false), + AccountMeta::new(*ctx.open_orders.key, false), + AccountMeta::new_readonly(*ctx.open_orders_authority.key, true), + AccountMeta::new(*ctx.base_vault.key, false), + AccountMeta::new(*ctx.quote_vault.key, false), + AccountMeta::new(*ctx.user_base_wallet.key, false), + AccountMeta::new(*ctx.user_quote_wallet.key, false), + AccountMeta::new_readonly(*ctx.vault_signer.key, false), + AccountMeta::new_readonly(*ctx.token_program.key, false), + AccountMeta::new(*ctx.user_quote_wallet.key, false), + ], + }; + + let account_infos = [ + ctx.program, + ctx.market, + ctx.open_orders, + ctx.open_orders_authority, + ctx.base_vault, + ctx.quote_vault, + ctx.user_base_wallet, + ctx.user_quote_wallet.clone(), + ctx.vault_signer, + ctx.token_program, + ctx.user_quote_wallet, + ]; + + let seeds = group_seeds!(group); + solana_program::program::invoke_signed_unchecked(&instruction, &account_infos, &[seeds])?; + + Ok(()) +} + +pub struct PlaceOrder<'info> { + pub program: AccountInfo<'info>, + pub market: AccountInfo<'info>, + pub request_queue: AccountInfo<'info>, + pub event_queue: AccountInfo<'info>, + pub bids: AccountInfo<'info>, + pub asks: AccountInfo<'info>, + pub base_vault: AccountInfo<'info>, + pub quote_vault: AccountInfo<'info>, + pub token_program: AccountInfo<'info>, + + pub open_orders: AccountInfo<'info>, + pub order_payer_token_account: AccountInfo<'info>, + // must cover the open_orders and the order_payer_token_account + pub user_authority: AccountInfo<'info>, +} + +pub fn place_order( + group: &Group, + ctx: PlaceOrder, + order: serum_dex::instruction::NewOrderInstructionV3, +) -> Result<()> { + let data = serum_dex::instruction::MarketInstruction::NewOrderV3(order).pack(); + let instruction = solana_program::instruction::Instruction { + program_id: *ctx.program.key, + data, + accounts: vec![ + AccountMeta::new(*ctx.market.key, false), + AccountMeta::new(*ctx.open_orders.key, false), + AccountMeta::new(*ctx.request_queue.key, false), + AccountMeta::new(*ctx.event_queue.key, false), + AccountMeta::new(*ctx.bids.key, false), + AccountMeta::new(*ctx.asks.key, false), + AccountMeta::new(*ctx.order_payer_token_account.key, false), + AccountMeta::new_readonly(*ctx.user_authority.key, true), + AccountMeta::new(*ctx.base_vault.key, false), + AccountMeta::new(*ctx.quote_vault.key, false), + AccountMeta::new_readonly(*ctx.token_program.key, false), + AccountMeta::new_readonly(*ctx.user_authority.key, false), + ], + }; + let account_infos = [ + ctx.program, + ctx.market, + ctx.open_orders, + ctx.request_queue, + ctx.event_queue, + ctx.bids, + ctx.asks, + ctx.order_payer_token_account, + ctx.user_authority.clone(), + ctx.base_vault, + ctx.quote_vault, + ctx.token_program, + ctx.user_authority, + ]; + + let seeds = group_seeds!(group); + solana_program::program::invoke_signed_unchecked(&instruction, &account_infos, &[seeds])?; + + Ok(()) +} diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 6a0dd3510..c6a69b03d 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -839,7 +839,87 @@ impl<'keypair> ClientInstruction for Serum3PlaceOrderInstruction<'keypair> { market_vault_signer: vault_signer, owner: self.owner.pubkey(), token_program: Token::id(), - rent: sysvar::rent::Rent::id(), + }; + + let mut instruction = make_instruction(program_id, &accounts, instruction); + instruction.accounts.extend(health_check_metas.into_iter()); + + (accounts, instruction) + } + + fn signers(&self) -> Vec<&Keypair> { + vec![self.owner] + } +} + +pub struct Serum3SettleFundsInstruction<'keypair> { + pub account: Pubkey, + pub owner: &'keypair Keypair, + + pub serum_market: Pubkey, +} +#[async_trait::async_trait(?Send)] +impl<'keypair> ClientInstruction for Serum3SettleFundsInstruction<'keypair> { + type Accounts = mango_v4::accounts::Serum3SettleFunds; + type Instruction = mango_v4::instruction::Serum3SettleFunds; + async fn to_instruction( + &self, + account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let instruction = Self::Instruction {}; + + let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); + let serum_market: Serum3Market = account_loader.load(&self.serum_market).await.unwrap(); + let open_orders = account + .serum3_account_map + .find(serum_market.market_index) + .unwrap() + .open_orders; + let quote_info = + get_mint_info_by_token_index(&account_loader, &account, serum_market.quote_token_index) + .await; + let base_info = + get_mint_info_by_token_index(&account_loader, &account, serum_market.base_token_index) + .await; + + let market_external_bytes = account_loader + .load_bytes(&serum_market.serum_market_external) + .await + .unwrap(); + let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes( + &market_external_bytes[5..5 + std::mem::size_of::()], + ); + // unpack the data, to avoid unaligned references + let coin_vault = market_external.coin_vault; + let pc_vault = market_external.pc_vault; + let vault_signer = serum_dex::state::gen_vault_signer_key( + market_external.vault_signer_nonce, + &serum_market.serum_market_external, + &serum_market.serum_program, + ) + .unwrap(); + + let health_check_metas = + derive_health_check_remaining_account_metas(&account_loader, &account, None, false) + .await; + + let accounts = Self::Accounts { + group: account.group, + account: self.account, + open_orders, + quote_bank: quote_info.bank, + quote_vault: quote_info.vault, + base_bank: base_info.bank, + base_vault: base_info.vault, + serum_market: self.serum_market, + serum_program: serum_market.serum_program, + serum_market_external: serum_market.serum_market_external, + market_base_vault: from_serum_style_pubkey(&coin_vault), + market_quote_vault: from_serum_style_pubkey(&pc_vault), + market_vault_signer: vault_signer, + owner: self.owner.pubkey(), + token_program: Token::id(), }; let mut instruction = make_instruction(program_id, &accounts, instruction); diff --git a/programs/mango-v4/tests/test_serum.rs b/programs/mango-v4/tests/test_serum.rs index cb0891c11..49378f9bb 100644 --- a/programs/mango-v4/tests/test_serum.rs +++ b/programs/mango-v4/tests/test_serum.rs @@ -208,5 +208,17 @@ async fn test_serum() -> Result<(), TransportError> { assert_eq!(native0, 1000); assert_eq!(native1, 900); + // TODO: Currently has no effect + send_tx( + solana, + Serum3SettleFundsInstruction { + account, + owner, + serum_market, + }, + ) + .await + .unwrap(); + Ok(()) }