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:
Christian Kamm 2023-01-12 09:07:13 +01:00 committed by GitHub
parent c5d875e04d
commit 93d33edb74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1290 additions and 548 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(&quote_bank),
liqor_before + insurance_vault_funding as f64,
liqor_before.tokens[0].native(&quote_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(())
}