Perp: liq with token instruction (#368)
The new instruction allows the liqor to take over negative pnl (limited by liqee settle health and settle limits) before applying the bankruptcy logic.
This commit is contained in:
parent
c5d875e04d
commit
93d33edb74
|
@ -879,7 +879,7 @@ impl MangoClient {
|
|||
self.send_and_confirm_owner_tx(vec![ix]).await
|
||||
}
|
||||
|
||||
pub async fn perp_liq_bankruptcy(
|
||||
pub async fn perp_liq_quote_and_bankruptcy(
|
||||
&self,
|
||||
liqee: (&Pubkey, &MangoAccountValue),
|
||||
market_index: PerpMarketIndex,
|
||||
|
@ -903,9 +903,10 @@ impl MangoClient {
|
|||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::PerpLiqBankruptcy {
|
||||
&mango_v4::accounts::PerpLiqQuoteAndBankruptcy {
|
||||
group: self.group(),
|
||||
perp_market: perp.address,
|
||||
oracle: perp.market.oracle,
|
||||
liqor: self.mango_account_address,
|
||||
liqor_owner: self.owner(),
|
||||
liqee: *liqee.0,
|
||||
|
@ -920,9 +921,9 @@ impl MangoClient {
|
|||
ams.extend(health_remaining_ams.into_iter());
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpLiqBankruptcy {
|
||||
max_liab_transfer,
|
||||
}),
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::PerpLiqQuoteAndBankruptcy { max_liab_transfer },
|
||||
),
|
||||
};
|
||||
self.send_and_confirm_owner_tx(vec![ix]).await
|
||||
}
|
||||
|
|
|
@ -247,6 +247,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
/*
|
||||
async fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
|
||||
let perp_settle_health = self.health_cache.perp_settle_health();
|
||||
let mut perp_settleable_pnl = self
|
||||
|
@ -257,6 +258,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
return None;
|
||||
}
|
||||
let pnl = pp.quote_position_native();
|
||||
// TODO: outdated: must account for perp settle limit
|
||||
let settleable_pnl = if pnl > 0 {
|
||||
pnl
|
||||
} else if pnl < 0 && perp_settle_health > 0 {
|
||||
|
@ -320,12 +322,13 @@ impl<'a> LiquidateHelper<'a> {
|
|||
}
|
||||
return Ok(None);
|
||||
}
|
||||
*/
|
||||
|
||||
async fn perp_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
||||
if self.health_cache.has_liquidatable_assets() {
|
||||
async fn perp_liq_quote_and_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
||||
if !self.health_cache.in_phase3_liquidation() {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut perp_bankruptcies = self
|
||||
let mut perp_negative_pnl = self
|
||||
.liqee
|
||||
.active_perp_positions()
|
||||
.filter_map(|pp| {
|
||||
|
@ -336,24 +339,24 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Some((pp.market_index, quote))
|
||||
})
|
||||
.collect::<Vec<(PerpMarketIndex, I80F48)>>();
|
||||
perp_bankruptcies.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
perp_negative_pnl.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
|
||||
if perp_bankruptcies.is_empty() {
|
||||
if perp_negative_pnl.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let (perp_market_index, _) = perp_bankruptcies.first().unwrap();
|
||||
let (perp_market_index, _) = perp_negative_pnl.first().unwrap();
|
||||
|
||||
let sig = self
|
||||
.client
|
||||
.perp_liq_bankruptcy(
|
||||
.perp_liq_quote_and_bankruptcy(
|
||||
(self.pubkey, &self.liqee),
|
||||
*perp_market_index,
|
||||
// Always use the max amount, since the health effect is always positive
|
||||
// Always use the max amount, since the health effect is >= 0
|
||||
u64::MAX,
|
||||
)
|
||||
.await?;
|
||||
log::info!(
|
||||
"Liquidated bankruptcy for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
"Liquidated negative perp pnl on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
self.pubkey,
|
||||
perp_market_index,
|
||||
self.maint_health,
|
||||
|
@ -564,7 +567,10 @@ impl<'a> LiquidateHelper<'a> {
|
|||
// return Ok(txsig);
|
||||
// }
|
||||
|
||||
// Try to close orders before touching the user's positions
|
||||
//
|
||||
// Phase 1: Try to close orders before touching the user's positions
|
||||
//
|
||||
// TODO: All these close ix could be in one transaction.
|
||||
if let Some(txsig) = self.perp_close_orders().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
@ -572,6 +578,10 @@ impl<'a> LiquidateHelper<'a> {
|
|||
return Ok(txsig);
|
||||
}
|
||||
|
||||
//
|
||||
// Phase 2: token, perp base, TODO: perp positive trusted pnl
|
||||
//
|
||||
|
||||
if let Some(txsig) = self.perp_liq_base_position().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
@ -581,16 +591,20 @@ impl<'a> LiquidateHelper<'a> {
|
|||
// It's possible that some positive pnl can't be settled (if there's
|
||||
// no liquid counterparty) and that some negative pnl can't be settled
|
||||
// (if the liqee isn't liquid enough).
|
||||
if let Some(txsig) = self.perp_settle_pnl().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
// if let Some(txsig) = self.perp_settle_pnl().await? {
|
||||
// return Ok(txsig);
|
||||
// }
|
||||
|
||||
if let Some(txsig) = self.token_liq().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
// Socialize/insurance fund unsettleable negative pnl
|
||||
if let Some(txsig) = self.perp_liq_bankruptcy().await? {
|
||||
//
|
||||
// Phase 3: perp and token bankruptcy
|
||||
//
|
||||
|
||||
// Negative pnl: take over (paid by liqee or insurance) or socialize the loss
|
||||
if let Some(txsig) = self.perp_liq_quote_and_bankruptcy().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
|
|
|
@ -69,6 +69,16 @@ pub enum MangoError {
|
|||
TokenInReduceOnlyMode,
|
||||
#[msg("market is in reduce only mode")]
|
||||
MarketInReduceOnlyMode,
|
||||
#[msg("the perp position has non-zero base lots")]
|
||||
PerpHasBaseLots,
|
||||
#[msg("there are open or unsettled serum3 orders")]
|
||||
HasOpenOrUnsettledSerum3Orders,
|
||||
#[msg("has liquidatable token position")]
|
||||
HasLiquidatableTokenPosition,
|
||||
#[msg("has liquidatable perp base position")]
|
||||
HasLiquidatablePerpBasePosition,
|
||||
#[msg("has liquidatable trusted perp pnl")]
|
||||
HasLiquidatableTrustedPerpPnl,
|
||||
}
|
||||
|
||||
impl MangoError {
|
||||
|
|
|
@ -4,8 +4,7 @@ use fixed::types::I80F48;
|
|||
|
||||
use crate::error::*;
|
||||
use crate::state::{
|
||||
Bank, MangoAccountFixed, MangoAccountRef, PerpMarket, PerpMarketIndex, PerpPosition,
|
||||
Serum3MarketIndex, TokenIndex,
|
||||
Bank, MangoAccountRef, PerpMarket, PerpMarketIndex, PerpPosition, Serum3MarketIndex, TokenIndex,
|
||||
};
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
|
@ -135,6 +134,8 @@ pub struct Serum3Info {
|
|||
pub base_index: usize,
|
||||
pub quote_index: usize,
|
||||
pub market_index: Serum3MarketIndex,
|
||||
/// The open orders account has no free or reserved funds
|
||||
pub has_zero_funds: bool,
|
||||
}
|
||||
|
||||
impl Serum3Info {
|
||||
|
@ -328,29 +329,6 @@ impl HealthCache {
|
|||
health
|
||||
}
|
||||
|
||||
pub fn check_health_pre(&self, account: &mut MangoAccountFixed) -> Result<I80F48> {
|
||||
let pre_health = self.health(HealthType::Init);
|
||||
msg!("pre_health: {}", pre_health);
|
||||
account.maybe_recover_from_being_liquidated(pre_health);
|
||||
require!(!account.being_liquidated(), MangoError::BeingLiquidated);
|
||||
Ok(pre_health)
|
||||
}
|
||||
|
||||
pub fn check_health_post(
|
||||
&self,
|
||||
account: &mut MangoAccountFixed,
|
||||
pre_health: I80F48,
|
||||
) -> Result<()> {
|
||||
let post_health = self.health(HealthType::Init);
|
||||
msg!("post_health: {}", post_health);
|
||||
require!(
|
||||
post_health >= 0 || post_health > pre_health,
|
||||
MangoError::HealthMustBePositiveOrIncrease
|
||||
);
|
||||
account.maybe_recover_from_being_liquidated(post_health);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn token_info(&self, token_index: TokenIndex) -> Result<&TokenInfo> {
|
||||
Ok(&self.token_infos[self.token_info_index(token_index)?])
|
||||
}
|
||||
|
@ -446,13 +424,94 @@ impl HealthCache {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn has_serum3_open_orders_funds(&self) -> bool {
|
||||
self.serum3_infos.iter().any(|si| !si.has_zero_funds)
|
||||
}
|
||||
|
||||
pub fn has_perp_open_orders(&self) -> bool {
|
||||
self.perp_infos.iter().any(|p| p.has_open_orders)
|
||||
}
|
||||
|
||||
pub fn has_perp_base_positions(&self) -> bool {
|
||||
self.perp_infos.iter().any(|p| p.base_lots != 0)
|
||||
}
|
||||
|
||||
pub fn has_perp_positive_trusted_pnl_without_base_position(&self) -> bool {
|
||||
self.perp_infos
|
||||
.iter()
|
||||
.any(|p| p.trusted_market && p.base_lots == 0 && p.quote > 0)
|
||||
}
|
||||
|
||||
pub fn has_perp_negative_pnl(&self) -> bool {
|
||||
self.perp_infos.iter().any(|p| p.quote < 0)
|
||||
}
|
||||
|
||||
/// Phase1 is spot/perp order cancellation and spot settlement since
|
||||
/// neither of these come at a cost to the liqee
|
||||
pub fn has_phase1_liquidatable(&self) -> bool {
|
||||
self.has_serum3_open_orders_funds() || self.has_perp_open_orders()
|
||||
}
|
||||
|
||||
pub fn require_after_phase1_liquidation(&self) -> Result<()> {
|
||||
require!(
|
||||
!self.has_serum3_open_orders_funds(),
|
||||
MangoError::HasOpenOrUnsettledSerum3Orders
|
||||
);
|
||||
require!(!self.has_perp_open_orders(), MangoError::HasOpenPerpOrders);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn in_phase1_liquidation(&self) -> bool {
|
||||
self.has_phase1_liquidatable()
|
||||
}
|
||||
|
||||
/// Phase2 is for:
|
||||
/// - token-token liquidation
|
||||
/// - liquidation of perp base positions
|
||||
/// - bringing positive trusted perp pnl into the spot realm
|
||||
pub fn has_phase2_liquidatable(&self) -> bool {
|
||||
self.has_spot_assets() && self.has_spot_borrows()
|
||||
|| self.has_perp_base_positions()
|
||||
|| self.has_perp_positive_trusted_pnl_without_base_position()
|
||||
}
|
||||
|
||||
pub fn require_after_phase2_liquidation(&self) -> Result<()> {
|
||||
self.require_after_phase1_liquidation()?;
|
||||
require!(
|
||||
!self.has_spot_assets() || !self.has_spot_borrows(),
|
||||
MangoError::HasLiquidatableTokenPosition
|
||||
);
|
||||
require!(
|
||||
!self.has_perp_base_positions(),
|
||||
MangoError::HasLiquidatablePerpBasePosition
|
||||
);
|
||||
require!(
|
||||
!self.has_perp_positive_trusted_pnl_without_base_position(),
|
||||
MangoError::HasLiquidatableTrustedPerpPnl
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn in_phase2_liquidation(&self) -> bool {
|
||||
!self.has_phase1_liquidatable() && self.has_phase2_liquidatable()
|
||||
}
|
||||
|
||||
/// Phase3 is bankruptcy:
|
||||
/// - token bankruptcy
|
||||
/// - perp bankruptcy
|
||||
pub fn has_phase3_liquidatable(&self) -> bool {
|
||||
self.has_spot_borrows() || self.has_perp_negative_pnl()
|
||||
}
|
||||
|
||||
pub fn in_phase3_liquidation(&self) -> bool {
|
||||
!self.has_phase1_liquidatable()
|
||||
&& !self.has_phase2_liquidatable()
|
||||
&& self.has_phase3_liquidatable()
|
||||
}
|
||||
|
||||
pub fn has_liquidatable_assets(&self) -> bool {
|
||||
let spot_liquidatable = self.has_spot_assets();
|
||||
// can use serum3_liq_force_cancel_orders
|
||||
let serum3_cancelable = self
|
||||
.serum3_infos
|
||||
.iter()
|
||||
.any(|si| si.reserved_base != 0 || si.reserved_quote != 0);
|
||||
let serum3_cancelable = self.has_serum3_open_orders_funds();
|
||||
let perp_liquidatable = self.perp_infos.iter().any(|p| {
|
||||
// can use perp_liq_base_position
|
||||
p.base_lots != 0
|
||||
|
@ -477,6 +536,21 @@ impl HealthCache {
|
|||
self.has_spot_borrows() || perp_borrows
|
||||
}
|
||||
|
||||
pub fn has_liquidatable_spot_or_perp_base(&self) -> bool {
|
||||
let spot_liquidatable = self.has_spot_assets();
|
||||
let serum3_cancelable = self.has_serum3_open_orders_funds();
|
||||
let perp_liquidatable = self.perp_infos.iter().any(|p| {
|
||||
// can use perp_liq_base_position
|
||||
p.base_lots != 0
|
||||
// can use perp_liq_force_cancel_orders
|
||||
|| p.has_open_orders
|
||||
// A remaining quote position can be reduced with perp_settle_pnl and that can improve health.
|
||||
// However, since it's not guaranteed that there is a counterparty, a positive perp quote position
|
||||
// does not prevent bankruptcy.
|
||||
});
|
||||
spot_liquidatable || serum3_cancelable || perp_liquidatable
|
||||
}
|
||||
|
||||
pub(crate) fn compute_serum3_reservations(
|
||||
&self,
|
||||
health_type: HealthType,
|
||||
|
@ -671,6 +745,9 @@ pub fn new_health_cache(
|
|||
base_index,
|
||||
quote_index,
|
||||
market_index: serum_account.market_index,
|
||||
has_zero_funds: oo.native_coin_total == 0
|
||||
&& oo.native_pc_total == 0
|
||||
&& oo.referrer_rebates_accrued == 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -846,6 +846,7 @@ mod tests {
|
|||
market_index: 0,
|
||||
reserved_base: I80F48::from(30 / 3),
|
||||
reserved_quote: I80F48::from(30 / 2),
|
||||
has_zero_funds: false,
|
||||
}];
|
||||
adjust_by_usdc(&mut health_cache, 0, -20.0);
|
||||
adjust_by_usdc(&mut health_cache, 1, -40.0);
|
||||
|
|
|
@ -20,9 +20,9 @@ pub use perp_consume_events::*;
|
|||
pub use perp_create_market::*;
|
||||
pub use perp_deactivate_position::*;
|
||||
pub use perp_edit_market::*;
|
||||
pub use perp_liq_bankruptcy::*;
|
||||
pub use perp_liq_base_position::*;
|
||||
pub use perp_liq_force_cancel_orders::*;
|
||||
pub use perp_liq_quote_and_bankruptcy::*;
|
||||
pub use perp_place_order::*;
|
||||
pub use perp_settle_fees::*;
|
||||
pub use perp_settle_pnl::*;
|
||||
|
@ -73,9 +73,9 @@ mod perp_consume_events;
|
|||
mod perp_create_market;
|
||||
mod perp_deactivate_position;
|
||||
mod perp_edit_market;
|
||||
mod perp_liq_bankruptcy;
|
||||
mod perp_liq_base_position;
|
||||
mod perp_liq_force_cancel_orders;
|
||||
mod perp_liq_quote_and_bankruptcy;
|
||||
mod perp_place_order;
|
||||
mod perp_settle_fees;
|
||||
mod perp_settle_pnl;
|
||||
|
|
|
@ -71,7 +71,6 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
|
|||
emit_perp_balances(
|
||||
ctx.accounts.group.key(),
|
||||
fill.maker,
|
||||
perp_market.perp_market_index,
|
||||
ma.perp_position(perp_market.perp_market_index).unwrap(),
|
||||
&perp_market,
|
||||
);
|
||||
|
@ -123,14 +122,12 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
|
|||
emit_perp_balances(
|
||||
ctx.accounts.group.key(),
|
||||
fill.maker,
|
||||
perp_market.perp_market_index,
|
||||
maker.perp_position(perp_market.perp_market_index).unwrap(),
|
||||
&perp_market,
|
||||
);
|
||||
emit_perp_balances(
|
||||
ctx.accounts.group.key(),
|
||||
fill.taker,
|
||||
perp_market.perp_market_index,
|
||||
taker.perp_position(perp_market.perp_market_index).unwrap(),
|
||||
&perp_market,
|
||||
);
|
||||
|
|
|
@ -1,238 +0,0 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token;
|
||||
use anchor_spl::token::Token;
|
||||
use anchor_spl::token::TokenAccount;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::health::*;
|
||||
use crate::state::*;
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
use crate::logs::{emit_perp_balances, PerpLiqBankruptcyLog, TokenBalanceLog};
|
||||
|
||||
// Remaining accounts:
|
||||
// - merged health accounts for liqor+liqee
|
||||
#[derive(Accounts)]
|
||||
pub struct PerpLiqBankruptcy<'info> {
|
||||
#[account(
|
||||
has_one = insurance_vault,
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(mut, has_one = group)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group
|
||||
// liqor_owner is checked at #1
|
||||
)]
|
||||
pub liqor: AccountLoader<'info, MangoAccountFixed>,
|
||||
pub liqor_owner: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group
|
||||
)]
|
||||
pub liqee: AccountLoader<'info, MangoAccountFixed>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
// address is checked at #2
|
||||
)]
|
||||
pub settle_bank: AccountLoader<'info, Bank>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
address = settle_bank.load()?.vault
|
||||
)]
|
||||
pub settle_vault: Account<'info, TokenAccount>,
|
||||
|
||||
/// CHECK: Oracle can have different account types
|
||||
#[account(address = settle_bank.load()?.oracle)]
|
||||
pub settle_oracle: UncheckedAccount<'info>,
|
||||
|
||||
// future: this would be an insurance fund vault specific to a
|
||||
// trustless token, separate from the shared one on the group
|
||||
#[account(mut)]
|
||||
pub insurance_vault: Account<'info, TokenAccount>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
||||
impl<'info> PerpLiqBankruptcy<'info> {
|
||||
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
|
||||
let program = self.token_program.to_account_info();
|
||||
let accounts = token::Transfer {
|
||||
from: self.insurance_vault.to_account_info(),
|
||||
to: self.settle_vault.to_account_info(),
|
||||
authority: self.group.to_account_info(),
|
||||
};
|
||||
CpiContext::new(program, accounts)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn perp_liq_bankruptcy(ctx: Context<PerpLiqBankruptcy>, max_liab_transfer: u64) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
let group_pk = &ctx.accounts.group.key();
|
||||
|
||||
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
|
||||
// account constraint #1
|
||||
require!(
|
||||
liqor
|
||||
.fixed
|
||||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
|
||||
|
||||
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
||||
let mut liqee_health_cache = {
|
||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)?;
|
||||
new_health_cache(&liqee.borrow(), &account_retriever)
|
||||
.context("create liqee health cache")?
|
||||
};
|
||||
|
||||
// Check if liqee is bankrupt
|
||||
require!(
|
||||
!liqee_health_cache.has_liquidatable_assets(),
|
||||
MangoError::IsNotBankrupt
|
||||
);
|
||||
liqee.fixed.set_being_liquidated(true);
|
||||
|
||||
// Find bankrupt liab amount
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let settle_token_index = perp_market.settle_token_index;
|
||||
let liqee_perp_position = liqee.perp_position_mut(perp_market.perp_market_index)?;
|
||||
require_msg!(
|
||||
liqee_perp_position.base_position_lots() == 0,
|
||||
"liqee must have zero base position"
|
||||
);
|
||||
require!(
|
||||
!liqee_perp_position.has_open_orders(),
|
||||
MangoError::HasOpenPerpOrders
|
||||
);
|
||||
|
||||
let liqee_pnl = liqee_perp_position.quote_position_native();
|
||||
require_msg!(
|
||||
liqee_pnl.is_negative(),
|
||||
"liqee pnl must be negative, was {}",
|
||||
liqee_pnl
|
||||
);
|
||||
let liab_transfer = (-liqee_pnl).min(I80F48::from(max_liab_transfer));
|
||||
|
||||
// Preparation for covering it with the insurance fund
|
||||
let insurance_vault_amount = if perp_market.elligible_for_group_insurance_fund() {
|
||||
ctx.accounts.insurance_vault.amount
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let liquidation_fee_factor = cm!(I80F48::ONE + perp_market.liquidation_fee);
|
||||
|
||||
let insurance_transfer = cm!(liab_transfer * liquidation_fee_factor)
|
||||
.checked_ceil()
|
||||
.unwrap()
|
||||
.checked_to_num::<u64>()
|
||||
.unwrap()
|
||||
.min(insurance_vault_amount);
|
||||
|
||||
let insurance_transfer_i80f48 = I80F48::from(insurance_transfer);
|
||||
let insurance_fund_exhausted = insurance_transfer == insurance_vault_amount;
|
||||
let insurance_liab_transfer =
|
||||
cm!(insurance_transfer_i80f48 / liquidation_fee_factor).min(liab_transfer);
|
||||
|
||||
// Try using the insurance fund if possible
|
||||
if insurance_transfer > 0 {
|
||||
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
|
||||
require_eq!(settle_bank.token_index, settle_token_index);
|
||||
require_keys_eq!(settle_bank.mint, ctx.accounts.insurance_vault.mint);
|
||||
|
||||
// move insurance assets into quote bank
|
||||
let group_seeds = group_seeds!(group);
|
||||
token::transfer(
|
||||
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
|
||||
insurance_transfer,
|
||||
)?;
|
||||
|
||||
// credit the liqor with quote tokens
|
||||
let (liqor_quote, _, _) = liqor.ensure_token_position(settle_token_index)?;
|
||||
settle_bank.deposit(
|
||||
liqor_quote,
|
||||
insurance_transfer_i80f48,
|
||||
Clock::get()?.unix_timestamp.try_into().unwrap(),
|
||||
)?;
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.liqor.key(),
|
||||
token_index: settle_token_index,
|
||||
indexed_position: liqor_quote.indexed_position.to_bits(),
|
||||
deposit_index: settle_bank.deposit_index.to_bits(),
|
||||
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||
});
|
||||
|
||||
// transfer perp quote loss from the liqee to the liqor
|
||||
let liqor_perp_position = liqor
|
||||
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)?
|
||||
.0;
|
||||
liqee_perp_position.record_settle(-insurance_liab_transfer);
|
||||
liqor_perp_position.record_liquidation_quote_change(-insurance_liab_transfer);
|
||||
|
||||
emit_perp_balances(
|
||||
ctx.accounts.group.key(),
|
||||
ctx.accounts.liqor.key(),
|
||||
perp_market.perp_market_index,
|
||||
liqor_perp_position,
|
||||
&perp_market,
|
||||
);
|
||||
}
|
||||
|
||||
// Socialize loss if the insurance fund is exhausted
|
||||
let remaining_liab = liab_transfer - insurance_liab_transfer;
|
||||
let mut socialized_loss = I80F48::ZERO;
|
||||
if insurance_fund_exhausted && remaining_liab.is_positive() {
|
||||
perp_market.socialize_loss(-remaining_liab)?;
|
||||
liqee_perp_position.record_settle(-remaining_liab);
|
||||
require_eq!(liqee_perp_position.quote_position_native(), 0);
|
||||
socialized_loss = remaining_liab;
|
||||
}
|
||||
|
||||
emit_perp_balances(
|
||||
ctx.accounts.group.key(),
|
||||
ctx.accounts.liqee.key(),
|
||||
perp_market.perp_market_index,
|
||||
liqee_perp_position,
|
||||
&perp_market,
|
||||
);
|
||||
|
||||
emit!(PerpLiqBankruptcyLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
liqee: ctx.accounts.liqee.key(),
|
||||
liqor: ctx.accounts.liqor.key(),
|
||||
perp_market_index: perp_market.perp_market_index,
|
||||
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
||||
socialized_loss: socialized_loss.to_bits()
|
||||
});
|
||||
|
||||
// Check liqee health again
|
||||
liqee_health_cache.recompute_perp_info(liqee_perp_position, &perp_market)?;
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
liqee
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(liqee_init_health);
|
||||
|
||||
drop(perp_market);
|
||||
|
||||
// Check liqor's health
|
||||
if !liqor.fixed.is_in_health_region() {
|
||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)?;
|
||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
||||
.context("compute liqor health")?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -45,7 +45,11 @@ pub fn perp_liq_base_position(
|
|||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
|
||||
require_msg_typed!(
|
||||
!liqor.fixed.being_liquidated(),
|
||||
MangoError::BeingLiquidated,
|
||||
"liqor account"
|
||||
);
|
||||
|
||||
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
||||
|
||||
|
@ -57,6 +61,7 @@ pub fn perp_liq_base_position(
|
|||
.context("create liqee health cache")?
|
||||
};
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
liqee_health_cache.require_after_phase1_liquidation()?;
|
||||
|
||||
// Once maint_health falls below 0, we want to start liquidating,
|
||||
// we want to allow liquidation to continue until init_health is positive,
|
||||
|
@ -96,11 +101,6 @@ pub fn perp_liq_base_position(
|
|||
.0;
|
||||
let liqee_base_lots = liqee_perp_position.base_position_lots();
|
||||
|
||||
require!(
|
||||
!liqee_perp_position.has_open_orders(),
|
||||
MangoError::HasOpenPerpOrders
|
||||
);
|
||||
|
||||
// Settle funding
|
||||
liqee_perp_position.settle_funding(&perp_market);
|
||||
liqor_perp_position.settle_funding(&perp_market);
|
||||
|
@ -173,7 +173,6 @@ pub fn perp_liq_base_position(
|
|||
emit_perp_balances(
|
||||
ctx.accounts.group.key(),
|
||||
ctx.accounts.liqor.key(),
|
||||
perp_market.perp_market_index,
|
||||
liqor_perp_position,
|
||||
&perp_market,
|
||||
);
|
||||
|
@ -181,7 +180,6 @@ pub fn perp_liq_base_position(
|
|||
emit_perp_balances(
|
||||
ctx.accounts.group.key(),
|
||||
ctx.accounts.liqee.key(),
|
||||
perp_market.perp_market_index,
|
||||
liqee_perp_position,
|
||||
&perp_market,
|
||||
);
|
||||
|
|
|
@ -0,0 +1,380 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token;
|
||||
use anchor_spl::token::Token;
|
||||
use anchor_spl::token::TokenAccount;
|
||||
use checked_math as cm;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::*;
|
||||
use crate::health::{compute_health, new_health_cache, HealthType, ScanningAccountRetriever};
|
||||
use crate::logs::{
|
||||
emit_perp_balances, PerpLiqBankruptcyLog, PerpLiqQuoteAndBankruptcyLog, TokenBalanceLog,
|
||||
};
|
||||
use crate::state::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct PerpLiqQuoteAndBankruptcy<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
// liqor_owner is checked at #1
|
||||
)]
|
||||
pub liqor: AccountLoader<'info, MangoAccountFixed>,
|
||||
pub liqor_owner: Signer<'info>,
|
||||
|
||||
// This account MUST have a loss
|
||||
#[account(mut, has_one = group)]
|
||||
pub liqee: AccountLoader<'info, MangoAccountFixed>,
|
||||
|
||||
#[account(mut, has_one = group, has_one = oracle)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
|
||||
/// CHECK: Oracle can have different account types, constrained by address in perp_market
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
|
||||
// bank correctness is checked at #2
|
||||
#[account(mut, has_one = group)]
|
||||
pub settle_bank: AccountLoader<'info, Bank>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
address = settle_bank.load()?.vault
|
||||
)]
|
||||
pub settle_vault: Account<'info, TokenAccount>,
|
||||
|
||||
/// CHECK: Oracle can have different account types
|
||||
#[account(address = settle_bank.load()?.oracle)]
|
||||
pub settle_oracle: UncheckedAccount<'info>,
|
||||
|
||||
// future: this would be an insurance fund vault specific to a
|
||||
// trustless token, separate from the shared one on the group
|
||||
#[account(mut)]
|
||||
pub insurance_vault: Account<'info, TokenAccount>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
||||
impl<'info> PerpLiqQuoteAndBankruptcy<'info> {
|
||||
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
|
||||
let program = self.token_program.to_account_info();
|
||||
let accounts = token::Transfer {
|
||||
from: self.insurance_vault.to_account_info(),
|
||||
to: self.settle_vault.to_account_info(),
|
||||
authority: self.group.to_account_info(),
|
||||
};
|
||||
CpiContext::new(program, accounts)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn perp_liq_quote_and_bankruptcy(
|
||||
ctx: Context<PerpLiqQuoteAndBankruptcy>,
|
||||
max_liab_transfer: u64,
|
||||
) -> Result<()> {
|
||||
let mango_group = ctx.accounts.group.key();
|
||||
|
||||
// Cannot settle with yourself
|
||||
require!(
|
||||
ctx.accounts.liqor.key() != ctx.accounts.liqee.key(),
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
let (perp_market_index, settle_token_index) = {
|
||||
let perp_market = ctx.accounts.perp_market.load()?;
|
||||
(
|
||||
perp_market.perp_market_index,
|
||||
perp_market.settle_token_index,
|
||||
)
|
||||
};
|
||||
|
||||
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
||||
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
|
||||
// account constraint #1
|
||||
require!(
|
||||
liqor
|
||||
.fixed
|
||||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require_msg_typed!(
|
||||
!liqor.fixed.being_liquidated(),
|
||||
MangoError::BeingLiquidated,
|
||||
"liqor account"
|
||||
);
|
||||
|
||||
let mut liqee_health_cache = {
|
||||
let retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)
|
||||
.context("create account retriever")?;
|
||||
new_health_cache(&liqee.borrow(), &retriever)?
|
||||
};
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
let liqee_settle_health = liqee_health_cache.perp_settle_health();
|
||||
liqee_health_cache.require_after_phase2_liquidation()?;
|
||||
|
||||
// Once maint_health falls below 0, we want to start liquidating,
|
||||
// we want to allow liquidation to continue until init_health is positive,
|
||||
// to prevent constant oscillation between the two states
|
||||
if liqee.being_liquidated() {
|
||||
if liqee
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(liqee_init_health)
|
||||
{
|
||||
msg!("Liqee init_health above zero");
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
let maint_health = liqee_health_cache.health(HealthType::Maint);
|
||||
require!(
|
||||
maint_health < I80F48::ZERO,
|
||||
MangoError::HealthMustBeNegative
|
||||
);
|
||||
liqee.fixed.set_being_liquidated(true);
|
||||
}
|
||||
|
||||
// check positions exist/create them, done early for nicer error messages
|
||||
{
|
||||
liqee.perp_position(perp_market_index)?;
|
||||
liqee.token_position(settle_token_index)?;
|
||||
liqor.ensure_perp_position(perp_market_index, settle_token_index)?;
|
||||
liqor.ensure_token_position(settle_token_index)?;
|
||||
}
|
||||
|
||||
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
|
||||
// account constraint #2
|
||||
require!(
|
||||
settle_bank.token_index == settle_token_index,
|
||||
MangoError::InvalidBank
|
||||
);
|
||||
|
||||
// Get oracle price for market. Price is validated inside
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let oracle_price = perp_market.oracle_price(
|
||||
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||
None, // staleness checked in health
|
||||
)?;
|
||||
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
|
||||
//
|
||||
// Step 1: Allow the liqor to take over ("settle") negative liqee pnl.
|
||||
//
|
||||
// The only limitation is the liqee's perp_settle_health and its perp pnl settle limit.
|
||||
//
|
||||
let settlement;
|
||||
let max_settlement_liqee;
|
||||
{
|
||||
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
|
||||
let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
|
||||
liqee_perp_position.settle_funding(&perp_market);
|
||||
liqor_perp_position.settle_funding(&perp_market);
|
||||
|
||||
let liqee_pnl = liqee_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
||||
// TODO: deal with positive liqee pnl! Maybe another instruction?
|
||||
require!(liqee_pnl < 0, MangoError::ProfitabilityMismatch);
|
||||
|
||||
// Get settleable pnl on the liqee
|
||||
liqee_perp_position.update_settle_limit(&perp_market, now_ts);
|
||||
let liqee_settleable_pnl =
|
||||
liqee_perp_position.apply_pnl_settle_limit(&perp_market, liqee_pnl);
|
||||
|
||||
max_settlement_liqee = liqee_settle_health
|
||||
.min(-liqee_settleable_pnl)
|
||||
.max(I80F48::ZERO);
|
||||
settlement = max_settlement_liqee
|
||||
.min(I80F48::from(max_liab_transfer))
|
||||
.max(I80F48::ZERO);
|
||||
if settlement > 0 {
|
||||
liqor_perp_position.record_liquidation_quote_change(-settlement);
|
||||
liqee_perp_position.record_settle(-settlement);
|
||||
|
||||
// Update the accounts' perp_spot_transfer statistics.
|
||||
let settlement_i64 = settlement.round_to_zero().checked_to_num::<i64>().unwrap();
|
||||
cm!(liqor_perp_position.perp_spot_transfers += settlement_i64);
|
||||
cm!(liqee_perp_position.perp_spot_transfers -= settlement_i64);
|
||||
cm!(liqor.fixed.perp_spot_transfers += settlement_i64);
|
||||
cm!(liqee.fixed.perp_spot_transfers -= settlement_i64);
|
||||
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
|
||||
// Transfer token balance
|
||||
let liqor_token_position = liqor.token_position_mut(settle_token_index)?.0;
|
||||
let liqee_token_position = liqee.token_position_mut(settle_token_index)?.0;
|
||||
settle_bank.deposit(liqor_token_position, settlement, now_ts)?;
|
||||
settle_bank.withdraw_without_fee(
|
||||
liqee_token_position,
|
||||
settlement,
|
||||
now_ts,
|
||||
oracle_price,
|
||||
)?;
|
||||
liqee_health_cache.adjust_token_balance(&settle_bank, -settlement)?;
|
||||
|
||||
emit!(PerpLiqQuoteAndBankruptcyLog {
|
||||
mango_group,
|
||||
liqee: ctx.accounts.liqee.key(),
|
||||
liqor: ctx.accounts.liqor.key(),
|
||||
perp_market_index: perp_market_index,
|
||||
settlement: settlement.to_bits(),
|
||||
});
|
||||
|
||||
msg!("liquidated pnl = {}", settlement);
|
||||
}
|
||||
};
|
||||
let max_liab_transfer = cm!(I80F48::from(max_liab_transfer) - settlement);
|
||||
|
||||
//
|
||||
// Step 2: bankruptcy
|
||||
//
|
||||
// Remaining pnl that brings the account into negative init health is either:
|
||||
// - taken by the liqor in exchange for spot from the insurance fund, or
|
||||
// - wiped away and socialized among all perp participants
|
||||
//
|
||||
let insurance_transfer = if settlement == max_settlement_liqee {
|
||||
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
|
||||
let liqee_pnl = liqee_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
||||
|
||||
let max_liab_transfer_from_liqee = (-liqee_pnl).min(-liqee_init_health).max(I80F48::ZERO);
|
||||
let liab_transfer = max_liab_transfer_from_liqee
|
||||
.min(max_liab_transfer)
|
||||
.max(I80F48::ZERO);
|
||||
|
||||
// Available insurance fund coverage
|
||||
let insurance_vault_amount = if perp_market.elligible_for_group_insurance_fund() {
|
||||
ctx.accounts.insurance_vault.amount
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let liquidation_fee_factor = cm!(I80F48::ONE + perp_market.liquidation_fee);
|
||||
|
||||
// Amount given to the liqor from the insurance fund
|
||||
let insurance_transfer = cm!(liab_transfer * liquidation_fee_factor)
|
||||
.checked_ceil()
|
||||
.unwrap()
|
||||
.checked_to_num::<u64>()
|
||||
.unwrap()
|
||||
.min(insurance_vault_amount);
|
||||
|
||||
let insurance_transfer_i80f48 = I80F48::from(insurance_transfer);
|
||||
let insurance_fund_exhausted = insurance_transfer == insurance_vault_amount;
|
||||
|
||||
// Amount of negative perp pnl transfered to the liqor
|
||||
let insurance_liab_transfer =
|
||||
cm!(insurance_transfer_i80f48 / liquidation_fee_factor).min(liab_transfer);
|
||||
|
||||
// Try using the insurance fund if possible
|
||||
if insurance_transfer > 0 {
|
||||
require_keys_eq!(settle_bank.mint, ctx.accounts.insurance_vault.mint);
|
||||
|
||||
// move insurance assets into quote bank
|
||||
let group = ctx.accounts.group.load()?;
|
||||
let group_seeds = group_seeds!(group);
|
||||
token::transfer(
|
||||
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
|
||||
insurance_transfer,
|
||||
)?;
|
||||
|
||||
// credit the liqor with quote tokens
|
||||
let (liqor_quote, _, _) = liqor.ensure_token_position(settle_token_index)?;
|
||||
settle_bank.deposit(liqor_quote, insurance_transfer_i80f48, now_ts)?;
|
||||
|
||||
// transfer perp quote loss from the liqee to the liqor
|
||||
let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
|
||||
liqee_perp_position.record_settle(-insurance_liab_transfer);
|
||||
liqor_perp_position.record_liquidation_quote_change(-insurance_liab_transfer);
|
||||
}
|
||||
|
||||
// Socialize loss if the insurance fund is exhausted
|
||||
// At this point, we don't care about the liqor's requested max_liab_tranfer
|
||||
let remaining_liab = max_liab_transfer_from_liqee - insurance_liab_transfer;
|
||||
let mut socialized_loss = I80F48::ZERO;
|
||||
let (starting_long_funding, starting_short_funding) =
|
||||
(perp_market.long_funding, perp_market.short_funding);
|
||||
if insurance_fund_exhausted && remaining_liab > 0 {
|
||||
perp_market.socialize_loss(-remaining_liab)?;
|
||||
liqee_perp_position.record_settle(-remaining_liab);
|
||||
socialized_loss = remaining_liab;
|
||||
}
|
||||
|
||||
emit!(PerpLiqBankruptcyLog {
|
||||
mango_group,
|
||||
liqee: ctx.accounts.liqee.key(),
|
||||
liqor: ctx.accounts.liqor.key(),
|
||||
perp_market_index: perp_market.perp_market_index,
|
||||
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
||||
socialized_loss: socialized_loss.to_bits(),
|
||||
starting_long_funding: starting_long_funding.to_bits(),
|
||||
starting_short_funding: starting_short_funding.to_bits(),
|
||||
ending_long_funding: perp_market.long_funding.to_bits(),
|
||||
ending_short_funding: perp_market.short_funding.to_bits(),
|
||||
});
|
||||
|
||||
insurance_transfer
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
//
|
||||
// Log positions aftewards
|
||||
//
|
||||
if settlement > 0 || insurance_transfer > 0 {
|
||||
let liqor_token_position = liqor.token_position(settle_token_index)?;
|
||||
emit!(TokenBalanceLog {
|
||||
mango_group,
|
||||
mango_account: ctx.accounts.liqor.key(),
|
||||
token_index: settle_token_index,
|
||||
indexed_position: liqor_token_position.indexed_position.to_bits(),
|
||||
deposit_index: settle_bank.deposit_index.to_bits(),
|
||||
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||
});
|
||||
}
|
||||
|
||||
if settlement > 0 {
|
||||
let liqee_token_position = liqee.token_position(settle_token_index)?;
|
||||
emit!(TokenBalanceLog {
|
||||
mango_group,
|
||||
mango_account: ctx.accounts.liqee.key(),
|
||||
token_index: settle_token_index,
|
||||
indexed_position: liqee_token_position.indexed_position.to_bits(),
|
||||
deposit_index: settle_bank.deposit_index.to_bits(),
|
||||
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||
});
|
||||
}
|
||||
|
||||
let liqee_perp_position = liqee.perp_position(perp_market_index)?;
|
||||
let liqor_perp_position = liqor.perp_position(perp_market_index)?;
|
||||
emit_perp_balances(
|
||||
mango_group,
|
||||
ctx.accounts.liqor.key(),
|
||||
liqor_perp_position,
|
||||
&perp_market,
|
||||
);
|
||||
emit_perp_balances(
|
||||
mango_group,
|
||||
ctx.accounts.liqee.key(),
|
||||
liqee_perp_position,
|
||||
&perp_market,
|
||||
);
|
||||
|
||||
// Check liqee health again: bankruptcy would improve health
|
||||
liqee_health_cache.recompute_perp_info(liqee_perp_position, &perp_market)?;
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
liqee
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(liqee_init_health);
|
||||
|
||||
drop(perp_market);
|
||||
drop(settle_bank);
|
||||
|
||||
// Check liqor's health
|
||||
if !liqor.fixed.is_in_health_region() {
|
||||
let account_retriever =
|
||||
ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)?;
|
||||
let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)
|
||||
.context("compute liqor health")?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -91,7 +91,6 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
|
|||
emit_perp_balances(
|
||||
ctx.accounts.group.key(),
|
||||
ctx.accounts.account.key(),
|
||||
perp_market.perp_market_index,
|
||||
perp_position,
|
||||
&perp_market,
|
||||
);
|
||||
|
|
|
@ -81,19 +81,12 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
a_maint_health = a_cache.health(HealthType::Maint);
|
||||
};
|
||||
|
||||
// Account B is the one that must have negative pnl. Check how much of that may be actualized
|
||||
// given the account's health. In that, we only care about the health of spot assets on the account.
|
||||
// Example: With +100 USDC and -2 SOL (-80 USD) and -500 USD PNL the account may still settle
|
||||
// 100 - 1.1*80 = 12 USD perp pnl, even though the overall health is already negative.
|
||||
// Further settlement would convert perp-losses into token-losses and isn't allowed.
|
||||
require!(b_settle_health >= 0, MangoError::HealthMustBePositive);
|
||||
|
||||
let mut bank = ctx.accounts.settle_bank.load_mut()?;
|
||||
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
|
||||
let perp_market = ctx.accounts.perp_market.load()?;
|
||||
|
||||
// Verify that the bank is the quote currency bank
|
||||
require!(
|
||||
bank.token_index == settle_token_index,
|
||||
settle_bank.token_index == settle_token_index,
|
||||
MangoError::InvalidBank
|
||||
);
|
||||
|
||||
|
@ -103,20 +96,16 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
None, // staleness checked in health
|
||||
)?;
|
||||
|
||||
// Fetch perp positions for accounts
|
||||
// Fetch perp position and pnl
|
||||
let a_perp_position = account_a.perp_position_mut(perp_market_index)?;
|
||||
let b_perp_position = account_b.perp_position_mut(perp_market_index)?;
|
||||
|
||||
// Settle funding before settling any PnL
|
||||
a_perp_position.settle_funding(&perp_market);
|
||||
b_perp_position.settle_funding(&perp_market);
|
||||
|
||||
// Calculate PnL for each account
|
||||
let a_pnl = a_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
||||
let b_pnl = b_perp_position.pnl_for_price(&perp_market, oracle_price)?;
|
||||
|
||||
// Account A must be profitable, and B must be unprofitable
|
||||
// PnL must be opposite signs for there to be a settlement
|
||||
// PnL must have opposite signs for there to be a settlement:
|
||||
// Account A must be profitable, and B must be unprofitable.
|
||||
require_msg_typed!(
|
||||
a_pnl.is_positive(),
|
||||
MangoError::ProfitabilityMismatch,
|
||||
|
@ -130,12 +119,11 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
b_pnl
|
||||
);
|
||||
|
||||
// Cap settlement of unrealized pnl
|
||||
// Settles at most x100% each hour
|
||||
// Apply pnl settle limits
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
a_perp_position.update_settle_limit(&perp_market, now_ts);
|
||||
b_perp_position.update_settle_limit(&perp_market, now_ts);
|
||||
let a_settleable_pnl = a_perp_position.apply_pnl_settle_limit(&perp_market, a_pnl);
|
||||
b_perp_position.update_settle_limit(&perp_market, now_ts);
|
||||
let b_settleable_pnl = b_perp_position.apply_pnl_settle_limit(&perp_market, b_pnl);
|
||||
|
||||
require_msg_typed!(
|
||||
|
@ -153,11 +141,23 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
b_pnl
|
||||
);
|
||||
|
||||
// Settle for the maximum possible capped to b's settle health
|
||||
// Check how much of account b's negative pnl may be actualized given the health.
|
||||
// In that, we only care about the health of spot assets on the account.
|
||||
// Example: With +100 USDC and -2 SOL (-80 USD) and -500 USD PNL the account may still settle
|
||||
// 100 - 1.1*80 = 12 USD perp pnl, even though the overall health is already negative.
|
||||
// Further settlement would convert perp-losses into unbacked token-losses and isn't allowed.
|
||||
require_msg_typed!(
|
||||
b_settle_health >= 0,
|
||||
MangoError::HealthMustBePositive,
|
||||
"account b settle health is negative: {}",
|
||||
b_settle_health
|
||||
);
|
||||
|
||||
// Settle for the maximum possible capped to target's settle health
|
||||
let settlement = a_settleable_pnl
|
||||
.abs()
|
||||
.min(b_settleable_pnl.abs())
|
||||
.min(b_settle_health);
|
||||
.min(-b_settleable_pnl)
|
||||
.min(b_settle_health)
|
||||
.max(I80F48::ZERO);
|
||||
require_msg_typed!(
|
||||
settlement >= 0,
|
||||
MangoError::SettlementAmountMustBePositive,
|
||||
|
@ -167,54 +167,28 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
b_settle_health,
|
||||
);
|
||||
|
||||
// Settle
|
||||
let fee = compute_settle_fee(&perp_market, a_init_health, a_maint_health, settlement)?;
|
||||
|
||||
a_perp_position.record_settle(settlement);
|
||||
b_perp_position.record_settle(-settlement);
|
||||
|
||||
emit_perp_balances(
|
||||
ctx.accounts.group.key(),
|
||||
ctx.accounts.account_a.key(),
|
||||
perp_market.perp_market_index,
|
||||
a_perp_position,
|
||||
&perp_market,
|
||||
);
|
||||
|
||||
emit_perp_balances(
|
||||
ctx.accounts.group.key(),
|
||||
ctx.accounts.account_b.key(),
|
||||
perp_market.perp_market_index,
|
||||
b_perp_position,
|
||||
&perp_market,
|
||||
);
|
||||
|
||||
// A percentage fee is paid to the settler when account_a's health is low.
|
||||
// That's because the settlement could avoid it getting liquidated.
|
||||
let low_health_fee = if a_init_health < 0 {
|
||||
let fee_fraction = I80F48::from_num(perp_market.settle_fee_fraction_low_health);
|
||||
if a_maint_health < 0 {
|
||||
cm!(settlement * fee_fraction)
|
||||
} else {
|
||||
cm!(settlement * fee_fraction * (-a_init_health / (a_maint_health - a_init_health)))
|
||||
}
|
||||
} else {
|
||||
I80F48::ZERO
|
||||
};
|
||||
|
||||
// The settler receives a flat fee
|
||||
let flat_fee = I80F48::from_num(perp_market.settle_fee_flat);
|
||||
|
||||
// Fees only apply when the settlement is large enough
|
||||
let fee = if settlement >= perp_market.settle_fee_amount_threshold {
|
||||
cm!(low_health_fee + flat_fee).min(settlement)
|
||||
} else {
|
||||
I80F48::ZERO
|
||||
};
|
||||
|
||||
// Safety check to prevent any accidental negative transfer
|
||||
require!(fee >= 0, MangoError::SettlementAmountMustBePositive);
|
||||
|
||||
// Update the account's net_settled with the new PnL.
|
||||
// Update the accounts' perp_spot_transfer statistics.
|
||||
//
|
||||
// Applying the fee here means that it decreases the displayed perp pnl.
|
||||
// Think about it like this: a's pnl reduces by `settlement` and spot increases by `settlement - fee`.
|
||||
// That means that it managed to extract `settlement - fee` from perp interactions.
|
||||
let settlement_i64 = settlement.round_to_zero().checked_to_num::<i64>().unwrap();
|
||||
let fee_i64 = fee.round_to_zero().checked_to_num::<i64>().unwrap();
|
||||
cm!(a_perp_position.perp_spot_transfers += settlement_i64 - fee_i64);
|
||||
|
@ -222,34 +196,32 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
cm!(account_a.fixed.perp_spot_transfers += settlement_i64 - fee_i64);
|
||||
cm!(account_b.fixed.perp_spot_transfers -= settlement_i64);
|
||||
|
||||
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
|
||||
|
||||
// Transfer token balances
|
||||
// The fee is paid by the account with positive unsettled pnl
|
||||
let a_token_position = account_a.token_position_mut(settle_token_index)?.0;
|
||||
let b_token_position = account_b.token_position_mut(settle_token_index)?.0;
|
||||
bank.deposit(a_token_position, cm!(settlement - fee), now_ts)?;
|
||||
settle_bank.deposit(a_token_position, cm!(settlement - fee), now_ts)?;
|
||||
// Don't charge loan origination fees on borrows created via settling:
|
||||
// Even small loan origination fees could accumulate if a perp position is
|
||||
// settled back and forth repeatedly.
|
||||
bank.withdraw_without_fee(b_token_position, settlement, now_ts, oracle_price)?;
|
||||
settle_bank.withdraw_without_fee(b_token_position, settlement, now_ts, oracle_price)?;
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.settler.key(),
|
||||
mango_account: ctx.accounts.account_a.key(),
|
||||
token_index: settle_token_index,
|
||||
indexed_position: a_token_position.indexed_position.to_bits(),
|
||||
deposit_index: bank.deposit_index.to_bits(),
|
||||
borrow_index: bank.borrow_index.to_bits(),
|
||||
deposit_index: settle_bank.deposit_index.to_bits(),
|
||||
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||
});
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.settler.key(),
|
||||
mango_account: ctx.accounts.account_b.key(),
|
||||
token_index: settle_token_index,
|
||||
indexed_position: b_token_position.indexed_position.to_bits(),
|
||||
deposit_index: bank.deposit_index.to_bits(),
|
||||
borrow_index: bank.borrow_index.to_bits(),
|
||||
deposit_index: settle_bank.deposit_index.to_bits(),
|
||||
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||
});
|
||||
|
||||
// settler might be the same as account a or b
|
||||
|
@ -267,15 +239,15 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
|
||||
let (settler_token_position, settler_token_raw_index, _) =
|
||||
settler.ensure_token_position(settle_token_index)?;
|
||||
let settler_token_position_active = bank.deposit(settler_token_position, fee, now_ts)?;
|
||||
let settler_token_position_active = settle_bank.deposit(settler_token_position, fee, now_ts)?;
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
mango_group: ctx.accounts.group.key(),
|
||||
mango_account: ctx.accounts.settler.key(),
|
||||
token_index: settler_token_position.token_index,
|
||||
indexed_position: settler_token_position.indexed_position.to_bits(),
|
||||
deposit_index: bank.deposit_index.to_bits(),
|
||||
borrow_index: bank.borrow_index.to_bits(),
|
||||
deposit_index: settle_bank.deposit_index.to_bits(),
|
||||
borrow_index: settle_bank.borrow_index.to_bits(),
|
||||
});
|
||||
|
||||
if !settler_token_position_active {
|
||||
|
@ -296,3 +268,41 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
msg!("settled pnl = {}, fee = {}", settlement, fee);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn compute_settle_fee(
|
||||
perp_market: &PerpMarket,
|
||||
source_init_health: I80F48,
|
||||
source_maint_health: I80F48,
|
||||
settlement: I80F48,
|
||||
) -> Result<I80F48> {
|
||||
// A percentage fee is paid to the settler when the source account's health is low.
|
||||
// That's because the settlement could avoid it getting liquidated: settling will
|
||||
// increase its health by actualizing positive perp pnl.
|
||||
let low_health_fee = if source_init_health < 0 {
|
||||
let fee_fraction = I80F48::from_num(perp_market.settle_fee_fraction_low_health);
|
||||
if source_maint_health < 0 {
|
||||
cm!(settlement * fee_fraction)
|
||||
} else {
|
||||
cm!(settlement
|
||||
* fee_fraction
|
||||
* (-source_init_health / (source_maint_health - source_init_health)))
|
||||
}
|
||||
} else {
|
||||
I80F48::ZERO
|
||||
};
|
||||
|
||||
// The settler receives a flat fee
|
||||
let flat_fee = I80F48::from_num(perp_market.settle_fee_flat);
|
||||
|
||||
// Fees only apply when the settlement is large enough
|
||||
let fee = if settlement >= perp_market.settle_fee_amount_threshold {
|
||||
cm!(low_health_fee + flat_fee).min(settlement)
|
||||
} else {
|
||||
I80F48::ZERO
|
||||
};
|
||||
|
||||
// Safety check to prevent any accidental negative transfer
|
||||
require!(fee >= 0, MangoError::SettlementAmountMustBePositive);
|
||||
|
||||
Ok(fee)
|
||||
}
|
||||
|
|
|
@ -89,17 +89,18 @@ pub fn token_liq_bankruptcy(
|
|||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
|
||||
require_msg_typed!(
|
||||
!liqor.fixed.being_liquidated(),
|
||||
MangoError::BeingLiquidated,
|
||||
"liqor account"
|
||||
);
|
||||
|
||||
let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
|
||||
|
||||
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
||||
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
||||
.context("create liqee health cache")?;
|
||||
require!(
|
||||
!liqee_health_cache.has_liquidatable_assets(),
|
||||
MangoError::IsNotBankrupt
|
||||
);
|
||||
liqee_health_cache.require_after_phase2_liquidation()?;
|
||||
liqee.fixed.set_being_liquidated(true);
|
||||
|
||||
let (liab_bank, liab_price, opt_quote_bank_and_price) =
|
||||
|
@ -244,6 +245,7 @@ pub fn token_liq_bankruptcy(
|
|||
// Socialize loss if there's more loss and noone else could use the
|
||||
// insurance fund to cover it.
|
||||
let mut socialized_loss = I80F48::ZERO;
|
||||
let starting_deposit_index = liab_deposit_index;
|
||||
if insurance_fund_exhausted && remaining_liab_loss.is_positive() {
|
||||
// find the total deposits
|
||||
let mut indexed_total_deposits = I80F48::ZERO;
|
||||
|
@ -318,7 +320,9 @@ pub fn token_liq_bankruptcy(
|
|||
liab_price: liab_price.to_bits(),
|
||||
insurance_token_index: QUOTE_TOKEN_INDEX,
|
||||
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
||||
socialized_loss: socialized_loss.to_bits()
|
||||
socialized_loss: socialized_loss.to_bits(),
|
||||
starting_liab_deposit_index: starting_deposit_index.to_bits(),
|
||||
ending_liab_deposit_index: liab_deposit_index.to_bits()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -50,7 +50,11 @@ pub fn token_liq_with_token(
|
|||
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated);
|
||||
require_msg_typed!(
|
||||
!liqor.fixed.being_liquidated(),
|
||||
MangoError::BeingLiquidated,
|
||||
"liqor account"
|
||||
);
|
||||
|
||||
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
|
||||
|
||||
|
@ -58,6 +62,7 @@ pub fn token_liq_with_token(
|
|||
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
||||
.context("create liqee health cache")?;
|
||||
let init_health = liqee_health_cache.health(HealthType::Init);
|
||||
liqee_health_cache.require_after_phase1_liquidation()?;
|
||||
|
||||
// Once maint_health falls below 0, we want to start liquidating,
|
||||
// we want to allow liquidation to continue until init_health is positive,
|
||||
|
|
|
@ -720,11 +720,11 @@ pub mod mango_v4 {
|
|||
instructions::perp_liq_force_cancel_orders(ctx, limit)
|
||||
}
|
||||
|
||||
pub fn perp_liq_bankruptcy(
|
||||
ctx: Context<PerpLiqBankruptcy>,
|
||||
pub fn perp_liq_quote_and_bankruptcy(
|
||||
ctx: Context<PerpLiqQuoteAndBankruptcy>,
|
||||
max_liab_transfer: u64,
|
||||
) -> Result<()> {
|
||||
instructions::perp_liq_bankruptcy(ctx, max_liab_transfer)
|
||||
instructions::perp_liq_quote_and_bankruptcy(ctx, max_liab_transfer)
|
||||
}
|
||||
|
||||
pub fn alt_set(ctx: Context<AltSet>, index: u8) -> Result<()> {
|
||||
|
|
|
@ -8,14 +8,13 @@ use borsh::BorshSerialize;
|
|||
pub fn emit_perp_balances(
|
||||
mango_group: Pubkey,
|
||||
mango_account: Pubkey,
|
||||
market_index: u16,
|
||||
pp: &PerpPosition,
|
||||
pm: &PerpMarket,
|
||||
) {
|
||||
emit!(PerpBalanceLog {
|
||||
mango_group,
|
||||
mango_account,
|
||||
market_index,
|
||||
market_index: pm.perp_market_index,
|
||||
base_position: pp.base_position_lots(),
|
||||
quote_position: pp.quote_position_native().to_bits(),
|
||||
long_settled_funding: pp.long_settled_funding.to_bits(),
|
||||
|
@ -212,6 +211,8 @@ pub struct TokenLiqBankruptcyLog {
|
|||
pub insurance_token_index: u16,
|
||||
pub insurance_transfer: i128,
|
||||
pub socialized_loss: i128,
|
||||
pub starting_liab_deposit_index: i128,
|
||||
pub ending_liab_deposit_index: i128,
|
||||
}
|
||||
|
||||
#[event]
|
||||
|
@ -286,6 +287,19 @@ pub struct PerpLiqBankruptcyLog {
|
|||
pub perp_market_index: u16,
|
||||
pub insurance_transfer: i128,
|
||||
pub socialized_loss: i128,
|
||||
pub starting_long_funding: i128,
|
||||
pub starting_short_funding: i128,
|
||||
pub ending_long_funding: i128,
|
||||
pub ending_short_funding: i128,
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct PerpLiqQuoteAndBankruptcyLog {
|
||||
pub mango_group: Pubkey,
|
||||
pub liqee: Pubkey,
|
||||
pub liqor: Pubkey,
|
||||
pub perp_market_index: u16,
|
||||
pub settlement: i128,
|
||||
}
|
||||
|
||||
#[event]
|
||||
|
|
|
@ -269,7 +269,7 @@ async fn derive_liquidation_remaining_account_metas(
|
|||
|
||||
let perp_markets: Vec<Pubkey> = liqee
|
||||
.active_perp_positions()
|
||||
.chain(liqee.active_perp_positions())
|
||||
.chain(liqor.active_perp_positions())
|
||||
.map(|perp| get_perp_market_address_by_index(liqee.fixed.group, perp.market_index))
|
||||
.unique()
|
||||
.collect();
|
||||
|
@ -339,6 +339,17 @@ pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank:
|
|||
native.to_num::<f64>()
|
||||
}
|
||||
|
||||
pub async fn account_init_health(solana: &SolanaCookie, account: Pubkey) -> f64 {
|
||||
send_tx(solana, ComputeAccountDataInstruction { account })
|
||||
.await
|
||||
.unwrap();
|
||||
let health_data = solana
|
||||
.program_log_events::<mango_v4::events::MangoAccountData>()
|
||||
.pop()
|
||||
.unwrap();
|
||||
health_data.init_health.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) {
|
||||
|
@ -1036,6 +1047,96 @@ impl ClientInstruction for TokenDeregisterInstruction {
|
|||
}
|
||||
}
|
||||
|
||||
fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
|
||||
mango_v4::instruction::TokenEdit {
|
||||
oracle_opt: None,
|
||||
oracle_config_opt: None,
|
||||
group_insurance_fund_opt: None,
|
||||
interest_rate_params_opt: None,
|
||||
loan_fee_rate_opt: None,
|
||||
loan_origination_fee_rate_opt: None,
|
||||
maint_asset_weight_opt: None,
|
||||
init_asset_weight_opt: None,
|
||||
maint_liab_weight_opt: None,
|
||||
init_liab_weight_opt: None,
|
||||
liquidation_fee_opt: None,
|
||||
stable_price_delay_interval_seconds_opt: None,
|
||||
stable_price_delay_growth_limit_opt: None,
|
||||
stable_price_growth_limit_opt: None,
|
||||
min_vault_to_deposits_ratio_opt: None,
|
||||
net_borrow_limit_per_window_quote_opt: None,
|
||||
net_borrow_limit_window_size_ts_opt: None,
|
||||
borrow_weight_scale_start_quote_opt: None,
|
||||
deposit_weight_scale_start_quote_opt: None,
|
||||
reset_stable_price: false,
|
||||
reset_net_borrow_limit: false,
|
||||
reduce_only_opt: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TokenEditWeights {
|
||||
pub group: Pubkey,
|
||||
pub admin: TestKeypair,
|
||||
pub mint: Pubkey,
|
||||
|
||||
pub maint_asset_weight: f32,
|
||||
pub maint_liab_weight: f32,
|
||||
pub init_asset_weight: f32,
|
||||
pub init_liab_weight: f32,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for TokenEditWeights {
|
||||
type Accounts = mango_v4::accounts::TokenEdit;
|
||||
type Instruction = mango_v4::instruction::TokenEdit;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
|
||||
let mint_info_key = Pubkey::find_program_address(
|
||||
&[
|
||||
b"MintInfo".as_ref(),
|
||||
self.group.as_ref(),
|
||||
self.mint.as_ref(),
|
||||
],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
||||
|
||||
let instruction = Self::Instruction {
|
||||
init_asset_weight_opt: Some(self.init_asset_weight),
|
||||
init_liab_weight_opt: Some(self.init_liab_weight),
|
||||
maint_asset_weight_opt: Some(self.maint_asset_weight),
|
||||
maint_liab_weight_opt: Some(self.maint_liab_weight),
|
||||
..token_edit_instruction_default()
|
||||
};
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
admin: self.admin.pubkey(),
|
||||
mint_info: mint_info_key,
|
||||
oracle: mint_info.oracle,
|
||||
};
|
||||
|
||||
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
||||
instruction
|
||||
.accounts
|
||||
.extend(mint_info.banks().iter().map(|&k| AccountMeta {
|
||||
pubkey: k,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}));
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<TestKeypair> {
|
||||
vec![self.admin]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TokenResetStablePriceModel {
|
||||
pub group: Pubkey,
|
||||
pub admin: TestKeypair,
|
||||
|
@ -1064,28 +1165,9 @@ impl ClientInstruction for TokenResetStablePriceModel {
|
|||
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
||||
|
||||
let instruction = Self::Instruction {
|
||||
oracle_opt: None,
|
||||
oracle_config_opt: None,
|
||||
group_insurance_fund_opt: None,
|
||||
interest_rate_params_opt: None,
|
||||
loan_fee_rate_opt: None,
|
||||
loan_origination_fee_rate_opt: None,
|
||||
maint_asset_weight_opt: None,
|
||||
init_asset_weight_opt: None,
|
||||
maint_liab_weight_opt: None,
|
||||
init_liab_weight_opt: None,
|
||||
liquidation_fee_opt: None,
|
||||
stable_price_delay_interval_seconds_opt: None,
|
||||
stable_price_delay_growth_limit_opt: None,
|
||||
stable_price_growth_limit_opt: None,
|
||||
min_vault_to_deposits_ratio_opt: None,
|
||||
net_borrow_limit_per_window_quote_opt: None,
|
||||
net_borrow_limit_window_size_ts_opt: None,
|
||||
borrow_weight_scale_start_quote_opt: None,
|
||||
deposit_weight_scale_start_quote_opt: None,
|
||||
reset_stable_price: true,
|
||||
reset_net_borrow_limit: false,
|
||||
reduce_only_opt: None,
|
||||
..token_edit_instruction_default()
|
||||
};
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
|
@ -1142,28 +1224,11 @@ impl ClientInstruction for TokenResetNetBorrows {
|
|||
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
||||
|
||||
let instruction = Self::Instruction {
|
||||
oracle_opt: None,
|
||||
oracle_config_opt: None,
|
||||
group_insurance_fund_opt: None,
|
||||
interest_rate_params_opt: None,
|
||||
loan_fee_rate_opt: None,
|
||||
loan_origination_fee_rate_opt: None,
|
||||
maint_asset_weight_opt: None,
|
||||
init_asset_weight_opt: None,
|
||||
maint_liab_weight_opt: None,
|
||||
init_liab_weight_opt: None,
|
||||
liquidation_fee_opt: None,
|
||||
stable_price_delay_interval_seconds_opt: None,
|
||||
stable_price_delay_growth_limit_opt: None,
|
||||
stable_price_growth_limit_opt: None,
|
||||
min_vault_to_deposits_ratio_opt: self.min_vault_to_deposits_ratio_opt,
|
||||
net_borrow_limit_per_window_quote_opt: self.net_borrow_limit_per_window_quote_opt,
|
||||
net_borrow_limit_window_size_ts_opt: self.net_borrow_limit_window_size_ts_opt,
|
||||
borrow_weight_scale_start_quote_opt: None,
|
||||
deposit_weight_scale_start_quote_opt: None,
|
||||
reset_stable_price: false,
|
||||
reset_net_borrow_limit: true,
|
||||
reduce_only_opt: None,
|
||||
..token_edit_instruction_default()
|
||||
};
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
|
@ -1217,28 +1282,8 @@ impl ClientInstruction for TokenMakeReduceOnly {
|
|||
let mint_info: MintInfo = account_loader.load(&mint_info_key).await.unwrap();
|
||||
|
||||
let instruction = Self::Instruction {
|
||||
oracle_opt: None,
|
||||
oracle_config_opt: None,
|
||||
group_insurance_fund_opt: None,
|
||||
interest_rate_params_opt: None,
|
||||
loan_fee_rate_opt: None,
|
||||
loan_origination_fee_rate_opt: None,
|
||||
maint_asset_weight_opt: None,
|
||||
init_asset_weight_opt: None,
|
||||
maint_liab_weight_opt: None,
|
||||
init_liab_weight_opt: None,
|
||||
liquidation_fee_opt: None,
|
||||
stable_price_delay_interval_seconds_opt: None,
|
||||
stable_price_delay_growth_limit_opt: None,
|
||||
stable_price_growth_limit_opt: None,
|
||||
min_vault_to_deposits_ratio_opt: None,
|
||||
net_borrow_limit_per_window_quote_opt: None,
|
||||
net_borrow_limit_window_size_ts_opt: None,
|
||||
borrow_weight_scale_start_quote_opt: None,
|
||||
deposit_weight_scale_start_quote_opt: None,
|
||||
reset_stable_price: false,
|
||||
reset_net_borrow_limit: false,
|
||||
reduce_only_opt: Some(true),
|
||||
..token_edit_instruction_default()
|
||||
};
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
|
@ -3361,7 +3406,7 @@ impl ClientInstruction for PerpLiqBasePositionInstruction {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct PerpLiqBankruptcyInstruction {
|
||||
pub struct PerpLiqQuoteAndBankruptcyInstruction {
|
||||
pub liqor: Pubkey,
|
||||
pub liqor_owner: TestKeypair,
|
||||
pub liqee: Pubkey,
|
||||
|
@ -3369,9 +3414,9 @@ pub struct PerpLiqBankruptcyInstruction {
|
|||
pub max_liab_transfer: u64,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for PerpLiqBankruptcyInstruction {
|
||||
type Accounts = mango_v4::accounts::PerpLiqBankruptcy;
|
||||
type Instruction = mango_v4::instruction::PerpLiqBankruptcy;
|
||||
impl ClientInstruction for PerpLiqQuoteAndBankruptcyInstruction {
|
||||
type Accounts = mango_v4::accounts::PerpLiqQuoteAndBankruptcy;
|
||||
type Instruction = mango_v4::instruction::PerpLiqQuoteAndBankruptcy;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
|
@ -3416,10 +3461,11 @@ impl ClientInstruction for PerpLiqBankruptcyInstruction {
|
|||
|
||||
let accounts = Self::Accounts {
|
||||
group: group_key,
|
||||
perp_market: self.perp_market,
|
||||
liqor: self.liqor,
|
||||
liqor_owner: self.liqor_owner.pubkey(),
|
||||
liqee: self.liqee,
|
||||
perp_market: self.perp_market,
|
||||
oracle: perp_market.oracle,
|
||||
settle_bank: quote_mint_info.first_bank(),
|
||||
settle_vault: quote_mint_info.first_vault(),
|
||||
settle_oracle: quote_mint_info.oracle,
|
||||
|
|
|
@ -136,14 +136,7 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
//
|
||||
// TEST: Compute the account health
|
||||
//
|
||||
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);
|
||||
assert_eq!(account_init_health(solana, account).await.round(), 60.0);
|
||||
|
||||
//
|
||||
// TEST: Withdraw funds
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use anchor_lang::prelude::Pubkey;
|
||||
use fixed::types::I80F48;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::transport::TransportError;
|
||||
|
||||
use mango_v4::state::*;
|
||||
use mango_v4::state::{PerpMarketIndex, *};
|
||||
use program_test::*;
|
||||
|
||||
use mango_setup::*;
|
||||
|
@ -180,7 +181,8 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportError> {
|
||||
let test_builder = TestContextBuilder::new();
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(100_000); // PerpLiqQuoteAndBankruptcy takes a lot of CU
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
|
@ -306,10 +308,6 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
|||
//
|
||||
// SETUP: Trade perps between accounts
|
||||
//
|
||||
// health was 1000 + 1 * 0.8 = 1000.8 before
|
||||
// after this order it is changed by -20*100*(1.4-1) = -800 for the short
|
||||
// and 20*100*(0.6-1) = -800 for the long
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
PerpPlaceOrderInstruction {
|
||||
|
@ -352,25 +350,29 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
// health was 1000 before;
|
||||
// after this order exchange it is changed by
|
||||
// 20*100*(0.6-1) = -800 for the long account0
|
||||
// 20*100*(1-1.4) = -800 for the short account1
|
||||
// (100 is base lot size)
|
||||
assert_eq!(
|
||||
account_init_health(solana, account_0).await.round(),
|
||||
1000.0 - 800.0
|
||||
);
|
||||
assert_eq!(
|
||||
account_init_health(solana, account_1).await.round(),
|
||||
1000.0 - 800.0
|
||||
);
|
||||
|
||||
//
|
||||
// SETUP: Change the oracle to make health go negative for account_0
|
||||
// perp base value decreases from 2000 * 0.6 to 2000 * 0.6 * 0.6, i.e. -480
|
||||
//
|
||||
set_bank_stub_oracle_price(solana, group, base_token, admin, 0.6).await;
|
||||
|
||||
// verify health is bad: can't withdraw
|
||||
assert!(send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: 1,
|
||||
allow_borrow: false,
|
||||
account: account_0,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[0],
|
||||
bank_index: 0,
|
||||
}
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
account_init_health(solana, account_0).await.round(),
|
||||
200.0 - 480.0
|
||||
);
|
||||
|
||||
//
|
||||
// TEST: Liquidate base position with limit
|
||||
|
@ -403,6 +405,13 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
|||
-20.0 * 100.0 + liq_amount,
|
||||
0.1
|
||||
));
|
||||
assert!(assert_equal(
|
||||
liqee_data.perps[0].realized_trade_pnl_native,
|
||||
liq_amount - 1000.0,
|
||||
0.1
|
||||
));
|
||||
// stable price is 1.0, so 0.2 * 1000
|
||||
assert_eq!(liqee_data.perps[0].settle_pnl_limit_realized_trade, -201);
|
||||
|
||||
//
|
||||
// TEST: Liquidate base position max
|
||||
|
@ -587,22 +596,6 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
|||
0.1
|
||||
));
|
||||
|
||||
//
|
||||
// TEST: Can't trigger perp bankruptcy yet, account_1 isn't bankrupt
|
||||
//
|
||||
assert!(send_tx(
|
||||
solana,
|
||||
PerpLiqBankruptcyInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account_1,
|
||||
perp_market,
|
||||
max_liab_transfer: u64::MAX,
|
||||
}
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
//
|
||||
// SETUP: We want pnl settling to cause a negative quote position,
|
||||
// thus we deposit some base token collateral. To be able to do that,
|
||||
|
@ -671,56 +664,17 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
|||
1
|
||||
);
|
||||
|
||||
/*
|
||||
Perp liquidation / bankruptcy tests temporarily disabled until further PRs have gone in.
|
||||
|
||||
//
|
||||
// TEST: Still can't trigger perp bankruptcy, account_1 has token collateral left
|
||||
//
|
||||
assert!(send_tx(
|
||||
solana,
|
||||
PerpLiqBankruptcyInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account_1,
|
||||
perp_market,
|
||||
max_liab_transfer: u64::MAX,
|
||||
}
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
//
|
||||
// SETUP: Liquidate token collateral
|
||||
// TEST: Can liquidate/bankruptcy away remaining negative pnl
|
||||
//
|
||||
let liqee_before = solana.get_account::<MangoAccount>(account_1).await;
|
||||
let liqor_before = solana.get_account::<MangoAccount>(liqor).await;
|
||||
let liqee_settle_limit_before = liqee_before.perps[0]
|
||||
.available_settle_limit(&perp_market_data)
|
||||
.0;
|
||||
send_tx(
|
||||
solana,
|
||||
TokenLiqWithTokenInstruction {
|
||||
liqee: account_1,
|
||||
liqor: liqor,
|
||||
liqor_owner: owner,
|
||||
asset_token_index: base_token.index,
|
||||
liab_token_index: quote_token.index,
|
||||
max_liab_transfer: I80F48::MAX,
|
||||
asset_bank_index: 0,
|
||||
liab_bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
account_position(solana, account_1, quote_token.bank).await,
|
||||
0
|
||||
);
|
||||
assert!(account_position_closed(solana, account_1, base_token.bank).await);
|
||||
|
||||
//
|
||||
// TEST: Now perp-bankruptcy will work, eat the insurance vault and socialize losses
|
||||
//
|
||||
let liqor_before = account_position_f64(solana, liqor, quote_token.bank).await;
|
||||
send_tx(
|
||||
solana,
|
||||
PerpLiqBankruptcyInstruction {
|
||||
PerpLiqQuoteAndBankruptcyInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account_1,
|
||||
|
@ -730,29 +684,47 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let liqee_after = solana.get_account::<MangoAccount>(account_1).await;
|
||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||
let quote_bank = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||
|
||||
// the amount of spot the liqor received: full insurance fund, plus what was still settleable
|
||||
let liq_spot_amount = insurance_vault_funding as f64 + (-liqee_settle_limit_before) as f64;
|
||||
// the amount of perp quote transfered
|
||||
let liq_perp_quote_amount =
|
||||
(insurance_vault_funding as f64) / 1.05 + (-liqee_settle_limit_before) as f64;
|
||||
|
||||
// insurance fund was depleted and the liqor received it
|
||||
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
|
||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||
let quote_bank = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||
assert!(assert_equal(
|
||||
liqor_data.tokens[0].native("e_bank),
|
||||
liqor_before + insurance_vault_funding as f64,
|
||||
liqor_before.tokens[0].native("e_bank).to_num::<f64>() + liq_spot_amount,
|
||||
0.1
|
||||
));
|
||||
|
||||
// liqee's position is gone
|
||||
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
|
||||
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
|
||||
// liqor took over the max possible negative pnl
|
||||
assert!(assert_equal(
|
||||
liqee_data.perps[0].quote_position_native(),
|
||||
0.0,
|
||||
liqor_data.perps[0].quote_position_native(),
|
||||
liqor_before.perps[0]
|
||||
.quote_position_native()
|
||||
.to_num::<f64>()
|
||||
- liq_perp_quote_amount,
|
||||
0.1
|
||||
));
|
||||
|
||||
// liqee exited liquidation
|
||||
assert!(account_init_health(solana, account_1).await >= 0.0);
|
||||
assert_eq!(liqee_after.being_liquidated, 0);
|
||||
|
||||
// the remainder got socialized via funding payments
|
||||
let socialized_amount = -remaining_pnl - 100.0 / 1.05;
|
||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||
let pnl_before = liqee_before.perps[0]
|
||||
.pnl_for_price(&perp_market, I80F48::ONE)
|
||||
.unwrap();
|
||||
let pnl_after = liqee_after.perps[0]
|
||||
.pnl_for_price(&perp_market, I80F48::ONE)
|
||||
.unwrap();
|
||||
let socialized_amount = (pnl_after - pnl_before).to_num::<f64>() - liq_perp_quote_amount;
|
||||
assert!(assert_equal(
|
||||
perp_market.long_funding,
|
||||
socialized_amount / 20.0,
|
||||
|
@ -763,7 +735,466 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
|
|||
-socialized_amount / 20.0,
|
||||
0.1
|
||||
));
|
||||
*/
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> {
|
||||
let mut test_builder = TestContextBuilder::new();
|
||||
test_builder.test().set_compute_max_units(200_000); // PerpLiqQuoteAndBankruptcy takes a lot of CU
|
||||
let context = test_builder.start_default().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = TestKeypair::new();
|
||||
let owner = context.users[0].key;
|
||||
let payer = context.users[1].key;
|
||||
let mints = &context.mints[0..3];
|
||||
let payer_mint_accounts = &context.users[1].token_accounts[0..3];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let GroupWithTokens {
|
||||
group,
|
||||
tokens,
|
||||
insurance_vault,
|
||||
..
|
||||
} = GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints: mints.to_vec(),
|
||||
zero_token_is_quote: true,
|
||||
..GroupWithTokensConfig::default()
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
TokenEditWeights {
|
||||
group,
|
||||
admin,
|
||||
mint: mints[2].pubkey,
|
||||
maint_liab_weight: 1.0,
|
||||
maint_asset_weight: 1.0,
|
||||
init_liab_weight: 1.0,
|
||||
init_asset_weight: 1.0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fund_insurance = |amount: u64| async move {
|
||||
let mut tx = ClientTransaction::new(solana);
|
||||
tx.add_instruction_direct(
|
||||
spl_token::instruction::transfer(
|
||||
&spl_token::ID,
|
||||
&payer_mint_accounts[0],
|
||||
&insurance_vault,
|
||||
&payer.pubkey(),
|
||||
&[&payer.pubkey()],
|
||||
amount,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
tx.add_signer(payer);
|
||||
tx.send().await.unwrap();
|
||||
};
|
||||
|
||||
let quote_token = &tokens[0]; // USDC, 1/1 weights, price 1, never changed
|
||||
let base_token = &tokens[1]; // used for perp market
|
||||
let collateral_token = &tokens[2]; // used for adjusting account health
|
||||
|
||||
// deposit some funds, to the vaults aren't empty
|
||||
let liqor = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
250,
|
||||
&context.users[1],
|
||||
mints,
|
||||
10000,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
// all perp markets used here default to price = 1.0, base_lot_size = 100
|
||||
let price_lots = 100;
|
||||
|
||||
let context_ref = &context;
|
||||
let mut perp_market_index: PerpMarketIndex = 0;
|
||||
let setup_perp_inner = |perp_market_index: PerpMarketIndex,
|
||||
health: i64,
|
||||
pnl: i64,
|
||||
settle_limit: i64| async move {
|
||||
// price used later to produce negative pnl with a short:
|
||||
// doubling the price leads to -100 pnl
|
||||
let adj_price = 1.0 + pnl as f64 / -100.0;
|
||||
let adj_price_lots = (price_lots as f64 * adj_price) as i64;
|
||||
|
||||
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
|
||||
solana,
|
||||
PerpCreateMarketInstruction {
|
||||
group,
|
||||
admin,
|
||||
payer,
|
||||
perp_market_index,
|
||||
quote_lot_size: 1,
|
||||
base_lot_size: 100,
|
||||
maint_asset_weight: 0.8,
|
||||
init_asset_weight: 0.6,
|
||||
maint_liab_weight: 1.2,
|
||||
init_liab_weight: 1.4,
|
||||
liquidation_fee: 0.05,
|
||||
maker_fee: 0.0,
|
||||
taker_fee: 0.0,
|
||||
group_insurance_fund: true,
|
||||
// adjust this factur such that we get the desired settle limit in the end
|
||||
settle_pnl_limit_factor: (settle_limit as f32 + 0.1).min(0.0)
|
||||
/ (-1.0 * 100.0 * adj_price) as f32,
|
||||
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
|
||||
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 1.0).await;
|
||||
set_bank_stub_oracle_price(solana, group, &collateral_token, admin, 1.0).await;
|
||||
|
||||
//
|
||||
// SETUP: accounts
|
||||
//
|
||||
let deposit_amount = 1000;
|
||||
let helper_account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
perp_market_index as u32 * 2,
|
||||
&context_ref.users[1],
|
||||
&mints[2..3],
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
let account = create_funded_account(
|
||||
&solana,
|
||||
group,
|
||||
owner,
|
||||
perp_market_index as u32 * 2 + 1,
|
||||
&context_ref.users[1],
|
||||
&mints[2..3],
|
||||
deposit_amount,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
//
|
||||
// SETUP: Trade perps between accounts twice to generate pnl, settle_limit
|
||||
//
|
||||
let mut tx = ClientTransaction::new(solana);
|
||||
tx.add_instruction(PerpPlaceOrderInstruction {
|
||||
account: helper_account,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_lots,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 0,
|
||||
reduce_only: false,
|
||||
})
|
||||
.await;
|
||||
tx.add_instruction(PerpPlaceOrderInstruction {
|
||||
account: account,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Ask,
|
||||
price_lots,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 0,
|
||||
reduce_only: false,
|
||||
})
|
||||
.await;
|
||||
tx.add_instruction(PerpConsumeEventsInstruction {
|
||||
perp_market,
|
||||
mango_accounts: vec![account, helper_account],
|
||||
})
|
||||
.await;
|
||||
tx.send().await.unwrap();
|
||||
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, adj_price).await;
|
||||
let mut tx = ClientTransaction::new(solana);
|
||||
tx.add_instruction(PerpPlaceOrderInstruction {
|
||||
account: helper_account,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Ask,
|
||||
price_lots: adj_price_lots,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 0,
|
||||
reduce_only: false,
|
||||
})
|
||||
.await;
|
||||
tx.add_instruction(PerpPlaceOrderInstruction {
|
||||
account: account,
|
||||
perp_market,
|
||||
owner,
|
||||
side: Side::Bid,
|
||||
price_lots: adj_price_lots,
|
||||
max_base_lots: 1,
|
||||
max_quote_lots: i64::MAX,
|
||||
client_order_id: 0,
|
||||
reduce_only: false,
|
||||
})
|
||||
.await;
|
||||
tx.add_instruction(PerpConsumeEventsInstruction {
|
||||
perp_market,
|
||||
mango_accounts: vec![account, helper_account],
|
||||
})
|
||||
.await;
|
||||
tx.send().await.unwrap();
|
||||
|
||||
set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 1.0).await;
|
||||
|
||||
// Adjust target health:
|
||||
// full health = 1000 * collat price * 1.0 + pnl
|
||||
set_bank_stub_oracle_price(
|
||||
solana,
|
||||
group,
|
||||
&collateral_token,
|
||||
admin,
|
||||
(health - pnl) as f64 / 1000.0,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Verify we got it right
|
||||
let account_data = solana.get_account::<MangoAccount>(account).await;
|
||||
assert_eq!(account_data.perps[0].quote_position_native(), pnl);
|
||||
assert_eq!(
|
||||
account_data.perps[0].settle_pnl_limit_realized_trade,
|
||||
settle_limit
|
||||
);
|
||||
assert_eq!(
|
||||
account_init_health(solana, account).await.round(),
|
||||
health as f64
|
||||
);
|
||||
|
||||
(perp_market, account)
|
||||
};
|
||||
let mut setup_perp = |health: i64, pnl: i64, settle_limit: i64| {
|
||||
let out = setup_perp_inner(perp_market_index, health, pnl, settle_limit);
|
||||
perp_market_index += 1;
|
||||
out
|
||||
};
|
||||
|
||||
let limit_prec = |f: f64| (f * 1000.0).round() / 1000.0;
|
||||
|
||||
let liq_event_amounts = || {
|
||||
let settlement = solana
|
||||
.program_log_events::<mango_v4::logs::PerpLiqQuoteAndBankruptcyLog>()
|
||||
.pop()
|
||||
.map(|v| limit_prec(I80F48::from_bits(v.settlement).to_num::<f64>()))
|
||||
.unwrap_or(0.0);
|
||||
let (insur, loss) = solana
|
||||
.program_log_events::<mango_v4::logs::PerpLiqBankruptcyLog>()
|
||||
.pop()
|
||||
.map(|v| {
|
||||
(
|
||||
I80F48::from_bits(v.insurance_transfer).to_num::<u64>(),
|
||||
limit_prec(I80F48::from_bits(v.socialized_loss).to_num::<f64>()),
|
||||
)
|
||||
})
|
||||
.unwrap_or((0, 0.0));
|
||||
(settlement, insur, loss)
|
||||
};
|
||||
|
||||
let liqor_info = |perp_market: Pubkey| async move {
|
||||
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
|
||||
let liqor_perp = liqor_data
|
||||
.perps
|
||||
.iter()
|
||||
.find(|p| p.market_index == perp_market.perp_market_index)
|
||||
.unwrap()
|
||||
.clone();
|
||||
(liqor_data, liqor_perp)
|
||||
};
|
||||
|
||||
{
|
||||
let (perp_market, account) = setup_perp(-28, -50, -10).await;
|
||||
let liqor_quote_before = account_position(solana, liqor, quote_token.bank).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpLiqQuoteAndBankruptcyInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account,
|
||||
perp_market,
|
||||
max_liab_transfer: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(liq_event_amounts(), (1.0, 0, 0.0));
|
||||
|
||||
assert_eq!(
|
||||
account_position(solana, account, quote_token.bank).await,
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, liqor, quote_token.bank).await,
|
||||
liqor_quote_before + 1
|
||||
);
|
||||
let acc_data = solana.get_account::<MangoAccount>(account).await;
|
||||
assert_eq!(acc_data.perps[0].quote_position_native(), -49);
|
||||
assert_eq!(acc_data.being_liquidated, 1);
|
||||
let (_liqor_data, liqor_perp) = liqor_info(perp_market).await;
|
||||
assert_eq!(liqor_perp.quote_position_native(), -1);
|
||||
}
|
||||
|
||||
{
|
||||
let (perp_market, account) = setup_perp(-28, -50, -10).await;
|
||||
fund_insurance(2).await;
|
||||
let liqor_quote_before = account_position(solana, liqor, quote_token.bank).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpLiqQuoteAndBankruptcyInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account,
|
||||
perp_market,
|
||||
max_liab_transfer: 11,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(liq_event_amounts(), (10.0, 2, 27.0));
|
||||
|
||||
assert_eq!(
|
||||
account_position(solana, account, quote_token.bank).await,
|
||||
-10
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, liqor, quote_token.bank).await,
|
||||
liqor_quote_before + 12
|
||||
);
|
||||
let acc_data = solana.get_account::<MangoAccount>(account).await;
|
||||
assert!(assert_equal(
|
||||
acc_data.perps[0].quote_position_native(),
|
||||
-50.0 + 11.0 + 27.0,
|
||||
0.1
|
||||
));
|
||||
assert_eq!(acc_data.being_liquidated, 0);
|
||||
let (_liqor_data, liqor_perp) = liqor_info(perp_market).await;
|
||||
assert_eq!(liqor_perp.quote_position_native(), -11);
|
||||
}
|
||||
|
||||
{
|
||||
let (perp_market, account) = setup_perp(-28, -50, -10).await;
|
||||
fund_insurance(5).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpLiqQuoteAndBankruptcyInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account,
|
||||
perp_market,
|
||||
max_liab_transfer: 16,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
liq_event_amounts(),
|
||||
(10.0, 5, limit_prec(28.0 - 5.0 / 1.05))
|
||||
);
|
||||
}
|
||||
|
||||
// no insurance
|
||||
{
|
||||
let (perp_market, account) = setup_perp(-28, -50, -10).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpLiqQuoteAndBankruptcyInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account,
|
||||
perp_market,
|
||||
max_liab_transfer: u64::MAX,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(liq_event_amounts(), (10.0, 0, limit_prec(28.0)));
|
||||
}
|
||||
|
||||
// no settlement: no settle health
|
||||
{
|
||||
let (perp_market, account) = setup_perp(-200, -50, -10).await;
|
||||
fund_insurance(5).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpLiqQuoteAndBankruptcyInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account,
|
||||
perp_market,
|
||||
max_liab_transfer: u64::MAX,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(liq_event_amounts(), (0.0, 5, limit_prec(50.0 - 5.0 / 1.05)));
|
||||
}
|
||||
|
||||
// no settlement: no settle limit
|
||||
{
|
||||
let (perp_market, account) = setup_perp(-40, -50, 0).await;
|
||||
// no insurance
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpLiqQuoteAndBankruptcyInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account,
|
||||
perp_market,
|
||||
max_liab_transfer: u64::MAX,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(liq_event_amounts(), (0.0, 0, limit_prec(40.0)));
|
||||
}
|
||||
|
||||
// no socialized loss: fully covered by insurance fund
|
||||
{
|
||||
let (perp_market, account) = setup_perp(-40, -50, -5).await;
|
||||
fund_insurance(42).await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PerpLiqQuoteAndBankruptcyInstruction {
|
||||
liqor,
|
||||
liqor_owner: owner,
|
||||
liqee: account,
|
||||
perp_market,
|
||||
max_liab_transfer: u64::MAX,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(liq_event_amounts(), (5.0, 42, 0.0));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue