diff --git a/client/src/client.rs b/client/src/client.rs index 61ba70ba4..b79965317 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -605,6 +605,10 @@ impl MangoClient { let rates = get_fee_rates(fee_tier); (s3.market.pc_lot_size as f64 * (1f64 + rates.0)) as u64 * (limit_price * max_base_qty) }; + let payer_mint_info = match side { + Serum3Side::Bid => s3.quote.mint_info, + Serum3Side::Ask => s3.base.mint_info, + }; self.program() .request() @@ -616,10 +620,8 @@ impl MangoClient { group: self.group(), account: self.mango_account_address, open_orders, - quote_bank: s3.quote.mint_info.first_bank(), - quote_vault: s3.quote.mint_info.first_vault(), - base_bank: s3.base.mint_info.first_bank(), - base_vault: s3.base.mint_info.first_vault(), + payer_bank: payer_mint_info.first_bank(), + payer_vault: payer_mint_info.first_vault(), serum_market: s3.market.address, serum_program: s3.market.market.serum_program, serum_market_external: s3.market.market.serum_market_external, diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index d4ba2cd18..94b1165c6 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -169,6 +169,10 @@ pub fn serum3_liq_force_cancel_orders( &mut base_bank, after_base_vault, before_base_vault, + )?; + apply_vault_difference( + &mut account.borrow_mut(), + serum_market.market_index, &mut quote_bank, after_quote_vault, before_quote_vault, diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 7a282c2d1..4a33b8c56 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -9,7 +9,6 @@ use fixed::types::I80F48; use num_enum::IntoPrimitive; use num_enum::TryFromPrimitive; use serum_dex::instruction::NewOrderInstructionV3; -use serum_dex::matching::Side; use serum_dex::state::OpenOrders; /// For loan origination fees bookkeeping purposes @@ -169,19 +168,13 @@ pub struct Serum3PlaceOrder<'info> { /// CHECK: Validated by the serum cpi call pub market_vault_signer: UncheckedAccount<'info>, - // TODO: do we need to pass both, or just payer? - // 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) - // token_index and bank.vault == vault is validated inline at #3 + /// The bank that pays for the order, if necessary + // token_index and payer_bank.vault == payer_vault is validated inline at #3 #[account(mut, has_one = group)] - pub quote_bank: AccountLoader<'info, Bank>, + pub payer_bank: AccountLoader<'info, Bank>, + /// The bank vault that pays for the order, if necessary #[account(mut)] - pub quote_vault: Box>, - #[account(mut, has_one = group)] - pub base_bank: AccountLoader<'info, Bank>, - #[account(mut)] - pub base_vault: Box>, + pub payer_vault: Box>, pub token_program: Program<'info, Token>, } @@ -221,25 +214,14 @@ pub fn serum3_place_order( MangoError::SomeError ); - // Validate banks and vaults #3 - 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 - ); + // Validate bank and vault #3 + let payer_bank = ctx.accounts.payer_bank.load()?; + require_keys_eq!(payer_bank.vault, ctx.accounts.payer_vault.key()); + let payer_token_index = match side { + Serum3Side::Bid => serum_market.quote_token_index, + Serum3Side::Ask => serum_market.base_token_index, + }; + require_eq!(payer_bank.token_index, payer_token_index); } // @@ -261,8 +243,7 @@ pub fn serum3_place_order( // Before-order tracking // - let before_base_vault = ctx.accounts.base_vault.amount; - let before_quote_vault = ctx.accounts.quote_vault.amount; + let before_vault = ctx.accounts.payer_vault.amount; let before_oo = { let oo_ai = &ctx.accounts.open_orders.as_ref(); @@ -271,21 +252,17 @@ pub fn serum3_place_order( }; // Provide a readable error message in case the vault doesn't have enough tokens - let (vault_amount, needed_amount) = match side { - Serum3Side::Ask => ( - before_base_vault, - max_base_qty.saturating_sub(before_oo.native_base_free()), - ), - Serum3Side::Bid => ( - before_quote_vault, - max_native_quote_qty_including_fees.saturating_sub(before_oo.native_quote_free()), - ), + let needed_amount = match side { + Serum3Side::Ask => max_base_qty.saturating_sub(before_oo.native_base_free()), + Serum3Side::Bid => { + max_native_quote_qty_including_fees.saturating_sub(before_oo.native_quote_free()) + } }; - if vault_amount < needed_amount { + if before_vault < needed_amount { return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { format!( "bank vault does not have enough tokens, need {} but have {}", - needed_amount, vault_amount + needed_amount, before_vault ) }); } @@ -318,29 +295,21 @@ pub fn serum3_place_order( // // 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; + ctx.accounts.payer_vault.reload()?; + let after_vault = ctx.accounts.payer_vault.amount; - // Placing an order cannot increase vault balances - require_gte!(before_base_vault, after_base_vault); - require_gte!(before_quote_vault, after_quote_vault); + // Placing an order cannot increase vault balance + require_gte!(before_vault, after_vault); - // Charge the difference in vault balances to the user's account + // Charge the difference in vault balance to the user's account let vault_difference = { - let mut base_bank = ctx.accounts.base_bank.load_mut()?; - let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - + let mut payer_bank = ctx.accounts.payer_bank.load_mut()?; apply_vault_difference( &mut account.borrow_mut(), serum_market.market_index, - &mut base_bank, - after_base_vault, - before_base_vault, - &mut quote_bank, - after_quote_vault, - before_quote_vault, + &mut payer_bank, + after_vault, + before_vault, )? }; @@ -395,16 +364,13 @@ impl OODifference { } pub struct VaultDifference { - base_index: TokenIndex, - quote_index: TokenIndex, - base_native_change: I80F48, - quote_native_change: I80F48, + token_index: TokenIndex, + native_change: I80F48, } impl VaultDifference { pub fn adjust_health_cache(&self, health_cache: &mut HealthCache) -> Result<()> { - health_cache.adjust_token_balance(self.base_index, self.base_native_change)?; - health_cache.adjust_token_balance(self.quote_index, self.quote_native_change)?; + health_cache.adjust_token_balance(self.token_index, self.native_change)?; Ok(()) } } @@ -414,74 +380,52 @@ impl VaultDifference { pub fn apply_vault_difference( account: &mut MangoAccountRefMut, serum_market_index: Serum3MarketIndex, - base_bank: &mut Bank, - after_base_vault: u64, - before_base_vault: u64, - quote_bank: &mut Bank, - after_quote_vault: u64, - before_quote_vault: u64, + bank: &mut Bank, + vault_after: u64, + vault_before: u64, ) -> Result { - let base_needed_change = cm!(I80F48::from(after_base_vault) - I80F48::from(before_base_vault)); + let needed_change = cm!(I80F48::from(vault_after) - I80F48::from(vault_before)); - let (base_position, _) = account.token_position_mut(base_bank.token_index)?; - let base_native_before = base_position.native(&base_bank); - base_bank.change_without_fee(base_position, base_needed_change)?; - let base_native_after = base_position.native(&base_bank); - let base_native_change = cm!(base_native_after - base_native_before); - let base_borrows = base_native_change - .max(base_native_after) - .min(I80F48::ZERO) - .abs() - .to_num::(); - - let quote_needed_change = - cm!(I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault)); - - let (quote_position, _) = account.token_position_mut(quote_bank.token_index)?; - let quote_native_before = quote_position.native("e_bank); - quote_bank.change_without_fee(quote_position, quote_needed_change)?; - let quote_native_after = quote_position.native("e_bank); - let quote_native_change = cm!(quote_native_after - quote_native_before); - let quote_borrows = quote_native_change - .max(quote_native_after) + let (position, _) = account.token_position_mut(bank.token_index)?; + let native_before = position.native(&bank); + bank.change_without_fee(position, needed_change)?; + let native_after = position.native(&bank); + let native_change = cm!(native_after - native_before); + let new_borrows = native_change + .max(native_after) .min(I80F48::ZERO) .abs() .to_num::(); let market = account.serum3_orders_mut(serum_market_index).unwrap(); + let borrows_without_fee = if bank.token_index == market.base_token_index { + &mut market.base_borrows_without_fee + } else if bank.token_index == market.quote_token_index { + &mut market.quote_borrows_without_fee + } else { + return Err(error_msg!( + "assert failed: apply_vault_difference called with bad token index" + )); + }; - // Only for place: Add to potential borrow amounts - market.base_borrows_without_fee = cm!(market.base_borrows_without_fee + base_borrows); - market.quote_borrows_without_fee = cm!(market.quote_borrows_without_fee + quote_borrows); + // Only for place: Add to potential borrow amount + let old_value = *borrows_without_fee; + *borrows_without_fee = cm!(old_value + new_borrows); // Only for settle/liq_force_cancel: Reduce the potential borrow amounts - if base_needed_change > 0 { - market.base_borrows_without_fee = market - .base_borrows_without_fee - .saturating_sub(base_needed_change.to_num::()); - } - if quote_needed_change > 0 { - market.quote_borrows_without_fee = market - .quote_borrows_without_fee - .saturating_sub(quote_needed_change.to_num::()); + if needed_change > 0 { + *borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::()); } Ok(VaultDifference { - base_index: base_bank.token_index, - quote_index: quote_bank.token_index, - base_native_change, - quote_native_change, + token_index: bank.token_index, + native_change, }) } fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> { use crate::serum3_cpi; - let order_payer_token_account = match order.side { - Side::Bid => &ctx.quote_vault, - Side::Ask => &ctx.base_vault, - }; - let group = ctx.group.load()?; serum3_cpi::PlaceOrder { program: ctx.serum_program.to_account_info(), @@ -495,7 +439,7 @@ fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Resu token_program: ctx.token_program.to_account_info(), open_orders: ctx.open_orders.to_account_info(), - order_payer_token_account: order_payer_token_account.to_account_info(), + order_payer_token_account: ctx.payer_vault.to_account_info(), user_authority: ctx.group.to_account_info(), } .call(&group, order) diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index e50d43276..5de906917 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -164,6 +164,10 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { &mut base_bank, after_base_vault, before_base_vault, + )?; + apply_vault_difference( + &mut account.borrow_mut(), + serum_market.market_index, &mut quote_bank, after_quote_vault, before_quote_vault, diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 2ae47747e..b38a14342 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -1600,14 +1600,17 @@ impl<'keypair> ClientInstruction for Serum3PlaceOrderInstruction<'keypair> { ) .await; + let (payer_bank, payer_vault) = match self.side { + Serum3Side::Bid => (quote_info.first_bank(), quote_info.first_vault()), + Serum3Side::Ask => (base_info.first_bank(), base_info.first_vault()), + }; + let accounts = Self::Accounts { group: account.fixed.group, account: self.account, open_orders, - quote_bank: quote_info.first_bank(), - quote_vault: quote_info.first_vault(), - base_bank: base_info.first_bank(), - base_vault: base_info.first_vault(), + payer_bank, + payer_vault, serum_market: self.serum_market, serum_program: serum_market.serum_program, serum_market_external: serum_market.serum_market_external, diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 30bc972d4..fefafda0e 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1079,6 +1079,13 @@ export class MangoClient { .baseSizeNumberToLots(size) .mul(serum3MarketExternal.priceNumberToLots(price)), ); + const payerTokenIndex = (() => { + if (side == Serum3Side.bid) { + return serum3Market.quoteTokenIndex; + } else { + return serum3Market.baseTokenIndex; + } + })(); return await this.program.methods .serum3PlaceOrder( @@ -1107,14 +1114,8 @@ export class MangoClient { marketBaseVault: serum3MarketExternal.decoded.baseVault, marketQuoteVault: serum3MarketExternal.decoded.quoteVault, marketVaultSigner: serum3MarketExternalVaultSigner, - quoteBank: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex) - .publicKey, - quoteVault: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex) - .vault, - baseBank: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) - .publicKey, - baseVault: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) - .vault, + payerBank: group.getFirstBankByTokenIndex(payerTokenIndex).publicKey, + payerVault: group.getFirstBankByTokenIndex(payerTokenIndex).vault, }) .remainingAccounts( healthRemainingAccounts.map( diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index e11622b3d..4b6a7c6c4 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -1605,24 +1605,20 @@ export type MangoV4 = { ] }, { - "name": "quoteBank", + "name": "payerBank", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The bank that pays for the order, if necessary" + ] }, { - "name": "quoteVault", + "name": "payerVault", "isMut": true, - "isSigner": false - }, - { - "name": "baseBank", - "isMut": true, - "isSigner": false - }, - { - "name": "baseVault", - "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The bank vault that pays for the order, if necessary" + ] }, { "name": "tokenProgram", @@ -4015,11 +4011,17 @@ export type MangoV4 = { "type": "publicKey" }, { - "name": "previousNativeCoinReserved", + "name": "baseBorrowsWithoutFee", + "docs": [ + "Tracks the amount of borrows that have flowed into the serum open orders account.", + "These borrows did not have the loan origination fee applied, and that may happen", + "later (in serum3_settle_funds) if we can guarantee that the funds were used.", + "In particular a place-on-book, cancel, settle should not cost fees." + ], "type": "u64" }, { - "name": "previousNativePcReserved", + "name": "quoteBorrowsWithoutFee", "type": "u64" }, { @@ -6887,24 +6889,20 @@ export const IDL: MangoV4 = { ] }, { - "name": "quoteBank", + "name": "payerBank", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The bank that pays for the order, if necessary" + ] }, { - "name": "quoteVault", + "name": "payerVault", "isMut": true, - "isSigner": false - }, - { - "name": "baseBank", - "isMut": true, - "isSigner": false - }, - { - "name": "baseVault", - "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The bank vault that pays for the order, if necessary" + ] }, { "name": "tokenProgram", @@ -9297,11 +9295,17 @@ export const IDL: MangoV4 = { "type": "publicKey" }, { - "name": "previousNativeCoinReserved", + "name": "baseBorrowsWithoutFee", + "docs": [ + "Tracks the amount of borrows that have flowed into the serum open orders account.", + "These borrows did not have the loan origination fee applied, and that may happen", + "later (in serum3_settle_funds) if we can guarantee that the funds were used.", + "In particular a place-on-book, cancel, settle should not cost fees." + ], "type": "u64" }, { - "name": "previousNativePcReserved", + "name": "quoteBorrowsWithoutFee", "type": "u64" }, {