PerpLiqBankruptcy instruction

This commit is contained in:
Christian Kamm 2022-09-12 15:25:50 +02:00
parent 1c67b8ed5f
commit a41a245e24
33 changed files with 729 additions and 113 deletions

View File

@ -114,19 +114,15 @@ pub fn maybe_liquidate_account(
let health_cache =
new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok");
let maint_health = health_cache.health(HealthType::Maint);
let is_bankrupt = health_cache.is_bankrupt();
let is_liquidatable = health_cache.is_liquidatable();
if !is_liquidatable && !is_bankrupt {
if !health_cache.is_liquidatable() {
return Ok(false);
}
log::trace!(
"possible candidate: {}, with owner: {}, maint health: {}, bankrupt: {}",
"possible candidate: {}, with owner: {}, maint health: {}",
pubkey,
account.fixed.owner,
maint_health,
is_bankrupt,
);
// Fetch a fresh account and re-compute
@ -135,9 +131,13 @@ pub fn maybe_liquidate_account(
let account = account_fetcher.fetch_fresh_mango_account(pubkey)?;
let health_cache =
new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok");
if !health_cache.is_liquidatable() {
return Ok(false);
}
let maint_health = health_cache.health(HealthType::Maint);
let is_bankrupt = health_cache.is_bankrupt();
let is_liquidatable = health_cache.is_liquidatable();
let is_spot_bankrupt = health_cache.can_call_spot_bankruptcy();
let is_spot_liquidatable = health_cache.has_borrows() && !is_spot_bankrupt;
// find asset and liab tokens
let mut tokens = account
@ -218,7 +218,7 @@ pub fn maybe_liquidate_account(
sig
);
sig
} else if is_bankrupt {
} else if is_spot_bankrupt {
if tokens.is_empty() {
anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey);
}
@ -249,7 +249,7 @@ pub fn maybe_liquidate_account(
sig
);
sig
} else if is_liquidatable {
} else if is_spot_liquidatable {
let asset_token_index = tokens
.iter()
.rev()
@ -286,7 +286,6 @@ pub fn maybe_liquidate_account(
//
// TODO: log liqor's assets in UI form
// TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side
// TODO: swap inherited liabs to desired asset for liqor
//
let sig = mango_client
.liq_token_with_token(
@ -304,7 +303,11 @@ pub fn maybe_liquidate_account(
);
sig
} else {
return Ok(false);
anyhow::bail!(
"Don't know what to do with liquidatable account {}, maint_health was {}",
pubkey,
maint_health
);
};
let slot = account_fetcher.transaction_max_slot(&[txsig])?;

View File

@ -20,6 +20,7 @@ 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_place_order::*;
@ -71,6 +72,7 @@ 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_place_order;

View File

@ -62,6 +62,8 @@ pub fn perp_create_market(
min_funding: f32,
max_funding: f32,
impact_quantity: i64,
group_insurance_fund: bool,
trusted_market: bool,
) -> Result<()> {
let mut perp_market = ctx.accounts.perp_market.load_init()?;
*perp_market = PerpMarket {
@ -96,6 +98,8 @@ pub fn perp_create_market(
base_decimals,
perp_market_index,
registration_time: Clock::get()?.unix_timestamp,
group_insurance_fund: if group_insurance_fund { 1 } else { 0 },
trusted_market: if trusted_market { 1 } else { 0 },
padding0: Default::default(),
padding1: Default::default(),
padding2: Default::default(),

View File

@ -2,7 +2,6 @@ use anchor_lang::prelude::*;
use crate::error::*;
use crate::state::*;
use crate::util::checked_math as cm;
#[derive(Accounts)]
pub struct PerpDeactivatePosition<'info> {
@ -51,11 +50,7 @@ pub fn perp_deactivate_position(ctx: Context<PerpDeactivatePosition>) -> Result<
"perp position still has events on event queue"
);
account.deactivate_perp_position(perp_market.perp_market_index)?;
// Reduce the in-use-count of the settlement token
let mut token_position = account.token_position_mut(QUOTE_TOKEN_INDEX)?.0;
cm!(token_position.in_use_count -= 1);
account.deactivate_perp_position(perp_market.perp_market_index, QUOTE_TOKEN_INDEX)?;
Ok(())
}

View File

@ -33,6 +33,8 @@ pub fn perp_edit_market(
min_funding_opt: Option<f32>,
max_funding_opt: Option<f32>,
impact_quantity_opt: Option<i64>,
group_insurance_fund_opt: Option<bool>,
trusted_market_opt: Option<bool>,
) -> Result<()> {
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
@ -107,7 +109,14 @@ pub fn perp_edit_market(
// perp_market_index
// unchanged -
// quote_token_index
// registration_time
if let Some(group_insurance_fund) = group_insurance_fund_opt {
perp_market.set_elligible_for_group_insurance_fund(group_insurance_fund);
}
if let Some(trusted_market) = trusted_market_opt {
perp_market.trusted_market = if trusted_market { 1 } else { 0 };
}
Ok(())
}

View File

@ -0,0 +1,189 @@
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::state::ScanningAccountRetriever;
use crate::state::*;
use crate::util::checked_math as cm;
// 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: AccountLoaderDynamic<'info, MangoAccount>,
pub liqor_owner: Signer<'info>,
#[account(
mut,
has_one = group
)]
pub liqee: AccountLoaderDynamic<'info, MangoAccount>,
#[account(
mut,
has_one = group,
constraint = quote_bank.load()?.vault == quote_vault.key()
// address is checked at #2
)]
pub quote_bank: AccountLoader<'info, Bank>,
#[account(mut)]
pub quote_vault: Account<'info, TokenAccount>,
// 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.quote_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_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_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 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 quote_bank = ctx.accounts.quote_bank.load_mut()?;
require_eq!(quote_bank.token_index, QUOTE_TOKEN_INDEX);
require_keys_eq!(quote_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(QUOTE_TOKEN_INDEX)?;
quote_bank.deposit(liqor_quote, insurance_transfer_i80f48)?;
// transfer perp quote loss from the liqee to the liqor
let liqor_perp_position = liqor
.ensure_perp_position(perp_market.perp_market_index, QUOTE_TOKEN_INDEX)?
.0;
liqee_perp_position.change_quote_position(insurance_liab_transfer);
liqor_perp_position.change_quote_position(-insurance_liab_transfer);
}
// Socialize loss if the insurance fund is exhausted
let remaining_liab = liab_transfer - insurance_liab_transfer;
if insurance_fund_exhausted && remaining_liab.is_positive() {
perp_market.socialize_loss(-remaining_liab)?;
liqee_perp_position.change_quote_position(remaining_liab);
require_eq!(liqee_perp_position.quote_position_native(), 0);
}
// 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

@ -86,7 +86,9 @@ pub fn perp_liq_base_position(
// Fetch perp positions for accounts, creating for the liqor if needed
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
let liqor_perp_position = liqor.ensure_perp_position(perp_market_index)?.0;
let liqor_perp_position = liqor
.ensure_perp_position(perp_market_index, QUOTE_TOKEN_INDEX)?
.0;
let liqee_base_lots = liqee_perp_position.base_position_lots();
require!(

View File

@ -7,7 +7,6 @@ use crate::state::{
new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, Book, BookSide,
EventQueue, Group, OrderType, PerpMarket, Side, QUOTE_TOKEN_INDEX,
};
use crate::util::checked_math as cm;
#[derive(Accounts)]
pub struct PerpPlaceOrder<'info> {
@ -89,16 +88,7 @@ pub fn perp_place_order(
//
// Create the perp position if needed
//
if !account
.active_perp_positions()
.any(|p| p.is_active_for_market(perp_market_index))
{
account.ensure_perp_position(perp_market_index)?;
// Require that the token position for the settlement token is retained
let mut token_position = account.ensure_token_position(QUOTE_TOKEN_INDEX)?.0;
cm!(token_position.in_use_count += 1);
}
account.ensure_perp_position(perp_market_index, QUOTE_TOKEN_INDEX)?;
//
// Pre-health computation, _after_ perp position is created

View File

@ -395,6 +395,8 @@ pub mod mango_v4 {
min_funding: f32,
max_funding: f32,
impact_quantity: i64,
group_insurance_fund: bool,
trusted_market: bool,
) -> Result<()> {
instructions::perp_create_market(
ctx,
@ -414,6 +416,8 @@ pub mod mango_v4 {
max_funding,
min_funding,
impact_quantity,
group_insurance_fund,
trusted_market,
)
}
@ -433,6 +437,8 @@ pub mod mango_v4 {
min_funding_opt: Option<f32>,
max_funding_opt: Option<f32>,
impact_quantity_opt: Option<i64>,
group_insurance_fund_opt: Option<bool>,
trusted_market_opt: Option<bool>,
) -> Result<()> {
instructions::perp_edit_market(
ctx,
@ -449,6 +455,8 @@ pub mod mango_v4 {
min_funding_opt,
max_funding_opt,
impact_quantity_opt,
group_insurance_fund_opt,
trusted_market_opt,
)
}
@ -540,15 +548,12 @@ pub mod mango_v4 {
instructions::perp_liq_force_cancel_orders(ctx, limit)
}
// TODO
// perp_force_cancel_order
// liquidate_token_and_perp
// settle_* - settle_funds
// resolve_banktruptcy
pub fn perp_liq_bankruptcy(
ctx: Context<PerpLiqBankruptcy>,
max_liab_transfer: u64,
) -> Result<()> {
instructions::perp_liq_bankruptcy(ctx, max_liab_transfer)
}
pub fn alt_set(ctx: Context<AltSet>, index: u8) -> Result<()> {
instructions::alt_set(ctx, index)

View File

@ -522,6 +522,7 @@ pub struct PerpInfo {
// in health-reference-token native units, no asset/liab factor needed
pub quote: I80F48,
oracle_price: I80F48,
has_open_orders: bool,
}
impl PerpInfo {
@ -607,6 +608,7 @@ impl PerpInfo {
base,
quote,
oracle_price,
has_open_orders: perp_position.has_open_orders(),
})
}
@ -762,14 +764,20 @@ impl HealthCache {
}
pub fn has_liquidatable_assets(&self) -> bool {
let spot_liquidatable = self
.token_infos
.iter()
.any(|ti| ti.balance.is_positive() || ti.serum3_max_reserved.is_positive());
let perp_liquidatable = self
.perp_infos
.iter()
.any(|p| p.base != 0 || p.quote > ONE_NATIVE_USDC_IN_USD);
let spot_liquidatable = self.token_infos.iter().any(|ti| {
// can use token_liq_with_token
ti.balance.is_positive()
// can use serum3_liq_force_cancel_orders
|| ti.serum3_max_reserved.is_positive()
});
let perp_liquidatable = self.perp_infos.iter().any(|p| {
// can use perp_liq_base_position
p.base != 0
// can use perp_settle_pnl
|| p.quote > ONE_NATIVE_USDC_IN_USD
// can use perp_liq_force_cancel_orders
|| p.has_open_orders
});
spot_liquidatable || perp_liquidatable
}
@ -783,7 +791,7 @@ impl HealthCache {
}
#[cfg(feature = "client")]
pub fn is_bankrupt(&self) -> bool {
pub fn can_call_spot_bankruptcy(&self) -> bool {
!self.has_liquidatable_assets() && self.has_borrows()
}
@ -1302,7 +1310,7 @@ mod tests {
oo1.data().referrer_rebates_accrued = 2;
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 9, 0.2, 0.1);
let perpaccount = account.ensure_perp_position(9).unwrap().0;
let perpaccount = account.ensure_perp_position(9, 1).unwrap().0;
perpaccount.change_base_and_quote_positions(perp1.data(), 3, -I80F48::from(310u16));
perpaccount.bids_base_lots = 7;
perpaccount.asks_base_lots = 11;
@ -1495,7 +1503,7 @@ mod tests {
oo2.data().native_coin_total = testcase.oo_1_3.1;
let mut perp1 = mock_perp_market(group, oracle2.pubkey, 9, 0.2, 0.1);
let perpaccount = account.ensure_perp_position(9).unwrap().0;
let perpaccount = account.ensure_perp_position(9, 1).unwrap().0;
perpaccount.change_base_and_quote_positions(
perp1.data(),
testcase.perp1.0,
@ -1848,7 +1856,7 @@ mod tests {
let mut perp1 = mock_perp_market(group, oracle1.pubkey, 9, 0.2, 0.1);
perp1.data().long_funding = I80F48::from_num(10.1);
let perpaccount = account.ensure_perp_position(9).unwrap().0;
let perpaccount = account.ensure_perp_position(9, 1).unwrap().0;
perpaccount.change_base_and_quote_positions(perp1.data(), 10, I80F48::from(-110));
perpaccount.long_settled_funding = I80F48::from_num(10.0);

View File

@ -705,6 +705,7 @@ impl<
pub fn ensure_perp_position(
&mut self,
perp_market_index: PerpMarketIndex,
settle_token_index: TokenIndex,
) -> Result<(&mut PerpPosition, usize)> {
let mut raw_index_opt = self
.all_perp_positions()
@ -715,6 +716,9 @@ impl<
let perp_position = self.perp_position_mut_by_raw_index(raw_index);
*perp_position = PerpPosition::default();
perp_position.market_index = perp_market_index;
let mut settle_token_position = self.ensure_token_position(settle_token_index)?.0;
cm!(settle_token_position.in_use_count += 1);
}
}
if let Some(raw_index) = raw_index_opt {
@ -724,8 +728,16 @@ impl<
}
}
pub fn deactivate_perp_position(&mut self, perp_market_index: PerpMarketIndex) -> Result<()> {
pub fn deactivate_perp_position(
&mut self,
perp_market_index: PerpMarketIndex,
settle_token_index: TokenIndex,
) -> Result<()> {
self.perp_position_mut(perp_market_index)?.market_index = PerpMarketIndex::MAX;
let mut settle_token_position = self.token_position_mut(settle_token_index)?.0;
cm!(settle_token_position.in_use_count -= 1);
Ok(())
}
@ -1192,19 +1204,22 @@ mod tests {
);
{
let (pos, raw) = account.ensure_perp_position(1).unwrap();
let (pos, raw) = account.ensure_perp_position(1, 0).unwrap();
assert_eq!(raw, 0);
assert_eq!(pos.market_index, 1);
assert_eq!(account.token_position_mut(0).unwrap().0.in_use_count, 1);
}
{
let (pos, raw) = account.ensure_perp_position(7).unwrap();
let (pos, raw) = account.ensure_perp_position(7, 0).unwrap();
assert_eq!(raw, 1);
assert_eq!(pos.market_index, 7);
assert_eq!(account.token_position_mut(0).unwrap().0.in_use_count, 2);
}
{
let (pos, raw) = account.ensure_perp_position(42).unwrap();
let (pos, raw) = account.ensure_perp_position(42, 0).unwrap();
assert_eq!(raw, 2);
assert_eq!(pos.market_index, 42);
assert_eq!(account.token_position_mut(0).unwrap().0.in_use_count, 3);
}
{
@ -1219,19 +1234,21 @@ mod tests {
}
{
assert!(account.deactivate_perp_position(7).is_ok());
assert!(account.deactivate_perp_position(7, 0).is_ok());
let (pos, raw) = account.ensure_perp_position(42).unwrap();
let (pos, raw) = account.ensure_perp_position(42, 0).unwrap();
assert_eq!(raw, 2);
assert_eq!(pos.market_index, 42);
assert_eq!(account.token_position_mut(0).unwrap().0.in_use_count, 2);
let (pos, raw) = account.ensure_perp_position(8).unwrap();
let (pos, raw) = account.ensure_perp_position(8, 0).unwrap();
assert_eq!(raw, 1);
assert_eq!(pos.market_index, 8);
assert_eq!(account.token_position_mut(0).unwrap().0.in_use_count, 3);
}
assert_eq!(account.active_perp_positions().count(), 3);
assert!(account.deactivate_perp_position(1).is_ok());
assert!(account.deactivate_perp_position(1, 0).is_ok());
assert_eq!(
account.perp_position_by_raw_index(0).market_index,
PerpMarketIndex::MAX

View File

@ -420,6 +420,8 @@ mod tests {
return PerpMarket {
group: Pubkey::new_unique(),
perp_market_index: 0,
group_insurance_fund: 0,
trusted_market: 0,
name: Default::default(),
oracle: Pubkey::new_unique(),
oracle_config: OracleConfig {

View File

@ -15,7 +15,9 @@ pub mod queue;
#[cfg(test)]
mod tests {
use super::*;
use crate::state::{MangoAccount, MangoAccountValue, PerpMarket, FREE_ORDER_SLOT};
use crate::state::{
MangoAccount, MangoAccountValue, PerpMarket, FREE_ORDER_SLOT, QUOTE_TOKEN_INDEX,
};
use anchor_lang::prelude::*;
use bytemuck::Zeroable;
use fixed::types::I80F48;
@ -104,7 +106,7 @@ mod tests {
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
account
.ensure_perp_position(perp_market.perp_market_index)
.ensure_perp_position(perp_market.perp_market_index, QUOTE_TOKEN_INDEX)
.unwrap();
let quantity = 1;
@ -203,10 +205,10 @@ mod tests {
let mut maker = MangoAccountValue::from_bytes(&buffer).unwrap();
let mut taker = MangoAccountValue::from_bytes(&buffer).unwrap();
maker
.ensure_perp_position(market.perp_market_index)
.ensure_perp_position(market.perp_market_index, QUOTE_TOKEN_INDEX)
.unwrap();
taker
.ensure_perp_position(market.perp_market_index)
.ensure_perp_position(market.perp_market_index, QUOTE_TOKEN_INDEX)
.unwrap();
let maker_pk = Pubkey::new_unique();

View File

@ -26,7 +26,13 @@ pub struct PerpMarket {
/// Lookup indices
pub perp_market_index: PerpMarketIndex,
pub padding1: [u8; 4],
/// May this market contribute positive values to health?
pub trusted_market: u8,
/// Is this market covered by the group insurance fund?
pub group_insurance_fund: u8,
pub padding1: [u8; 2],
pub name: [u8; 16],
@ -109,6 +115,14 @@ impl PerpMarket {
.trim_matches(char::from(0))
}
pub fn elligible_for_group_insurance_fund(&self) -> bool {
self.group_insurance_fund == 1
}
pub fn set_elligible_for_group_insurance_fund(&mut self, v: bool) {
self.group_insurance_fund = if v { 1 } else { 0 };
}
pub fn gen_order_id(&mut self, side: Side, price: i64) -> i128 {
self.seq_num += 1;
@ -191,4 +205,24 @@ impl PerpMarket {
Side::Ask => native_price >= cm!(self.maint_asset_weight * oracle_price),
}
}
/// Socialize the loss in this account across all longs and shorts
pub fn socialize_loss(&mut self, loss: I80F48) -> Result<I80F48> {
require_gte!(0, loss);
// TODO convert into only socializing on one side
// native USDC per contract open interest
let socialized_loss = if self.open_interest == 0 {
// AUDIT: think about the following:
// This is kind of an unfortunate situation. This means socialized loss occurs on the
// last person to call settle_pnl on their profits. Any advice on better mechanism
// would be appreciated. Luckily, this will be an extremely rare situation.
I80F48::ZERO
} else {
cm!(loss / I80F48::from(self.open_interest))
};
self.long_funding -= socialized_loss;
self.short_funding += socialized_loss;
Ok(socialized_loss)
}
}

View File

@ -191,7 +191,7 @@ async fn derive_health_check_remaining_account_metas(
}
if let Some(affected_perp_market_index) = affected_perp_market_index {
adjusted_account
.ensure_perp_position(affected_perp_market_index)
.ensure_perp_position(affected_perp_market_index, QUOTE_TOKEN_INDEX)
.unwrap();
}
@ -2141,6 +2141,8 @@ pub struct PerpCreateMarketInstruction {
pub liquidation_fee: f32,
pub maker_fee: f32,
pub taker_fee: f32,
pub group_insurance_fund: bool,
pub trusted_market: bool,
}
impl PerpCreateMarketInstruction {
pub async fn with_new_book_and_queue(
@ -2191,6 +2193,8 @@ impl ClientInstruction for PerpCreateMarketInstruction {
min_funding: 0.05,
impact_quantity: 100,
base_decimals: self.base_decimals,
group_insurance_fund: self.group_insurance_fund,
trusted_market: self.trusted_market,
};
let perp_market = Pubkey::find_program_address(
@ -2759,6 +2763,81 @@ impl ClientInstruction for PerpLiqBasePositionInstruction {
}
}
pub struct PerpLiqBankruptcyInstruction {
pub liqor: Pubkey,
pub liqor_owner: TestKeypair,
pub liqee: Pubkey,
pub perp_market: Pubkey,
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;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
max_liab_transfer: self.max_liab_transfer,
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let group_key = perp_market.group;
let liqor = account_loader
.load_mango_account(&self.liqor)
.await
.unwrap();
let liqee = account_loader
.load_mango_account(&self.liqee)
.await
.unwrap();
let health_check_metas = derive_liquidation_remaining_account_metas(
&account_loader,
&liqee,
&liqor,
TokenIndex::MAX,
0,
TokenIndex::MAX,
0,
)
.await;
let group = account_loader.load::<Group>(&group_key).await.unwrap();
let quote_mint_info = Pubkey::find_program_address(
&[
b"MintInfo".as_ref(),
group_key.as_ref(),
group.insurance_mint.as_ref(),
],
&program_id,
)
.0;
let quote_mint_info: MintInfo = account_loader.load(&quote_mint_info).await.unwrap();
let accounts = Self::Accounts {
group: group_key,
perp_market: self.perp_market,
liqor: self.liqor,
liqor_owner: self.liqor_owner.pubkey(),
liqee: self.liqee,
quote_bank: quote_mint_info.first_bank(),
quote_vault: quote_mint_info.first_vault(),
insurance_vault: group.insurance_vault,
token_program: Token::id(),
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.liqor_owner]
}
}
pub struct BenchmarkInstruction {}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for BenchmarkInstruction {

View File

@ -6,10 +6,12 @@ use super::mango_client::*;
use super::solana::SolanaCookie;
use super::{send_tx, MintCookie, TestKeypair, UserCookie};
pub struct GroupWithTokensConfig<'a> {
#[derive(Default)]
pub struct GroupWithTokensConfig {
pub admin: TestKeypair,
pub payer: TestKeypair,
pub mints: &'a [MintCookie],
pub mints: Vec<MintCookie>,
pub zero_token_is_quote: bool,
}
#[derive(Clone)]
@ -29,12 +31,13 @@ pub struct GroupWithTokens {
pub tokens: Vec<Token>,
}
impl<'a> GroupWithTokensConfig<'a> {
impl<'a> GroupWithTokensConfig {
pub async fn create(self, solana: &SolanaCookie) -> GroupWithTokens {
let GroupWithTokensConfig {
admin,
payer,
mints,
zero_token_is_quote,
} = self;
let create_group_accounts = send_tx(
solana,
@ -76,6 +79,11 @@ impl<'a> GroupWithTokensConfig<'a> {
.await
.unwrap();
let token_index = index as u16;
let (iaw, maw, mlw, ilw) = if token_index == 0 && zero_token_is_quote {
(1.0, 1.0, 1.0, 1.0)
} else {
(0.6, 0.8, 1.2, 1.4)
};
let register_token_accounts = send_tx(
solana,
TokenRegisterInstruction {
@ -89,10 +97,10 @@ impl<'a> GroupWithTokensConfig<'a> {
max_rate: 1.50,
loan_origination_fee_rate: 0.0005,
loan_fee_rate: 0.0005,
maint_asset_weight: 0.8,
init_asset_weight: 0.6,
maint_liab_weight: 1.2,
init_liab_weight: 1.4,
maint_asset_weight: maw,
init_asset_weight: iaw,
maint_liab_weight: mlw,
init_liab_weight: ilw,
liquidation_fee: 0.02,
group,
admin,

View File

@ -26,7 +26,8 @@ async fn test_alt() -> Result<(), TransportError> {
let GroupWithTokens { group, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -29,7 +29,8 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
@ -301,7 +302,8 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
} = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -30,7 +30,8 @@ async fn test_basic() -> Result<(), TransportError> {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..mango_setup::GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -29,7 +29,8 @@ async fn test_delegate() -> Result<(), TransportError> {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -29,7 +29,8 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
let GroupWithTokens { group, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
@ -62,7 +63,8 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
@ -177,7 +179,8 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -29,7 +29,8 @@ async fn test_health_wrap() -> Result<(), TransportError> {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -32,7 +32,8 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
@ -184,7 +185,7 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> {
}
#[tokio::test]
async fn test_liq_perps_base_position() -> Result<(), TransportError> {
async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportError> {
let test_builder = TestContextBuilder::new();
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
@ -199,14 +200,41 @@ async fn test_liq_perps_base_position() -> Result<(), TransportError> {
// SETUP: Create a group and an account to fill the vaults
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
let GroupWithTokens {
group,
tokens,
insurance_vault,
..
} = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
zero_token_is_quote: true,
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
//let quote_token = &tokens[0];
// fund the insurance vault
let insurance_vault_funding = 100;
{
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()],
insurance_vault_funding,
)
.unwrap(),
);
tx.add_signer(payer);
tx.send().await.unwrap();
}
let quote_token = &tokens[0];
let base_token = &tokens[1];
// deposit some funds, to the vaults aren't empty
@ -241,6 +269,7 @@ async fn test_liq_perps_base_position() -> Result<(), TransportError> {
liquidation_fee: 0.05,
maker_fee: 0.0,
taker_fee: 0.0,
group_insurance_fund: true,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await
},
)
@ -291,9 +320,9 @@ async fn test_liq_perps_base_position() -> Result<(), TransportError> {
//
// SETUP: Trade perps between accounts
//
// health was 1000 * 0.6 = 600 before
// after this order it is -14*100*(1.4-1) = -560 for the short
// and 14*100*(0.6-1) = -560 for the long
// 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,
@ -303,7 +332,7 @@ async fn test_liq_perps_base_position() -> Result<(), TransportError> {
owner,
side: Side::Bid,
price_lots,
max_base_lots: 14,
max_base_lots: 20,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
@ -318,7 +347,7 @@ async fn test_liq_perps_base_position() -> Result<(), TransportError> {
owner,
side: Side::Ask,
price_lots,
max_base_lots: 14,
max_base_lots: 20,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
@ -391,10 +420,10 @@ async fn test_liq_perps_base_position() -> Result<(), TransportError> {
0.1
));
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 4);
assert_eq!(liqee_data.perps[0].base_position_lots(), 10);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
-14.0 * 100.0 + liq_amount,
-20.0 * 100.0 + liq_amount,
0.1
));
@ -445,9 +474,9 @@ async fn test_liq_perps_base_position() -> Result<(), TransportError> {
.await
.unwrap();
let liq_amount_2 = 14.0 * 100.0 * 2.0 * (1.0 + 0.05);
let liq_amount_2 = 20.0 * 100.0 * 2.0 * (1.0 + 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 - 14);
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 - 20);
assert!(assert_equal(
liqor_data.perps[0].quote_position_native(),
-liq_amount + liq_amount_2,
@ -457,7 +486,148 @@ async fn test_liq_perps_base_position() -> Result<(), TransportError> {
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
14.0 * 100.0 - liq_amount_2,
20.0 * 100.0 - liq_amount_2,
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());
//
// TEST: Can settle-pnl even though health is negative
//
send_tx(
solana,
PerpSettlePnlInstruction {
account_a: liqor,
account_b: account_1,
perp_market,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
)
.await
.unwrap();
let liqee_spot_health_before = 1000.0 + 1.0 * 2.0 * 0.8;
let remaining_pnl = 20.0 * 100.0 - liq_amount_2 + liqee_spot_health_before;
assert!(remaining_pnl < 0.0);
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
remaining_pnl,
0.1
));
assert_eq!(
account_position(solana, account_1, quote_token.bank).await,
-2
);
assert_eq!(
account_position(solana, account_1, base_token.bank).await,
1
);
//
// 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
//
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 {
liqor,
liqor_owner: owner,
liqee: account_1,
perp_market,
max_liab_transfer: u64::MAX,
},
)
.await
.unwrap();
// 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,
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);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
0.0,
0.1
));
// 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;
assert!(assert_equal(
perp_market.long_funding,
socialized_amount / 20.0,
0.1
));
assert!(assert_equal(
perp_market.short_funding,
-socialized_amount / 20.0,
0.1
));

View File

@ -31,7 +31,8 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
@ -199,7 +200,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -32,7 +32,8 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -29,7 +29,8 @@ async fn test_perp() -> Result<(), TransportError> {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -1,6 +1,7 @@
#![cfg(all(feature = "test-bpf"))]
use fixed::types::I80F48;
use mango_setup::*;
use mango_v4::{error::MangoError, state::*};
use program_test::*;
use solana_program_test::*;
@ -27,10 +28,11 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -1,6 +1,7 @@
#![cfg(all(feature = "test-bpf"))]
use fixed::types::I80F48;
use mango_setup::*;
use mango_v4::{error::MangoError, state::*};
use program_test::*;
use solana_program_test::*;
@ -25,10 +26,11 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
// SETUP: Create a group and an account
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -5,7 +5,7 @@ use solana_program_test::*;
use program_test::*;
use crate::mango_setup::*;
use mango_setup::*;
mod program_test;
@ -26,10 +26,11 @@ async fn test_position_lifetime() -> Result<()> {
// SETUP: Create a group and accounts
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -164,7 +164,8 @@ async fn test_serum_basics() -> Result<(), TransportError> {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
@ -344,7 +345,8 @@ async fn test_serum_loan_origination_fees() -> Result<(), TransportError> {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -27,7 +27,8 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> {
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;

View File

@ -1287,6 +1287,8 @@ export class MangoClient {
minFunding: number,
maxFunding: number,
impactQuantity: number,
groupInsuranceFund: boolean,
trustedMarket: boolean,
): Promise<TransactionSignature> {
const bids = new Keypair();
const asks = new Keypair();
@ -1314,6 +1316,8 @@ export class MangoClient {
minFunding,
maxFunding,
new BN(impactQuantity),
groupInsuranceFund,
trustedMarket,
)
.accounts({
group: group.publicKey,
@ -1383,6 +1387,8 @@ export class MangoClient {
minFunding: number,
maxFunding: number,
impactQuantity: number,
groupInsuranceFund: boolean,
trustedMarket: boolean,
): Promise<TransactionSignature> {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
@ -1405,6 +1411,8 @@ export class MangoClient {
minFunding,
maxFunding,
new BN(impactQuantity),
groupInsuranceFund,
trustedMarket,
)
.accounts({
group: group.publicKey,

View File

@ -2307,6 +2307,14 @@ export type MangoV4 = {
{
"name": "impactQuantity",
"type": "i64"
},
{
"name": "groupInsuranceFund",
"type": "bool"
},
{
"name": "trustedMarket",
"type": "bool"
}
]
},
@ -2409,6 +2417,18 @@ export type MangoV4 = {
"type": {
"option": "i64"
}
},
{
"name": "groupInsuranceFundOpt",
"type": {
"option": "bool"
}
},
{
"name": "trustedMarketOpt",
"type": {
"option": "bool"
}
}
]
},
@ -3734,12 +3754,26 @@ export type MangoV4 = {
],
"type": "u16"
},
{
"name": "trustedMarket",
"docs": [
"May this market contribute positive values to health?"
],
"type": "u8"
},
{
"name": "groupInsuranceFund",
"docs": [
"Is this market covered by the group insurance fund?"
],
"type": "u8"
},
{
"name": "padding1",
"type": {
"array": [
"u8",
4
2
]
}
},
@ -8310,6 +8344,14 @@ export const IDL: MangoV4 = {
{
"name": "impactQuantity",
"type": "i64"
},
{
"name": "groupInsuranceFund",
"type": "bool"
},
{
"name": "trustedMarket",
"type": "bool"
}
]
},
@ -8412,6 +8454,18 @@ export const IDL: MangoV4 = {
"type": {
"option": "i64"
}
},
{
"name": "groupInsuranceFundOpt",
"type": {
"option": "bool"
}
},
{
"name": "trustedMarketOpt",
"type": {
"option": "bool"
}
}
]
},
@ -9737,12 +9791,26 @@ export const IDL: MangoV4 = {
],
"type": "u16"
},
{
"name": "trustedMarket",
"docs": [
"May this market contribute positive values to health?"
],
"type": "u8"
},
{
"name": "groupInsuranceFund",
"docs": [
"Is this market covered by the group insurance fund?"
],
"type": "u8"
},
{
"name": "padding1",
"type": {
"array": [
"u8",
4
2
]
}
},