From a41a245e24c063f3f24d0ea31742c3e234a15bfc Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 12 Sep 2022 15:25:50 +0200 Subject: [PATCH] PerpLiqBankruptcy instruction --- liquidator/src/liquidate.rs | 27 +-- programs/mango-v4/src/instructions/mod.rs | 2 + .../src/instructions/perp_create_market.rs | 4 + .../instructions/perp_deactivate_position.rs | 7 +- .../src/instructions/perp_edit_market.rs | 11 +- .../src/instructions/perp_liq_bankruptcy.rs | 189 +++++++++++++++++ .../instructions/perp_liq_base_position.rs | 4 +- .../src/instructions/perp_place_order.rs | 12 +- programs/mango-v4/src/lib.rs | 23 +- programs/mango-v4/src/state/health.rs | 32 +-- programs/mango-v4/src/state/mango_account.rs | 33 ++- .../src/state/mango_account_components.rs | 2 + programs/mango-v4/src/state/orderbook/mod.rs | 10 +- programs/mango-v4/src/state/perp_market.rs | 36 +++- .../tests/program_test/mango_client.rs | 81 ++++++- .../tests/program_test/mango_setup.rs | 22 +- programs/mango-v4/tests/test_alt.rs | 3 +- .../mango-v4/tests/test_bankrupt_tokens.rs | 6 +- programs/mango-v4/tests/test_basic.rs | 3 +- programs/mango-v4/tests/test_delegate.rs | 3 +- .../mango-v4/tests/test_health_compute.rs | 9 +- programs/mango-v4/tests/test_health_region.rs | 3 +- programs/mango-v4/tests/test_liq_perps.rs | 200 ++++++++++++++++-- programs/mango-v4/tests/test_liq_tokens.rs | 6 +- programs/mango-v4/tests/test_margin_trade.rs | 3 +- programs/mango-v4/tests/test_perp.rs | 3 +- programs/mango-v4/tests/test_perp_settle.rs | 6 +- .../mango-v4/tests/test_perp_settle_fees.rs | 6 +- .../mango-v4/tests/test_position_lifetime.rs | 7 +- programs/mango-v4/tests/test_serum.rs | 6 +- .../tests/test_token_update_index_and_rate.rs | 3 +- ts/client/src/client.ts | 8 + ts/client/src/mango_v4.ts | 72 ++++++- 33 files changed, 729 insertions(+), 113 deletions(-) create mode 100644 programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 3177cca7c..3986a830e 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -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])?; diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index 66cabeab3..a62bd0d73 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -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; diff --git a/programs/mango-v4/src/instructions/perp_create_market.rs b/programs/mango-v4/src/instructions/perp_create_market.rs index 134144f9e..e6a4410ba 100644 --- a/programs/mango-v4/src/instructions/perp_create_market.rs +++ b/programs/mango-v4/src/instructions/perp_create_market.rs @@ -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(), diff --git a/programs/mango-v4/src/instructions/perp_deactivate_position.rs b/programs/mango-v4/src/instructions/perp_deactivate_position.rs index c46fb866a..59e4de272 100644 --- a/programs/mango-v4/src/instructions/perp_deactivate_position.rs +++ b/programs/mango-v4/src/instructions/perp_deactivate_position.rs @@ -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) -> 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(()) } diff --git a/programs/mango-v4/src/instructions/perp_edit_market.rs b/programs/mango-v4/src/instructions/perp_edit_market.rs index 420015d1c..026ac6f9f 100644 --- a/programs/mango-v4/src/instructions/perp_edit_market.rs +++ b/programs/mango-v4/src/instructions/perp_edit_market.rs @@ -33,6 +33,8 @@ pub fn perp_edit_market( min_funding_opt: Option, max_funding_opt: Option, impact_quantity_opt: Option, + group_insurance_fund_opt: Option, + trusted_market_opt: Option, ) -> 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(()) } diff --git a/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs new file mode 100644 index 000000000..dc6f874ef --- /dev/null +++ b/programs/mango-v4/src/instructions/perp_liq_bankruptcy.rs @@ -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, 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::() + .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(()) +} diff --git a/programs/mango-v4/src/instructions/perp_liq_base_position.rs b/programs/mango-v4/src/instructions/perp_liq_base_position.rs index ea9f1d75a..59cf3d1b7 100644 --- a/programs/mango-v4/src/instructions/perp_liq_base_position.rs +++ b/programs/mango-v4/src/instructions/perp_liq_base_position.rs @@ -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!( diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index ebd5674ec..8f8edfb11 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -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 diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index eab9eb1c2..45f7d2746 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -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, max_funding_opt: Option, impact_quantity_opt: Option, + group_insurance_fund_opt: Option, + trusted_market_opt: Option, ) -> 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, + max_liab_transfer: u64, + ) -> Result<()> { + instructions::perp_liq_bankruptcy(ctx, max_liab_transfer) + } pub fn alt_set(ctx: Context, index: u8) -> Result<()> { instructions::alt_set(ctx, index) diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index cd70de393..7d10b882d 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -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); diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 6a2557737..a7a9a24ee 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -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 diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 89a27a9f7..d7cb06c68 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -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 { diff --git a/programs/mango-v4/src/state/orderbook/mod.rs b/programs/mango-v4/src/state/orderbook/mod.rs index 54c52a38d..4fc8ab3fe 100644 --- a/programs/mango-v4/src/state/orderbook/mod.rs +++ b/programs/mango-v4/src/state/orderbook/mod.rs @@ -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(); diff --git a/programs/mango-v4/src/state/perp_market.rs b/programs/mango-v4/src/state/perp_market.rs index a79b54ed9..49a8d2e31 100644 --- a/programs/mango-v4/src/state/perp_market.rs +++ b/programs/mango-v4/src/state/perp_market.rs @@ -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 { + 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) + } } diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 2baafbe95..49c634e2a 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -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_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 { + vec![self.liqor_owner] + } +} + pub struct BenchmarkInstruction {} #[async_trait::async_trait(?Send)] impl ClientInstruction for BenchmarkInstruction { diff --git a/programs/mango-v4/tests/program_test/mango_setup.rs b/programs/mango-v4/tests/program_test/mango_setup.rs index b13039e5c..a5b949f2d 100644 --- a/programs/mango-v4/tests/program_test/mango_setup.rs +++ b/programs/mango-v4/tests/program_test/mango_setup.rs @@ -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, + pub zero_token_is_quote: bool, } #[derive(Clone)] @@ -29,12 +31,13 @@ pub struct GroupWithTokens { pub tokens: Vec, } -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, diff --git a/programs/mango-v4/tests/test_alt.rs b/programs/mango-v4/tests/test_alt.rs index a4f102242..faa4247c8 100644 --- a/programs/mango-v4/tests/test_alt.rs +++ b/programs/mango-v4/tests/test_alt.rs @@ -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; diff --git a/programs/mango-v4/tests/test_bankrupt_tokens.rs b/programs/mango-v4/tests/test_bankrupt_tokens.rs index ac904ae80..755d7621a 100644 --- a/programs/mango-v4/tests/test_bankrupt_tokens.rs +++ b/programs/mango-v4/tests/test_bankrupt_tokens.rs @@ -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; diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index c7d6d0577..1a733e287 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -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; diff --git a/programs/mango-v4/tests/test_delegate.rs b/programs/mango-v4/tests/test_delegate.rs index 7026b4ec5..7ea5056ad 100644 --- a/programs/mango-v4/tests/test_delegate.rs +++ b/programs/mango-v4/tests/test_delegate.rs @@ -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; diff --git a/programs/mango-v4/tests/test_health_compute.rs b/programs/mango-v4/tests/test_health_compute.rs index 0cecf12b8..656e2e534 100644 --- a/programs/mango-v4/tests/test_health_compute.rs +++ b/programs/mango-v4/tests/test_health_compute.rs @@ -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; diff --git a/programs/mango-v4/tests/test_health_region.rs b/programs/mango-v4/tests/test_health_region.rs index 9af16fa75..80e7eb8ce 100644 --- a/programs/mango-v4/tests/test_health_region.rs +++ b/programs/mango-v4/tests/test_health_region.rs @@ -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; diff --git a/programs/mango-v4/tests/test_liq_perps.rs b/programs/mango-v4/tests/test_liq_perps.rs index b8e3fc7e8..c72655a06 100644 --- a/programs/mango-v4/tests/test_liq_perps.rs +++ b/programs/mango-v4/tests/test_liq_perps.rs @@ -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::(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::(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::(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::(liqor).await; + let quote_bank = solana.get_account::(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::(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::(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 )); diff --git a/programs/mango-v4/tests/test_liq_tokens.rs b/programs/mango-v4/tests/test_liq_tokens.rs index 692d54f68..bb9315141 100644 --- a/programs/mango-v4/tests/test_liq_tokens.rs +++ b/programs/mango-v4/tests/test_liq_tokens.rs @@ -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; diff --git a/programs/mango-v4/tests/test_margin_trade.rs b/programs/mango-v4/tests/test_margin_trade.rs index aca8e4fb7..bcb8242ee 100644 --- a/programs/mango-v4/tests/test_margin_trade.rs +++ b/programs/mango-v4/tests/test_margin_trade.rs @@ -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; diff --git a/programs/mango-v4/tests/test_perp.rs b/programs/mango-v4/tests/test_perp.rs index c0b0009b8..e0a60362b 100644 --- a/programs/mango-v4/tests/test_perp.rs +++ b/programs/mango-v4/tests/test_perp.rs @@ -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; diff --git a/programs/mango-v4/tests/test_perp_settle.rs b/programs/mango-v4/tests/test_perp_settle.rs index a2824e0ff..a54f0be20 100644 --- a/programs/mango-v4/tests/test_perp_settle.rs +++ b/programs/mango-v4/tests/test_perp_settle.rs @@ -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; diff --git a/programs/mango-v4/tests/test_perp_settle_fees.rs b/programs/mango-v4/tests/test_perp_settle_fees.rs index ce6125c06..b4b7e41f5 100644 --- a/programs/mango-v4/tests/test_perp_settle_fees.rs +++ b/programs/mango-v4/tests/test_perp_settle_fees.rs @@ -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; diff --git a/programs/mango-v4/tests/test_position_lifetime.rs b/programs/mango-v4/tests/test_position_lifetime.rs index f8bb92136..ad982691f 100644 --- a/programs/mango-v4/tests/test_position_lifetime.rs +++ b/programs/mango-v4/tests/test_position_lifetime.rs @@ -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; diff --git a/programs/mango-v4/tests/test_serum.rs b/programs/mango-v4/tests/test_serum.rs index 7f6fba9b0..5622ce6c7 100644 --- a/programs/mango-v4/tests/test_serum.rs +++ b/programs/mango-v4/tests/test_serum.rs @@ -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; diff --git a/programs/mango-v4/tests/test_token_update_index_and_rate.rs b/programs/mango-v4/tests/test_token_update_index_and_rate.rs index ddd65908d..15bb248eb 100644 --- a/programs/mango-v4/tests/test_token_update_index_and_rate.rs +++ b/programs/mango-v4/tests/test_token_update_index_and_rate.rs @@ -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; diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 844f85e16..c6ae0a29c 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1287,6 +1287,8 @@ export class MangoClient { minFunding: number, maxFunding: number, impactQuantity: number, + groupInsuranceFund: boolean, + trustedMarket: boolean, ): Promise { 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 { const perpMarket = group.perpMarketsMap.get(perpMarketName)!; @@ -1405,6 +1411,8 @@ export class MangoClient { minFunding, maxFunding, new BN(impactQuantity), + groupInsuranceFund, + trustedMarket, ) .accounts({ group: group.publicKey, diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 71f17f6b2..83cd9d234 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -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 ] } },