Merge pull request #190 from blockworks-foundation/ckamm/pre-health

Compute pre-health, to allow some actions even if init_health<0
This commit is contained in:
Christian Kamm 2022-08-24 15:07:56 +02:00 committed by GitHub
commit 4bad1b9b1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 433 additions and 221 deletions

View File

@ -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);

View File

@ -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(())
}

View File

@ -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<VaultDifferenceResult> {
// 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(&quote_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(&quote_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<()> {

View File

@ -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<Serum3SettleFunds>) -> 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::<u64>(
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::<u64>(
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;

View File

@ -94,6 +94,9 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, 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())?;

View File

@ -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<TokenWithdraw>, 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::<u64>()
} 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::<i64>();
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::<u64>()
} 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::<i64>();
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);
}
//

View File

@ -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<usize> {
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,
});
}

View File

@ -319,6 +319,28 @@ pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank:
native.to_num::<f64>()
}
// 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::<f64>().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::<mango_v4::events::MangoAccountData>()
.pop()
.unwrap();
assert_eq!(health_data.init_health.to_num::<f64>(), 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 {

View File

@ -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,

View File

@ -223,4 +223,19 @@ impl SolanaCookie {
pub fn program_log(&self) -> Vec<String> {
self.program_log.read().unwrap().clone()
}
pub fn program_log_events<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
&self,
) -> Vec<T> {
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()
}
}

View File

@ -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::<mango_v4::events::MangoAccountData>()
.pop()
.unwrap();
assert_eq!(health_data.init_health.to_num::<i64>(), 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,

View File

@ -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();

View File

@ -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);