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:
commit
4bad1b9b1a
|
@ -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);
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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("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<()> {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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())?;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue