diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index 2616ff5a0..b56b43afa 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -316,7 +316,15 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( // Check health before balance adjustments let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?; - let _pre_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; + let pre_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; + msg!("pre_health {:?}", pre_health); + account + .fixed + .maybe_recover_from_being_liquidated(pre_health); + require!( + !account.fixed.being_liquidated(), + MangoError::BeingLiquidated + ); // Prices for logging let mut prices = vec![]; @@ -390,8 +398,11 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( // Check health after account position changes let post_health = compute_health_from_fixed_accounts(&account.borrow(), HealthType::Init, health_ais)?; - msg!("post_cpi_health {:?}", post_health); - require!(post_health >= 0, MangoError::HealthMustBePositive); + msg!("post_health {:?}", post_health); + require!( + post_health >= 0 || post_health > pre_health, + MangoError::HealthMustBePositive + ); account .fixed .maybe_recover_from_being_liquidated(post_health); 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 165f85ada..70b2979ca 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 @@ -5,7 +5,7 @@ use crate::error::*; use crate::instructions::apply_vault_difference; use crate::state::*; -use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog}; +use crate::logs::LoanOriginationFeeInstruction; #[derive(Accounts)] pub struct Serum3LiqForceCancelOrders<'info> { @@ -140,36 +140,21 @@ pub fn serum3_liq_force_cancel_orders( let mut account = ctx.accounts.account.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) = - apply_vault_difference( - &mut account.borrow_mut(), - &mut base_bank, - after_base_vault, - before_base_vault, - &mut quote_bank, - after_quote_vault, - before_quote_vault, - )?; - vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); - - if base_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.base_token_index, - loan_origination_fee: base_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders - }); - } - if quote_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.quote_token_index, - loan_origination_fee: quote_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders - }); - } + let difference_result = apply_vault_difference( + &mut account.borrow_mut(), + &mut base_bank, + after_base_vault, + before_base_vault, + &mut quote_bank, + after_quote_vault, + before_quote_vault, + )?; + difference_result.log_loan_origination_fees( + &ctx.accounts.group.key(), + &ctx.accounts.account.key(), + LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders, + ); + difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 37db488cb..7553209d8 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -20,6 +20,7 @@ pub struct OpenOrdersSlim { pub native_coin_total: u64, pub native_pc_free: u64, pub native_pc_total: u64, + pub referrer_rebates_accrued: u64, } impl OpenOrdersSlim { pub fn from_oo(oo: &OpenOrders) -> Self { @@ -28,30 +29,45 @@ impl OpenOrdersSlim { native_coin_total: oo.native_coin_total, native_pc_free: oo.native_pc_free, native_pc_total: oo.native_pc_total, + referrer_rebates_accrued: oo.referrer_rebates_accrued, } } } -pub trait OpenOrdersReserved { - fn native_coin_reserved(&self) -> u64; - fn native_pc_reserved(&self) -> u64; +pub trait OpenOrdersAmounts { + fn native_base_reserved(&self) -> u64; + fn native_quote_reserved(&self) -> u64; + fn native_base_free(&self) -> u64; + fn native_quote_free(&self) -> u64; // includes settleable referrer rebates } -impl OpenOrdersReserved for OpenOrdersSlim { - fn native_coin_reserved(&self) -> u64 { - self.native_coin_total - self.native_coin_free +impl OpenOrdersAmounts for OpenOrdersSlim { + fn native_base_reserved(&self) -> u64 { + cm!(self.native_coin_total - self.native_coin_free) } - fn native_pc_reserved(&self) -> u64 { - self.native_pc_total - self.native_pc_free + fn native_quote_reserved(&self) -> u64 { + cm!(self.native_pc_total - self.native_pc_free) + } + fn native_base_free(&self) -> u64 { + self.native_coin_free + } + fn native_quote_free(&self) -> u64 { + cm!(self.native_pc_free + self.referrer_rebates_accrued) } } -impl OpenOrdersReserved for OpenOrders { - fn native_coin_reserved(&self) -> u64 { - self.native_coin_total - self.native_coin_free +impl OpenOrdersAmounts for OpenOrders { + fn native_base_reserved(&self) -> u64 { + cm!(self.native_coin_total - self.native_coin_free) } - fn native_pc_reserved(&self) -> u64 { - self.native_pc_total - self.native_pc_free + fn native_quote_reserved(&self) -> u64 { + cm!(self.native_pc_total - self.native_pc_free) + } + fn native_base_free(&self) -> u64 { + self.native_coin_free + } + fn native_quote_free(&self) -> u64 { + cm!(self.native_pc_free + self.referrer_rebates_accrued) } } @@ -223,7 +239,28 @@ pub fn serum3_place_order( }); } - // TODO: pre-health check + // + // Pre-health computation + // + let mut account = ctx.accounts.account.load_mut()?; + let pre_health_opt = if !account.fixed.is_in_health_region() { + let retriever = + new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; + let health_cache = + new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?; + let pre_health = health_cache.health(HealthType::Init); + msg!("pre_health: {}", pre_health); + account + .fixed + .maybe_recover_from_being_liquidated(pre_health); + require!( + !account.fixed.being_liquidated(), + MangoError::BeingLiquidated + ); + Some((health_cache, pre_health)) + } else { + None + }; // // Apply the order to serum. Also immediately settle, in case the order @@ -248,22 +285,22 @@ pub fn serum3_place_order( let open_orders = load_open_orders_ref(oo_ai)?; OpenOrdersSlim::from_oo(&open_orders) }; - cpi_place_order(ctx.accounts, order)?; - { + cpi_place_order(ctx.accounts, order)?; + cpi_settle_funds(ctx.accounts)?; + + let oo_difference = { let oo_ai = &ctx.accounts.open_orders.as_ref(); let open_orders = load_open_orders_ref(oo_ai)?; let after_oo = OpenOrdersSlim::from_oo(&open_orders); - let mut account = ctx.accounts.account.load_mut()?; inc_maybe_loan( serum_market.market_index, &mut account.borrow_mut(), &before_oo, &after_oo, ); - } - - cpi_settle_funds(ctx.accounts)?; + OODifference::new(&before_oo, &after_oo) + }; // // After-order tracking @@ -274,8 +311,7 @@ pub fn serum3_place_order( 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 (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) = { + let vault_difference = { let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; @@ -293,35 +329,27 @@ pub fn serum3_place_order( // // Health check // - if !account.fixed.is_in_health_region() { - let retriever = - new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; - msg!("health: {}", health); - require!(health >= 0, MangoError::HealthMustBePositive); - account.fixed.maybe_recover_from_being_liquidated(health); + if let Some((mut health_cache, pre_health)) = pre_health_opt { + vault_difference.adjust_health_cache(&mut health_cache)?; + oo_difference.adjust_health_cache(&mut health_cache, &serum_market)?; + + let post_health = health_cache.health(HealthType::Init); + msg!("post_health: {}", post_health); + require!( + post_health >= 0 || post_health > pre_health, + MangoError::HealthMustBePositive + ); + account + .fixed + .maybe_recover_from_being_liquidated(post_health); } - vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); - - if base_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.base_token_index, - loan_origination_fee: base_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3PlaceOrder - }); - } - if quote_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.quote_token_index, - loan_origination_fee: quote_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3PlaceOrder - }); - } + vault_difference.log_loan_origination_fees( + &ctx.accounts.group.key(), + &ctx.accounts.account.key(), + LoanOriginationFeeInstruction::Serum3PlaceOrder, + ); + vault_difference.deactivate_inactive_token_accounts(&mut account.borrow_mut()); Ok(()) } @@ -335,25 +363,70 @@ pub fn inc_maybe_loan( ) { let serum3_account = account.serum3_orders_mut(market_index).unwrap(); - if after_oo.native_coin_reserved() > before_oo.native_coin_reserved() { + if after_oo.native_base_reserved() > before_oo.native_base_reserved() { let native_coin_reserved_increase = - after_oo.native_coin_reserved() - before_oo.native_coin_reserved(); + after_oo.native_base_reserved() - before_oo.native_base_reserved(); serum3_account.previous_native_coin_reserved = cm!(serum3_account.previous_native_coin_reserved + native_coin_reserved_increase); } - if after_oo.native_pc_reserved() > before_oo.native_pc_reserved() { - let reserved_pc_increase = after_oo.native_pc_reserved() - before_oo.native_pc_reserved(); + if after_oo.native_quote_reserved() > before_oo.native_quote_reserved() { + let reserved_pc_increase = + after_oo.native_quote_reserved() - before_oo.native_quote_reserved(); serum3_account.previous_native_pc_reserved = cm!(serum3_account.previous_native_pc_reserved + reserved_pc_increase); } } +pub struct OODifference { + reserved_base_change: I80F48, + reserved_quote_change: I80F48, + free_base_change: I80F48, + free_quote_change: I80F48, +} + +impl OODifference { + pub fn new(before_oo: &OpenOrdersSlim, after_oo: &OpenOrdersSlim) -> Self { + Self { + reserved_base_change: cm!(I80F48::from(after_oo.native_base_reserved()) + - I80F48::from(before_oo.native_base_reserved())), + reserved_quote_change: cm!(I80F48::from(after_oo.native_quote_reserved()) + - I80F48::from(before_oo.native_quote_reserved())), + free_base_change: cm!(I80F48::from(after_oo.native_base_free()) + - I80F48::from(before_oo.native_base_free())), + free_quote_change: cm!(I80F48::from(after_oo.native_quote_free()) + - I80F48::from(before_oo.native_quote_free())), + } + } + + pub fn adjust_health_cache( + &self, + health_cache: &mut HealthCache, + market: &Serum3Market, + ) -> Result<()> { + health_cache.adjust_serum3_reserved( + market.market_index, + market.base_token_index, + self.reserved_base_change, + self.free_base_change, + market.quote_token_index, + self.reserved_quote_change, + self.free_quote_change, + ) + } +} + pub struct VaultDifferenceResult { base_raw_index: usize, + base_index: TokenIndex, base_active: bool, quote_raw_index: usize, + quote_index: TokenIndex, quote_active: bool, + base_loan_origination_fee: I80F48, + quote_loan_origination_fee: I80F48, + base_native_change: I80F48, + quote_native_change: I80F48, } impl VaultDifferenceResult { @@ -365,6 +438,38 @@ impl VaultDifferenceResult { account.deactivate_token_position(self.quote_raw_index); } } + + pub fn log_loan_origination_fees( + &self, + group: &Pubkey, + account: &Pubkey, + instruction: LoanOriginationFeeInstruction, + ) { + if self.base_loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: *group, + mango_account: *account, + token_index: self.base_index, + loan_origination_fee: self.base_loan_origination_fee.to_bits(), + instruction, + }); + } + if self.quote_loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: *group, + mango_account: *account, + token_index: self.quote_index, + loan_origination_fee: self.quote_loan_origination_fee.to_bits(), + instruction, + }); + } + } + + 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)?; + Ok(()) + } } pub fn apply_vault_difference( @@ -375,31 +480,38 @@ pub fn apply_vault_difference( quote_bank: &mut Bank, after_quote_vault: u64, before_quote_vault: u64, -) -> Result<(VaultDifferenceResult, I80F48, I80F48)> { +) -> Result { // TODO: Applying the loan origination fee here may be too early: it should only be // charged if an order executes and the loan materializes? Otherwise MMs that place // an order without having the funds will be charged for each place_order! let (base_position, base_raw_index) = account.token_position_mut(base_bank.token_index)?; - let base_change = I80F48::from(after_base_vault) - I80F48::from(before_base_vault); + let base_native_before = base_position.native(&base_bank); + let base_needed_change = cm!(I80F48::from(after_base_vault) - I80F48::from(before_base_vault)); let (base_active, base_loan_origination_fee) = - base_bank.change_with_fee(base_position, base_change)?; + base_bank.change_with_fee(base_position, base_needed_change)?; + let base_native_after = base_position.native(&base_bank); let (quote_position, quote_raw_index) = account.token_position_mut(quote_bank.token_index)?; - let quote_change = I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault); + let quote_native_before = quote_position.native("e_bank); + let quote_needed_change = + cm!(I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault)); let (quote_active, quote_loan_origination_fee) = - quote_bank.change_with_fee(quote_position, quote_change)?; + quote_bank.change_with_fee(quote_position, quote_needed_change)?; + let quote_native_after = quote_position.native("e_bank); - Ok(( - VaultDifferenceResult { - base_raw_index, - base_active, - quote_raw_index, - quote_active, - }, + Ok(VaultDifferenceResult { + base_raw_index, + base_index: base_bank.token_index, + base_active, + quote_raw_index, + quote_index: quote_bank.token_index, + quote_active, base_loan_origination_fee, quote_loan_origination_fee, - )) + base_native_change: cm!(base_native_after - base_native_before), + quote_native_change: cm!(quote_native_after - quote_native_before), + }) } fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> { diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index 8b4d92d7a..2b4530fe0 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -9,8 +9,8 @@ use crate::error::*; use crate::serum3_cpi::load_open_orders_ref; use crate::state::*; -use super::{apply_vault_difference, OpenOrdersReserved, OpenOrdersSlim}; -use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog}; +use super::{apply_vault_difference, OpenOrdersAmounts, OpenOrdersSlim}; +use crate::logs::LoanOriginationFeeInstruction; #[derive(Accounts)] pub struct Serum3SettleFunds<'info> { @@ -150,36 +150,21 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { let mut account = ctx.accounts.account.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) = - apply_vault_difference( - &mut account.borrow_mut(), - &mut base_bank, - after_base_vault, - before_base_vault, - &mut quote_bank, - after_quote_vault, - before_quote_vault, - )?; - vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); - - if base_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.base_token_index, - loan_origination_fee: base_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3SettleFunds - }); - } - if quote_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.quote_token_index, - loan_origination_fee: quote_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3SettleFunds - }); - } + let difference_result = apply_vault_difference( + &mut account.borrow_mut(), + &mut base_bank, + after_base_vault, + before_base_vault, + &mut quote_bank, + after_quote_vault, + before_quote_vault, + )?; + difference_result.log_loan_origination_fees( + &ctx.accounts.group.key(), + &ctx.accounts.account.key(), + LoanOriginationFeeInstruction::Serum3SettleFunds, + ); + difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); } Ok(()) @@ -198,11 +183,11 @@ pub fn charge_maybe_fees( let maybe_actualized_coin_loan = I80F48::from_num::( serum3_account .previous_native_coin_reserved - .saturating_sub(after_oo.native_coin_reserved()), + .saturating_sub(after_oo.native_base_reserved()), ); if maybe_actualized_coin_loan > 0 { - serum3_account.previous_native_coin_reserved = after_oo.native_coin_reserved(); + serum3_account.previous_native_coin_reserved = after_oo.native_base_reserved(); // loan origination fees let coin_token_account = account.token_position_mut(coin_bank.token_index)?.0; @@ -223,11 +208,11 @@ pub fn charge_maybe_fees( let maybe_actualized_pc_loan = I80F48::from_num::( serum3_account .previous_native_pc_reserved - .saturating_sub(after_oo.native_pc_reserved()), + .saturating_sub(after_oo.native_quote_reserved()), ); if maybe_actualized_pc_loan > 0 { - serum3_account.previous_native_pc_reserved = after_oo.native_pc_reserved(); + serum3_account.previous_native_pc_reserved = after_oo.native_quote_reserved(); // loan origination fees let pc_token_account = account.token_position_mut(pc_bank.token_index)?.0; diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index cd9ede702..05a2d9e51 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -94,6 +94,9 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { // // Health computation // + // Since depositing can only increase health, we can skip the usual pre-health computation. + // Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated. + // if !account.fixed.is_in_health_region() { let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 522b6e74d..45ca975ba 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -10,7 +10,6 @@ use fixed::types::I80F48; use crate::logs::{ LoanOriginationFeeInstruction, TokenBalanceLog, WithdrawLoanOriginationFeeLog, WithdrawLog, }; -use crate::state::new_fixed_order_account_retriever; use crate::util::checked_math as cm; #[derive(Accounts)] @@ -61,91 +60,107 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo let group = ctx.accounts.group.load()?; let token_index = ctx.accounts.bank.load()?.token_index; - // Get the account's position for that token index + // Create the account's position for that token index let mut account = ctx.accounts.account.load_mut()?; + let (_, raw_token_index, _) = account.ensure_token_position(token_index)?; - let (position, raw_token_index, _active_token_index) = - account.ensure_token_position(token_index)?; - - // The bank will also be passed in remainingAccounts. Use an explicit scope - // to drop the &mut before we borrow it immutably again later. - let (position_is_active, amount_i80f48, loan_origination_fee) = { - let mut bank = ctx.accounts.bank.load_mut()?; - let native_position = position.native(&bank); - - // Handle amount special case for withdrawing everything - let amount = if amount == u64::MAX && !allow_borrow { - if native_position.is_positive() { - // TODO: This rounding may mean that if we deposit and immediately withdraw - // we can't withdraw the full amount! - native_position.floor().to_num::() - } else { - return Ok(()); - } - } else { - amount - }; - + // Health check _after_ the token position is guaranteed to exist + let pre_health_opt = if !account.fixed.is_in_health_region() { + let retriever = + new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; + let health_cache = + new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?; + let pre_health = health_cache.health(HealthType::Init); + msg!("pre_health: {}", pre_health); + account + .fixed + .maybe_recover_from_being_liquidated(pre_health); require!( - allow_borrow || amount <= native_position, - MangoError::SomeError + !account.fixed.being_liquidated(), + MangoError::BeingLiquidated ); - - let amount_i80f48 = I80F48::from(amount); - - // Update the bank and position - let (position_is_active, loan_origination_fee) = - bank.withdraw_with_fee(position, amount_i80f48)?; - - // Provide a readable error message in case the vault doesn't have enough tokens - if ctx.accounts.vault.amount < amount { - return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { - format!( - "bank vault does not have enough tokens, need {} but have {}", - amount, ctx.accounts.vault.amount - ) - }); - } - - // Transfer the actual tokens - let group_seeds = group_seeds!(group); - token::transfer( - ctx.accounts.transfer_ctx().with_signer(&[group_seeds]), - amount, - )?; - - (position_is_active, amount_i80f48, loan_origination_fee) + Some((health_cache, pre_health)) + } else { + None }; - let indexed_position = position.indexed_position; - let bank = ctx.accounts.bank.load()?; - let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?; + let mut bank = ctx.accounts.bank.load_mut()?; + let position = account.token_position_mut_by_raw_index(raw_token_index); + let native_position = position.native(&bank); - // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) - let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::(); - account.fixed.net_deposits = cm!(account.fixed.net_deposits - amount_usd); + // Handle amount special case for withdrawing everything + let amount = if amount == u64::MAX && !allow_borrow { + if native_position.is_positive() { + // TODO: This rounding may mean that if we deposit and immediately withdraw + // we can't withdraw the full amount! + native_position.floor().to_num::() + } else { + return Ok(()); + } + } else { + amount + }; + + require!( + allow_borrow || amount <= native_position, + MangoError::SomeError + ); + + let amount_i80f48 = I80F48::from(amount); + + // Update the bank and position + let (position_is_active, loan_origination_fee) = + bank.withdraw_with_fee(position, amount_i80f48)?; + + // Provide a readable error message in case the vault doesn't have enough tokens + if ctx.accounts.vault.amount < amount { + return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { + format!( + "bank vault does not have enough tokens, need {} but have {}", + amount, ctx.accounts.vault.amount + ) + }); + } + + // Transfer the actual tokens + let group_seeds = group_seeds!(group); + token::transfer( + ctx.accounts.transfer_ctx().with_signer(&[group_seeds]), + amount, + )?; + + let native_position_after = position.native(&bank); + let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?; emit!(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), token_index, - indexed_position: indexed_position.to_bits(), + indexed_position: position.indexed_position.to_bits(), deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), price: oracle_price.to_bits(), }); + // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) + let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::(); + account.fixed.net_deposits = cm!(account.fixed.net_deposits - amount_usd); + // // Health check // - if !account.fixed.is_in_health_region() { - let retriever = - new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health = compute_health(&account.borrow(), HealthType::Init, &retriever) - .context("post-withdraw init health")?; - msg!("health: {}", health); - require!(health >= 0, MangoError::HealthMustBePositive); - account.fixed.maybe_recover_from_being_liquidated(health); + if let Some((mut health_cache, pre_health)) = pre_health_opt { + health_cache + .adjust_token_balance(token_index, cm!(native_position_after - native_position))?; + let post_health = health_cache.health(HealthType::Init); + msg!("post_health: {}", post_health); + require!( + post_health >= 0 || post_health > pre_health, + MangoError::HealthMustBePositive + ); + account + .fixed + .maybe_recover_from_being_liquidated(post_health); } // diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 8afa24c6c..e775ef0ec 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use crate::accounts_zerocopy::*; use crate::error::*; use crate::serum3_cpi; -use crate::state::{Bank, PerpMarket, PerpMarketIndex, TokenIndex}; +use crate::state::{Bank, PerpMarket, PerpMarketIndex, Serum3MarketIndex, TokenIndex}; use crate::util::checked_math as cm; use super::MangoAccountRef; @@ -427,6 +427,7 @@ pub struct Serum3Info { reserved: I80F48, base_index: usize, quote_index: usize, + market_index: Serum3MarketIndex, } impl Serum3Info { @@ -526,16 +527,70 @@ impl HealthCache { health } + fn token_entry_index(&mut self, token_index: TokenIndex) -> Result { + self.token_infos + .iter() + .position(|t| t.token_index == token_index) + .ok_or_else(|| error_msg!("token index {} not found", token_index)) + } + pub fn adjust_token_balance(&mut self, token_index: TokenIndex, change: I80F48) -> Result<()> { - let mut entry = self - .token_infos - .iter_mut() - .find(|t| t.token_index == token_index) - .ok_or_else(|| error_msg!("token index {} not found", token_index))?; + let entry_index = self.token_entry_index(token_index)?; + let mut entry = &mut self.token_infos[entry_index]; entry.balance = cm!(entry.balance + change * entry.oracle_price); Ok(()) } + pub fn adjust_serum3_reserved( + &mut self, + market_index: Serum3MarketIndex, + base_token_index: TokenIndex, + reserved_base_change: I80F48, + free_base_change: I80F48, + quote_token_index: TokenIndex, + reserved_quote_change: I80F48, + free_quote_change: I80F48, + ) -> Result<()> { + let base_entry_index = self.token_entry_index(base_token_index)?; + let quote_entry_index = self.token_entry_index(quote_token_index)?; + + // Compute the total reserved amount change in health reference units + let mut reserved_amount; + { + let base_entry = &mut self.token_infos[base_entry_index]; + reserved_amount = cm!(reserved_base_change * base_entry.oracle_price); + } + { + let quote_entry = &mut self.token_infos[quote_entry_index]; + reserved_amount = + cm!(reserved_amount + reserved_quote_change * quote_entry.oracle_price); + } + + // Apply it to the tokens + { + let base_entry = &mut self.token_infos[base_entry_index]; + base_entry.serum3_max_reserved = cm!(base_entry.serum3_max_reserved + reserved_amount); + base_entry.balance = + cm!(base_entry.balance + free_base_change * base_entry.oracle_price); + } + { + let quote_entry = &mut self.token_infos[quote_entry_index]; + quote_entry.serum3_max_reserved = + cm!(quote_entry.serum3_max_reserved + reserved_amount); + quote_entry.balance = + cm!(quote_entry.balance + free_quote_change * quote_entry.oracle_price); + } + + // Apply it to the serum3 info + let market_entry = self + .serum3_infos + .iter_mut() + .find(|m| m.market_index == market_index) + .ok_or_else(|| error_msg!("serum3 market {} not found", market_index))?; + market_entry.reserved = cm!(market_entry.reserved + reserved_amount); + Ok(()) + } + pub fn has_liquidatable_assets(&self) -> bool { let spot_liquidatable = self .token_infos @@ -832,6 +887,7 @@ pub fn new_health_cache( reserved: reserved_balance, base_index, quote_index, + market_index: serum_account.market_index, }); } diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index eb69e5a55..379e9f1b7 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -319,6 +319,28 @@ pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank: native.to_num::() } +// Verifies that the "post_health: ..." log emitted by the previous instruction +// matches the init health of the account. +pub async fn check_prev_instruction_post_health(solana: &SolanaCookie, account: Pubkey) { + let logs = solana.program_log(); + let post_health_str = logs + .iter() + .find_map(|line| line.strip_prefix("post_health: ")) + .unwrap(); + let post_health = post_health_str.parse::().unwrap(); + + solana.advance_by_slots(1).await; // ugly, just to avoid sending the same tx next + send_tx(solana, ComputeAccountDataInstruction { account }) + .await + .unwrap(); + + let health_data = solana + .program_log_events::() + .pop() + .unwrap(); + assert_eq!(health_data.init_health.to_num::(), post_health); +} + // // a struct for each instruction along with its // ClientInstruction impl @@ -2501,7 +2523,6 @@ impl ClientInstruction for TokenUpdateIndexAndRateInstruction { pub struct ComputeAccountDataInstruction { pub account: Pubkey, - pub health_type: HealthType, } #[async_trait::async_trait(?Send)] impl ClientInstruction for ComputeAccountDataInstruction { diff --git a/programs/mango-v4/tests/program_test/mod.rs b/programs/mango-v4/tests/program_test/mod.rs index 726a8e7d8..50f3a2cf1 100644 --- a/programs/mango-v4/tests/program_test/mod.rs +++ b/programs/mango-v4/tests/program_test/mod.rs @@ -68,6 +68,8 @@ impl Log for LoggerWrapper { let msg = record.args().to_string(); if let Some(data) = msg.strip_prefix("Program log: ") { self.program_log.write().unwrap().push(data.into()); + } else if let Some(data) = msg.strip_prefix("Program data: ") { + self.program_log.write().unwrap().push(data.into()); } } self.inner.log(record); @@ -112,7 +114,7 @@ impl TestContextBuilder { let mut test = ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry)); // intentionally set to as tight as possible, to catch potential problems early - test.set_compute_max_units(87000); + test.set_compute_max_units(75000); Self { test, diff --git a/programs/mango-v4/tests/program_test/solana.rs b/programs/mango-v4/tests/program_test/solana.rs index 293909683..170cf102c 100644 --- a/programs/mango-v4/tests/program_test/solana.rs +++ b/programs/mango-v4/tests/program_test/solana.rs @@ -223,4 +223,19 @@ impl SolanaCookie { pub fn program_log(&self) -> Vec { self.program_log.read().unwrap().clone() } + + pub fn program_log_events( + &self, + ) -> Vec { + self.program_log() + .iter() + .filter_map(|data| { + let bytes = base64::decode(data).ok()?; + if bytes[0..8] != T::discriminator() { + return None; + } + T::try_from_slice(&bytes[8..]).ok() + }) + .collect() + } } diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index 6a0673d91..7cefa5771 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -111,15 +111,14 @@ async fn test_basic() -> Result<(), TransportError> { // // TEST: Compute the account health // - send_tx( - solana, - ComputeAccountDataInstruction { - account, - health_type: HealthType::Init, - }, - ) - .await - .unwrap(); + send_tx(solana, ComputeAccountDataInstruction { account }) + .await + .unwrap(); + let health_data = solana + .program_log_events::() + .pop() + .unwrap(); + assert_eq!(health_data.init_health.to_num::(), 60); // // TEST: Withdraw funds @@ -143,6 +142,8 @@ async fn test_basic() -> Result<(), TransportError> { .await .unwrap(); + check_prev_instruction_post_health(&solana, account).await; + assert_eq!(solana.token_account_balance(vault).await, withdraw_amount); assert_eq!( solana.token_account_balance(payer_mint0_account).await, diff --git a/programs/mango-v4/tests/test_liq_tokens.rs b/programs/mango-v4/tests/test_liq_tokens.rs index d89a2b59c..84b9e69e3 100644 --- a/programs/mango-v4/tests/test_liq_tokens.rs +++ b/programs/mango-v4/tests/test_liq_tokens.rs @@ -11,7 +11,9 @@ mod program_test; #[tokio::test] async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> { - let context = TestContext::new().await; + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k + let context = test_builder.start_default().await; let solana = &context.solana.clone(); let admin = &Keypair::new(); diff --git a/programs/mango-v4/tests/test_serum.rs b/programs/mango-v4/tests/test_serum.rs index b7d9b63f9..3dd39494d 100644 --- a/programs/mango-v4/tests/test_serum.rs +++ b/programs/mango-v4/tests/test_serum.rs @@ -10,7 +10,9 @@ mod program_test; #[tokio::test] async fn test_serum() -> Result<(), TransportError> { - let context = TestContext::new().await; + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k + let context = test_builder.start_default().await; let solana = &context.solana.clone(); let admin = &Keypair::new(); @@ -158,6 +160,8 @@ async fn test_serum() -> Result<(), TransportError> { .await .unwrap(); + check_prev_instruction_post_health(&solana, account).await; + let native0 = account_position(solana, account, base_token.bank).await; let native1 = account_position(solana, account, quote_token.bank).await; assert_eq!(native0, 1000);