PerpLiqBankruptcy instruction
This commit is contained in:
parent
1c67b8ed5f
commit
a41a245e24
|
@ -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])?;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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!(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("e_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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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("e_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
|
||||
));
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue