From ec2d10af6e822a9caf74bdc8dc5e635578ca9552 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 23 Apr 2024 09:17:53 +0200 Subject: [PATCH] Allow the insurance fund to be for any bank (#946) Co-authored-by: microwavedcola1 --- lib/client/src/client.rs | 28 +- mango_v4.json | 83 +++++ .../group_change_insurance_fund.rs | 66 ++++ programs/mango-v4/src/accounts_ix/mod.rs | 2 + .../perp_liq_negative_pnl_or_bankruptcy.rs | 2 +- .../src/accounts_ix/token_liq_bankruptcy.rs | 2 +- .../group_change_insurance_fund.rs | 24 ++ .../mango-v4/src/instructions/ix_gate_set.rs | 1 + programs/mango-v4/src/instructions/mod.rs | 2 + .../src/instructions/token_liq_bankruptcy.rs | 82 +++-- .../src/instructions/token_register.rs | 7 - programs/mango-v4/src/lib.rs | 6 + programs/mango-v4/src/state/group.rs | 6 +- .../tests/cases/test_bankrupt_tokens.rs | 336 ++++++++++++++++-- .../tests/cases/test_liq_perps_bankruptcy.rs | 300 ++++++++++++++++ .../tests/program_test/mango_client.rs | 116 ++++-- ts/client/src/client.ts | 18 + ts/client/src/clientIxParamBuilder.ts | 3 + ts/client/src/mango_v4.ts | 166 +++++++++ 19 files changed, 1130 insertions(+), 120 deletions(-) create mode 100644 programs/mango-v4/src/accounts_ix/group_change_insurance_fund.rs create mode 100644 programs/mango-v4/src/instructions/group_change_insurance_fund.rs diff --git a/lib/client/src/client.rs b/lib/client/src/client.rs index 81fa30aec..96d9390a1 100644 --- a/lib/client/src/client.rs +++ b/lib/client/src/client.rs @@ -25,7 +25,6 @@ use mango_v4::health::HealthCache; use mango_v4::state::{ Bank, Group, MangoAccountValue, OpenbookV2MarketIndex, OracleAccountInfos, PerpMarket, PerpMarketIndex, PlaceOrderType, SelfTradeBehavior, Serum3MarketIndex, Side, TokenIndex, - INSURANCE_TOKEN_INDEX, }; use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTransactionConfig}; @@ -1854,13 +1853,13 @@ impl MangoClient { let mango_account = &self.mango_account().await?; let perp = self.context.perp(market_index); let settle_token_info = self.context.token(perp.settle_token_index); - let insurance_token_info = self.context.token(INSURANCE_TOKEN_INDEX); + let insurance_token_info = self.context.token_by_mint(&group.insurance_mint)?; let (health_remaining_ams, health_cu) = self .derive_health_check_remaining_account_metas_two_accounts( mango_account, liqee.1, - &[INSURANCE_TOKEN_INDEX], + &[insurance_token_info.token_index], &[], ) .await @@ -2008,10 +2007,15 @@ impl MangoClient { liab_token_index: TokenIndex, max_liab_transfer: I80F48, ) -> anyhow::Result { - let mango_account = &self.mango_account().await?; - let quote_token_index = 0; + let group = account_fetcher_fetch_anchor_account::( + &*self.account_fetcher, + &self.context.group, + ) + .await?; - let quote_info = self.context.token(quote_token_index); + let mango_account = &self.mango_account().await?; + + let insurance_info = self.context.token_by_mint(&group.insurance_mint)?; let liab_info = self.context.token(liab_token_index); let bank_remaining_ams = liab_info @@ -2024,18 +2028,12 @@ impl MangoClient { .derive_health_check_remaining_account_metas_two_accounts( mango_account, liqee.1, - &[INSURANCE_TOKEN_INDEX], - &[quote_token_index, liab_token_index], + &[insurance_info.token_index], + &[insurance_info.token_index, liab_token_index], ) .await .unwrap(); - let group = account_fetcher_fetch_anchor_account::( - &*self.account_fetcher, - &self.context.group, - ) - .await?; - let ix = Instruction { program_id: mango_v4::id(), accounts: { @@ -2046,7 +2044,7 @@ impl MangoClient { liqor: self.mango_account_address, liqor_owner: self.owner(), liab_mint_info: liab_info.mint_info_address, - quote_vault: quote_info.first_vault(), + quote_vault: insurance_info.first_vault(), insurance_vault: group.insurance_vault, token_program: Token::id(), }, diff --git a/mango_v4.json b/mango_v4.json index b6e6091ea..93da805f8 100644 --- a/mango_v4.json +++ b/mango_v4.json @@ -326,6 +326,86 @@ } ] }, + { + "name": "groupChangeInsuranceFund", + "accounts": [ + { + "name": "group", + "isMut": true, + "isSigner": false, + "relations": [ + "insurance_vault", + "admin" + ] + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceVault", + "isMut": true, + "isSigner": false + }, + { + "name": "withdrawDestination", + "isMut": true, + "isSigner": false + }, + { + "name": "newInsuranceMint", + "isMut": false, + "isSigner": false + }, + { + "name": "newInsuranceVault", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "InsuranceVault" + }, + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Mint", + "path": "new_insurance_mint" + } + ] + } + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "ixGateSet", "accounts": [ @@ -11410,6 +11490,9 @@ }, { "name": "OpenbookV2CancelAllOrders" + }, + { + "name": "GroupChangeInsuranceFund" } ] } diff --git a/programs/mango-v4/src/accounts_ix/group_change_insurance_fund.rs b/programs/mango-v4/src/accounts_ix/group_change_insurance_fund.rs new file mode 100644 index 000000000..61478fa4f --- /dev/null +++ b/programs/mango-v4/src/accounts_ix/group_change_insurance_fund.rs @@ -0,0 +1,66 @@ +use crate::{error::MangoError, state::*}; +use anchor_lang::prelude::*; +use anchor_spl::token::{self, Mint, Token, TokenAccount}; + +#[derive(Accounts)] +pub struct GroupChangeInsuranceFund<'info> { + #[account( + mut, + has_one = insurance_vault, + has_one = admin, + constraint = group.load()?.is_ix_enabled(IxGate::GroupChangeInsuranceFund) @ MangoError::IxIsDisabled, + )] + pub group: AccountLoader<'info, Group>, + pub admin: Signer<'info>, + + #[account( + mut, + close = payer, + )] + pub insurance_vault: Account<'info, TokenAccount>, + + #[account(mut)] + pub withdraw_destination: Account<'info, TokenAccount>, + + pub new_insurance_mint: Account<'info, Mint>, + + #[account( + init, + seeds = [b"InsuranceVault".as_ref(), group.key().as_ref(), new_insurance_mint.key().as_ref()], + bump, + token::authority = group, + token::mint = new_insurance_mint, + payer = payer + )] + pub new_insurance_vault: Account<'info, TokenAccount>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, + pub rent: Sysvar<'info, Rent>, +} + +impl<'info> GroupChangeInsuranceFund<'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.withdraw_destination.to_account_info(), + authority: self.group.to_account_info(), + }; + CpiContext::new(program, accounts) + } + + pub fn close_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::CloseAccount<'info>> { + CpiContext::new( + self.token_program.to_account_info(), + token::CloseAccount { + account: self.insurance_vault.to_account_info(), + destination: self.payer.to_account_info(), + authority: self.group.to_account_info(), + }, + ) + } +} diff --git a/programs/mango-v4/src/accounts_ix/mod.rs b/programs/mango-v4/src/accounts_ix/mod.rs index d5573fe4a..eb040bc53 100644 --- a/programs/mango-v4/src/accounts_ix/mod.rs +++ b/programs/mango-v4/src/accounts_ix/mod.rs @@ -12,6 +12,7 @@ pub use alt_set::*; pub use benchmark::*; pub use compute_account_data::*; pub use flash_loan::*; +pub use group_change_insurance_fund::*; pub use group_close::*; pub use group_create::*; pub use group_edit::*; @@ -91,6 +92,7 @@ mod alt_set; mod benchmark; mod compute_account_data; mod flash_loan; +mod group_change_insurance_fund; mod group_close; mod group_create; mod group_edit; diff --git a/programs/mango-v4/src/accounts_ix/perp_liq_negative_pnl_or_bankruptcy.rs b/programs/mango-v4/src/accounts_ix/perp_liq_negative_pnl_or_bankruptcy.rs index f38993494..e78363fbc 100644 --- a/programs/mango-v4/src/accounts_ix/perp_liq_negative_pnl_or_bankruptcy.rs +++ b/programs/mango-v4/src/accounts_ix/perp_liq_negative_pnl_or_bankruptcy.rs @@ -115,7 +115,7 @@ pub struct PerpLiqNegativePnlOrBankruptcyV2<'info> { #[account( mut, has_one = group, - constraint = insurance_bank.load()?.token_index == INSURANCE_TOKEN_INDEX + constraint = insurance_bank.load()?.mint == insurance_vault.mint, )] pub insurance_bank: AccountLoader<'info, Bank>, diff --git a/programs/mango-v4/src/accounts_ix/token_liq_bankruptcy.rs b/programs/mango-v4/src/accounts_ix/token_liq_bankruptcy.rs index b5a351526..f31555262 100644 --- a/programs/mango-v4/src/accounts_ix/token_liq_bankruptcy.rs +++ b/programs/mango-v4/src/accounts_ix/token_liq_bankruptcy.rs @@ -8,7 +8,7 @@ use crate::state::*; // Remaining accounts: // - all banks for liab_mint_info (writable) -// - merged health accounts for liqor+liqee +// - merged health accounts for liqor + liqee, including the bank for the insurance token #[derive(Accounts)] pub struct TokenLiqBankruptcy<'info> { #[account( diff --git a/programs/mango-v4/src/instructions/group_change_insurance_fund.rs b/programs/mango-v4/src/instructions/group_change_insurance_fund.rs new file mode 100644 index 000000000..c3c11190f --- /dev/null +++ b/programs/mango-v4/src/instructions/group_change_insurance_fund.rs @@ -0,0 +1,24 @@ +use anchor_lang::prelude::*; +use anchor_spl::token; + +use crate::{accounts_ix::GroupChangeInsuranceFund, group_seeds}; + +pub fn group_change_insurance_fund(ctx: Context) -> Result<()> { + { + let group = ctx.accounts.group.load()?; + let group_seeds = group_seeds!(group); + token::transfer( + ctx.accounts.transfer_ctx().with_signer(&[group_seeds]), + ctx.accounts.insurance_vault.amount, + )?; + token::close_account(ctx.accounts.close_ctx().with_signer(&[group_seeds]))?; + } + + { + let mut group = ctx.accounts.group.load_mut()?; + group.insurance_vault = ctx.accounts.new_insurance_vault.key(); + group.insurance_mint = ctx.accounts.new_insurance_mint.key(); + } + + Ok(()) +} diff --git a/programs/mango-v4/src/instructions/ix_gate_set.rs b/programs/mango-v4/src/instructions/ix_gate_set.rs index 69ddf6cb7..d04d38565 100644 --- a/programs/mango-v4/src/instructions/ix_gate_set.rs +++ b/programs/mango-v4/src/instructions/ix_gate_set.rs @@ -99,6 +99,7 @@ pub fn ix_gate_set(ctx: Context, ix_gate: u128) -> Result<()> { log_if_changed(&group, ix_gate, IxGate::SequenceCheck); log_if_changed(&group, ix_gate, IxGate::HealthCheck); log_if_changed(&group, ix_gate, IxGate::OpenbookV2CancelAllOrders); + log_if_changed(&group, ix_gate, IxGate::GroupChangeInsuranceFund); group.ix_gate = ix_gate; diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index d75e0b37c..c402a0958 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -12,6 +12,7 @@ pub use alt_set::*; pub use benchmark::*; pub use compute_account_data::*; pub use flash_loan::*; +pub use group_change_insurance_fund::*; pub use group_close::*; pub use group_create::*; pub use group_edit::*; @@ -93,6 +94,7 @@ mod alt_set; mod benchmark; mod compute_account_data; mod flash_loan; +mod group_change_insurance_fund; mod group_close; mod group_create; mod group_edit; diff --git a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs index 8cc5e8638..cd04dec11 100644 --- a/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/token_liq_bankruptcy.rs @@ -26,6 +26,23 @@ pub fn token_liq_bankruptcy( let (bank_ais, health_ais) = &ctx.remaining_accounts.split_at(liab_mint_info.num_banks()); liab_mint_info.verify_banks_ais(bank_ais)?; + // find the insurance bank token index + let insurance_mint = ctx.accounts.insurance_vault.mint; + let insurance_token_index = health_ais + .iter() + .find_map(|ai| { + ai.load::() + .and_then(|b| { + if b.mint == insurance_mint { + Ok(b.token_index) + } else { + Err(MangoError::InvalidBank.into()) + } + }) + .ok() + }) + .ok_or_else(|| error_msg!("could not find bank for insurance mint in health accounts"))?; + require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key()); let mut liqor = ctx.accounts.liqor.load_full_mut()?; @@ -51,10 +68,10 @@ pub fn token_liq_bankruptcy( liqee_health_cache.require_after_phase2_liquidation()?; liqee.fixed.set_being_liquidated(true); - let liab_is_insurance_token = liab_token_index == INSURANCE_TOKEN_INDEX; - let (liab_bank, liab_oracle_price, opt_quote_bank_and_price) = - account_retriever.banks_mut_and_oracles(liab_token_index, INSURANCE_TOKEN_INDEX)?; - assert!(liab_is_insurance_token == opt_quote_bank_and_price.is_none()); + let liab_is_insurance_token = liab_token_index == insurance_token_index; + let (liab_bank, liab_oracle_price, opt_insurance_bank_and_price) = + account_retriever.banks_mut_and_oracles(liab_token_index, insurance_token_index)?; + assert!(liab_is_insurance_token == opt_insurance_bank_and_price.is_none()); let mut liab_deposit_index = liab_bank.deposit_index; let liab_borrow_index = liab_bank.borrow_index; @@ -76,11 +93,12 @@ pub fn token_liq_bankruptcy( // guaranteed positive let mut remaining_liab_loss = (-initial_liab_native).min(-liqee_liab_health_balance); - // We pay for the liab token in quote. Example: SOL is at $20 and USDC is at $2, then for a liab + // We pay for the liab token in insurance token. + // Example: SOL is at $20 and USDC is at $2, then for a liab // of 3 SOL, we'd pay 3 * 20 / 2 * (1+fee) = 30 * (1+fee) USDC. - let liab_to_quote_with_fee = - if let Some((_quote_bank, quote_price)) = opt_quote_bank_and_price.as_ref() { - liab_oracle_price * (I80F48::ONE + liab_bank.liquidation_fee) / quote_price + let liab_to_insurance_with_fee = + if let Some((_insurance_bank, insurance_price)) = opt_insurance_bank_and_price.as_ref() { + liab_oracle_price * (I80F48::ONE + liab_bank.liquidation_fee) / insurance_price } else { I80F48::ONE }; @@ -93,7 +111,7 @@ pub fn token_liq_bankruptcy( 0 }; - let insurance_transfer = (liab_transfer_unrounded * liab_to_quote_with_fee) + let insurance_transfer = (liab_transfer_unrounded * liab_to_insurance_with_fee) .ceil() .to_num::() .min(insurance_vault_amount); @@ -105,7 +123,7 @@ pub fn token_liq_bankruptcy( // AUDIT: v3 does this, but it seems bad, because it can make liab_transfer // exceed max_liab_transfer due to the ceil() above! Otoh, not doing it would allow // liquidators to exploit the insurance fund for 1 native token each call. - let liab_transfer = insurance_transfer_i80f48 / liab_to_quote_with_fee; + let liab_transfer = insurance_transfer_i80f48 / liab_to_insurance_with_fee; let mut liqee_liab_active = true; if insurance_transfer > 0 { @@ -115,36 +133,36 @@ pub fn token_liq_bankruptcy( // update correctly even if dusting happened remaining_liab_loss -= liqee_liab.native(liab_bank) - initial_liab_native; - // move insurance assets into quote bank + // move insurance assets into insurance bank let group_seeds = group_seeds!(group); token::transfer( ctx.accounts.transfer_ctx().with_signer(&[group_seeds]), insurance_transfer, )?; - // move quote assets into liqor and withdraw liab assets - if let Some((quote_bank, _)) = opt_quote_bank_and_price { + // move insurance assets into liqor and withdraw liab assets + if let Some((insurance_bank, _)) = opt_insurance_bank_and_price { // account constraint #2 a) - require_keys_eq!(quote_bank.vault, ctx.accounts.quote_vault.key()); - require_keys_eq!(quote_bank.mint, ctx.accounts.insurance_vault.mint); + require_keys_eq!(insurance_bank.vault, ctx.accounts.quote_vault.key()); + require_keys_eq!(insurance_bank.mint, ctx.accounts.insurance_vault.mint); - let quote_deposit_index = quote_bank.deposit_index; - let quote_borrow_index = quote_bank.borrow_index; + let insurance_deposit_index = insurance_bank.deposit_index; + let insurance_borrow_index = insurance_bank.borrow_index; // credit the liqor - let (liqor_quote, liqor_quote_raw_token_index, _) = - liqor.ensure_token_position(INSURANCE_TOKEN_INDEX)?; - let liqor_quote_active = - quote_bank.deposit(liqor_quote, insurance_transfer_i80f48, now_ts)?; + let (liqor_insurance, liqor_insurance_raw_token_index, _) = + liqor.ensure_token_position(insurance_token_index)?; + let liqor_insurance_active = + insurance_bank.deposit(liqor_insurance, insurance_transfer_i80f48, now_ts)?; - // liqor quote + // liqor insurance emit_stack(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.liqor.key(), - token_index: INSURANCE_TOKEN_INDEX, - indexed_position: liqor_quote.indexed_position.to_bits(), - deposit_index: quote_deposit_index.to_bits(), - borrow_index: quote_borrow_index.to_bits(), + token_index: insurance_token_index, + indexed_position: liqor_insurance.indexed_position.to_bits(), + deposit_index: insurance_deposit_index.to_bits(), + borrow_index: insurance_borrow_index.to_bits(), }); // transfer liab from liqee to liqor @@ -189,9 +207,9 @@ pub fn token_liq_bankruptcy( }); } - if !liqor_quote_active { + if !liqor_insurance_active { liqor.deactivate_token_position_and_log( - liqor_quote_raw_token_index, + liqor_insurance_raw_token_index, ctx.accounts.liqor.key(), ); } @@ -202,12 +220,12 @@ pub fn token_liq_bankruptcy( ); } } else { - // For liab_token_index == INSURANCE_TOKEN_INDEX: the insurance fund deposits directly into liqee, + // For liab_token_index == insurance_token_index: the insurance fund deposits directly into liqee, // without a fee or the liqor being involved // account constraint #2 b) require_keys_eq!(liab_bank.vault, ctx.accounts.quote_vault.key()); - require_eq!(liab_token_index, INSURANCE_TOKEN_INDEX); - require_eq!(liab_to_quote_with_fee, I80F48::ONE); + require_eq!(liab_token_index, insurance_token_index); + require_eq!(liab_to_insurance_with_fee, I80F48::ONE); require_eq!(insurance_transfer_i80f48, liab_transfer); } } @@ -287,7 +305,7 @@ pub fn token_liq_bankruptcy( liab_token_index, initial_liab_native: initial_liab_native.to_bits(), liab_price: liab_oracle_price.to_bits(), - insurance_token_index: INSURANCE_TOKEN_INDEX, + insurance_token_index, insurance_transfer: insurance_transfer_i80f48.to_bits(), socialized_loss: socialized_loss.to_bits(), starting_liab_deposit_index: starting_deposit_index.to_bits(), diff --git a/programs/mango-v4/src/instructions/token_register.rs b/programs/mango-v4/src/instructions/token_register.rs index 88a0229f7..f35bdcfb8 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -47,13 +47,6 @@ pub fn token_register( disable_asset_liquidation: bool, collateral_fee_per_day: f32, ) -> Result<()> { - // Require token 0 to be in the insurance token - if token_index == INSURANCE_TOKEN_INDEX { - require_keys_eq!( - ctx.accounts.group.load()?.insurance_mint, - ctx.accounts.mint.key() - ); - } require_neq!(token_index, TokenIndex::MAX); let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap(); diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 593f69474..c52592a38 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -115,6 +115,12 @@ pub mod mango_v4 { Ok(()) } + pub fn group_change_insurance_fund(ctx: Context) -> Result<()> { + #[cfg(feature = "enable-gpl")] + instructions::group_change_insurance_fund(ctx)?; + Ok(()) + } + pub fn ix_gate_set(ctx: Context, ix_gate: u128) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::ix_gate_set(ctx, ix_gate)?; diff --git a/programs/mango-v4/src/state/group.rs b/programs/mango-v4/src/state/group.rs index d125ac409..b0b33dda1 100644 --- a/programs/mango-v4/src/state/group.rs +++ b/programs/mango-v4/src/state/group.rs @@ -12,11 +12,6 @@ pub type TokenIndex = u16; /// incorrect assumption. pub const QUOTE_TOKEN_INDEX: TokenIndex = 0; -/// The token index used for the insurance fund. -/// -/// We should eventually generalize insurance funds. -pub const INSURANCE_TOKEN_INDEX: TokenIndex = 0; - /// The token index used for settling perp markets. /// /// We should eventually generalize to make the whole perp quote (and settle) token @@ -245,6 +240,7 @@ pub enum IxGate { SequenceCheck = 73, HealthCheck = 74, OpenbookV2CancelAllOrders = 75, + GroupChangeInsuranceFund = 76, // NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction. } diff --git a/programs/mango-v4/tests/cases/test_bankrupt_tokens.rs b/programs/mango-v4/tests/cases/test_bankrupt_tokens.rs index c9ba251bb..e21ed6d0b 100644 --- a/programs/mango-v4/tests/cases/test_bankrupt_tokens.rs +++ b/programs/mango-v4/tests/cases/test_bankrupt_tokens.rs @@ -320,36 +320,18 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { } // deposit some funds, to the vaults aren't empty - let vault_account = send_tx( - solana, - AccountCreateInstruction { - account_num: 2, - group, - owner, - payer, - ..Default::default() - }, - ) - .await - .unwrap() - .account; let vault_amount = 100000; - for &token_account in payer_mint_accounts { - send_tx( - solana, - TokenDepositInstruction { - amount: vault_amount, - reduce_only: false, - account: vault_account, - owner, - token_account, - token_authority: payer.clone(), - bank_index: 1, - }, - ) - .await - .unwrap(); - } + let vault_account = create_funded_account( + &solana, + group, + owner, + 2, + &context.users[1], + mints, + vault_amount, + 1, + ) + .await; // Also add a tiny amount to bank0 for borrow_token1, so we can test multi-bank socialized loss. // It must be enough to not trip the borrow limits on the bank. @@ -610,3 +592,299 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> { Ok(()) } + +#[tokio::test] +async fn test_bankrupt_tokens_other_insurance_fund() -> Result<(), TransportError> { + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(85_000); // TokenLiqWithToken needs 84k + let context = test_builder.start_default().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..4]; + let payer_mint_accounts = &context.users[1].token_accounts[0..4]; + + // + // SETUP: Create a group and an account to fill the vaults + // + + let mango_setup::GroupWithTokens { + group, + tokens, + insurance_vault, + .. + } = mango_setup::GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + let borrow_token1 = &tokens[0]; // USDC + let borrow_token2 = &tokens[1]; + let collateral_token1 = &tokens[2]; + let collateral_token2 = &tokens[3]; + let insurance_token = collateral_token2; + + // fund the insurance vault + { + 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()], + 1051, + ) + .unwrap(), + ); + tx.add_signer(payer); + tx.send().await.unwrap(); + } + + // + // TEST: switch the insurance vault mint, reclaiming the deposited tokens + // + let before_withdraw_dest = solana.token_account_balance(payer_mint_accounts[0]).await; + let insurance_vault = send_tx( + solana, + GroupChangeInsuranceFund { + group, + admin, + payer, + insurance_mint: insurance_token.mint.pubkey, + withdraw_destination: payer_mint_accounts[0], + }, + ) + .await + .unwrap() + .new_insurance_vault; + let after_withdraw_dest = solana.token_account_balance(payer_mint_accounts[0]).await; + assert_eq!(after_withdraw_dest - before_withdraw_dest, 1051); + + // SETUP: Fund the new insurance vault + { + let mut tx = ClientTransaction::new(solana); + tx.add_instruction_direct( + spl_token::instruction::transfer( + &spl_token::ID, + &payer_mint_accounts[3], + &insurance_vault, + &payer.pubkey(), + &[&payer.pubkey()], + 2000, + ) + .unwrap(), + ); + tx.add_signer(payer); + tx.send().await.unwrap(); + } + + // deposit some funds, to the vaults aren't empty + let vault_amount = 100000; + let vault_account = create_funded_account( + &solana, + group, + owner, + 2, + &context.users[1], + mints, + vault_amount, + 0, + ) + .await; + + // + // SETUP: Make an account with some collateral and some borrows + // + let account = send_tx( + solana, + AccountCreateInstruction { + account_num: 0, + group, + owner, + payer, + ..Default::default() + }, + ) + .await + .unwrap() + .account; + + let deposit1_amount = 20; + let deposit2_amount = 1000; + send_tx( + solana, + TokenDepositInstruction { + amount: deposit1_amount, + reduce_only: false, + account, + owner, + token_account: payer_mint_accounts[2], + token_authority: payer.clone(), + bank_index: 0, + }, + ) + .await + .unwrap(); + send_tx( + solana, + TokenDepositInstruction { + amount: deposit2_amount, + reduce_only: false, + account, + owner, + token_account: payer_mint_accounts[3], + token_authority: payer.clone(), + bank_index: 0, + }, + ) + .await + .unwrap(); + + let borrow1_amount = 50; + let borrow1_amount_bank0 = 10; + let borrow1_amount_bank1 = borrow1_amount - borrow1_amount_bank0; + let borrow2_amount = 350; + send_tx( + solana, + TokenWithdrawInstruction { + amount: borrow1_amount_bank1, + allow_borrow: true, + account, + owner, + token_account: payer_mint_accounts[0], + bank_index: 0, + }, + ) + .await + .unwrap(); + send_tx( + solana, + TokenWithdrawInstruction { + amount: borrow1_amount_bank0, + allow_borrow: true, + account, + owner, + token_account: payer_mint_accounts[0], + bank_index: 0, + }, + ) + .await + .unwrap(); + send_tx( + solana, + TokenWithdrawInstruction { + amount: borrow2_amount, + allow_borrow: true, + account, + owner, + token_account: payer_mint_accounts[1], + bank_index: 0, + }, + ) + .await + .unwrap(); + + // + // SETUP: Change the oracle to make health go very negative + // and change the insurance token price to verify it has an effect + // + set_bank_stub_oracle_price(solana, group, borrow_token2, admin, 20.0).await; + set_bank_stub_oracle_price(solana, group, insurance_token, admin, 1.5).await; + + // + // SETUP: liquidate all the collateral against borrow2 + // + + // eat collateral1 + send_tx( + solana, + TokenLiqWithTokenInstruction { + liqee: account, + liqor: vault_account, + liqor_owner: owner, + asset_token_index: collateral_token1.index, + asset_bank_index: 1, + liab_token_index: borrow_token2.index, + liab_bank_index: 1, + max_liab_transfer: I80F48::from_num(100000.0), + }, + ) + .await + .unwrap(); + assert!(account_position_closed(solana, account, collateral_token1.bank).await); + let liqee = get_mango_account(solana, account).await; + assert!(liqee.being_liquidated()); + + // eat collateral2, leaving the account bankrupt + send_tx( + solana, + TokenLiqWithTokenInstruction { + liqee: account, + liqor: vault_account, + liqor_owner: owner, + asset_token_index: collateral_token2.index, + asset_bank_index: 1, + liab_token_index: borrow_token2.index, + liab_bank_index: 1, + max_liab_transfer: I80F48::from_num(100000.0), + }, + ) + .await + .unwrap(); + assert!(account_position_closed(solana, account, collateral_token2.bank).await,); + let liqee = get_mango_account(solana, account).await; + assert!(liqee.being_liquidated()); + + // + // TEST: use the insurance fund to liquidate borrow1 and borrow2 + // + + // Change value of token that the insurance fund is in, to check that bankruptcy amounts + // are correct if it depegs + set_bank_stub_oracle_price(solana, group, borrow_token1, admin, 2.0).await; + + // bankruptcy: insurance token to liqor, liability to liqee + // liquidating only a partial amount + let liab_before = account_position_f64(solana, account, borrow_token2.bank).await; + let insurance_vault_before = solana.token_account_balance(insurance_vault).await; + let liqor_before = account_position(solana, vault_account, insurance_token.bank).await; + let insurance_to_liab = 1.5 / 20.0; + let liab_transfer: f64 = 500.0 * insurance_to_liab; + send_tx( + solana, + TokenLiqBankruptcyInstruction { + liqee: account, + liqor: vault_account, + liqor_owner: owner, + liab_mint_info: borrow_token2.mint_info, + max_liab_transfer: I80F48::from_num(liab_transfer), + }, + ) + .await + .unwrap(); + let liqee = get_mango_account(solana, account).await; + assert!(liqee.being_liquidated()); + assert!(account_position_closed(solana, account, insurance_token.bank).await); + assert_eq!( + account_position(solana, account, borrow_token2.bank).await, + (liab_before + liab_transfer).floor() as i64 + ); + let usdc_amount = (liab_transfer / insurance_to_liab * 1.02).ceil() as u64; + assert_eq!( + solana.token_account_balance(insurance_vault).await, + insurance_vault_before - usdc_amount + ); + assert_eq!( + account_position(solana, vault_account, insurance_token.bank).await, + liqor_before + usdc_amount as i64 + ); + + Ok(()) +} diff --git a/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs b/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs index d551cb4ed..7ed6e4012 100644 --- a/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs +++ b/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs @@ -450,3 +450,303 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { Ok(()) } + +#[tokio::test] +async fn test_liq_perps_bankruptcy_other_insurance_fund() -> Result<(), TransportError> { + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(200_000); // PerpLiqNegativePnlOrBankruptcy takes a lot of CU + let context = test_builder.start_default().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..4]; + let payer_mint_accounts = &context.users[1].token_accounts[0..4]; + + // + // SETUP: Create a group and an account to fill the vaults + // + + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + zero_token_is_quote: true, + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + + let _quote_token = &tokens[0]; // USDC, 1/1 weights, price 1, never changed + let base_token = &tokens[1]; // used for perp market + let collateral_token = &tokens[2]; // used for adjusting account health + let insurance_token = &tokens[3]; + + let insurance_vault = send_tx( + solana, + GroupChangeInsuranceFund { + group, + admin, + payer, + insurance_mint: insurance_token.mint.pubkey, + withdraw_destination: payer_mint_accounts[0], + }, + ) + .await + .unwrap() + .new_insurance_vault; + + // An unusual price to verify the oracle is used + set_bank_stub_oracle_price(solana, group, &insurance_token, admin, 1.6).await; + + send_tx( + solana, + TokenEditWeights { + group, + admin, + mint: mints[2].pubkey, + maint_liab_weight: 1.0, + maint_asset_weight: 1.0, + init_liab_weight: 1.0, + init_asset_weight: 1.0, + }, + ) + .await + .unwrap(); + + let fund_insurance = |amount: u64| async move { + let mut tx = ClientTransaction::new(solana); + tx.add_instruction_direct( + spl_token::instruction::transfer( + &spl_token::ID, + &payer_mint_accounts[3], + &insurance_vault, + &payer.pubkey(), + &[&payer.pubkey()], + amount, + ) + .unwrap(), + ); + tx.add_signer(payer); + tx.send().await.unwrap(); + }; + + // all perp markets used here default to price = 1.0, base_lot_size = 100 + let price_lots = 100; + + let context_ref = &context; + let mut perp_market_index: PerpMarketIndex = 0; + let setup_perp_inner = |perp_market_index: PerpMarketIndex, + health: i64, + pnl: i64, + settle_limit: i64| async move { + // price used later to produce negative pnl with a short: + // doubling the price leads to -100 pnl + let adj_price = 1.0 + pnl as f64 / -100.0; + let adj_price_lots = (price_lots as f64 * adj_price) as i64; + + let fresh_liqor = create_funded_account( + &solana, + group, + owner, + 200 + perp_market_index as u32, + &context_ref.users[1], + mints, + 10000, + 0, + ) + .await; + + let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx( + solana, + PerpCreateMarketInstruction { + group, + admin, + payer, + perp_market_index, + quote_lot_size: 1, + base_lot_size: 100, + maint_base_asset_weight: 0.8, + init_base_asset_weight: 0.6, + maint_base_liab_weight: 1.2, + init_base_liab_weight: 1.4, + base_liquidation_fee: 0.05, + maker_fee: 0.0, + taker_fee: 0.0, + group_insurance_fund: true, + // adjust this factur such that we get the desired settle limit in the end + settle_pnl_limit_factor: (settle_limit as f32 - 0.1).max(0.0) + / (1.0 * 100.0 * adj_price) as f32, + settle_pnl_limit_window_size_ts: 24 * 60 * 60, + ..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, base_token).await + }, + ) + .await + .unwrap(); + set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 1.0).await; + set_bank_stub_oracle_price(solana, group, &collateral_token, admin, 1.0).await; + + // + // SETUP: accounts + // + let deposit_amount = 1000; + let helper_account = create_funded_account( + &solana, + group, + owner, + perp_market_index as u32 * 2, + &context_ref.users[1], + &mints[2..3], + deposit_amount, + 0, + ) + .await; + let account = create_funded_account( + &solana, + group, + owner, + perp_market_index as u32 * 2 + 1, + &context_ref.users[1], + &mints[2..3], + deposit_amount, + 0, + ) + .await; + + // + // SETUP: Trade perps between accounts twice to generate pnl, settle_limit + // + let mut tx = ClientTransaction::new(solana); + tx.add_instruction(PerpPlaceOrderInstruction { + account: helper_account, + perp_market, + owner, + side: Side::Bid, + price_lots, + max_base_lots: 1, + ..PerpPlaceOrderInstruction::default() + }) + .await; + tx.add_instruction(PerpPlaceOrderInstruction { + account: account, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 1, + ..PerpPlaceOrderInstruction::default() + }) + .await; + tx.add_instruction(PerpConsumeEventsInstruction { + perp_market, + mango_accounts: vec![account, helper_account], + }) + .await; + tx.send().await.unwrap(); + + set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, adj_price).await; + let mut tx = ClientTransaction::new(solana); + tx.add_instruction(PerpPlaceOrderInstruction { + account: helper_account, + perp_market, + owner, + side: Side::Ask, + price_lots: adj_price_lots, + max_base_lots: 1, + ..PerpPlaceOrderInstruction::default() + }) + .await; + tx.add_instruction(PerpPlaceOrderInstruction { + account: account, + perp_market, + owner, + side: Side::Bid, + price_lots: adj_price_lots, + max_base_lots: 1, + ..PerpPlaceOrderInstruction::default() + }) + .await; + tx.add_instruction(PerpConsumeEventsInstruction { + perp_market, + mango_accounts: vec![account, helper_account], + }) + .await; + tx.send().await.unwrap(); + + set_perp_stub_oracle_price(solana, group, perp_market, &base_token, admin, 1.0).await; + + // Adjust target health: + // full health = 1000 * collat price * 1.0 + pnl + set_bank_stub_oracle_price( + solana, + group, + &collateral_token, + admin, + (health - pnl) as f64 / 1000.0, + ) + .await; + + // Verify we got it right + let account_data = solana.get_account::(account).await; + assert_eq!(account_data.perps[0].quote_position_native(), pnl); + assert_eq!( + account_data.perps[0].recurring_settle_pnl_allowance, + settle_limit + ); + assert_eq!( + account_init_health(solana, account).await.round(), + health as f64 + ); + + (perp_market, account, fresh_liqor) + }; + let mut setup_perp = |health: i64, pnl: i64, settle_limit: i64| { + let out = setup_perp_inner(perp_market_index, health, pnl, settle_limit); + perp_market_index += 1; + out + }; + + let limit_prec = |f: f64| (f * 1000.0).round() / 1000.0; + + let liq_event_amounts = || { + let settlement = solana + .program_log_events::() + .pop() + .map(|v| limit_prec(I80F48::from_bits(v.settlement).to_num::())) + .unwrap_or(0.0); + let (insur, loss) = solana + .program_log_events::() + .pop() + .map(|v| { + ( + I80F48::from_bits(v.insurance_transfer).to_num::(), + limit_prec(I80F48::from_bits(v.socialized_loss).to_num::()), + ) + }) + .unwrap_or((0, 0.0)); + (settlement, insur, loss) + }; + + { + let (perp_market, account, liqor) = setup_perp(-40, -50, 5).await; + fund_insurance(42).await; + + send_tx( + solana, + PerpLiqNegativePnlOrBankruptcyInstruction { + liqor, + liqor_owner: owner, + liqee: account, + perp_market, + max_liab_transfer: u64::MAX, + }, + ) + .await + .unwrap(); + // 27 insurance cover 27*1.6 = 43.2, where the needs is for 40 * 1.05 = 42 + assert_eq!(liq_event_amounts(), (5.0, 27, 0.0)); + } + + Ok(()) +} diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 8f49f1f88..6dbcd6487 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -1917,6 +1917,58 @@ impl ClientInstruction for GroupEdit { } } +pub struct GroupChangeInsuranceFund { + pub group: Pubkey, + pub admin: TestKeypair, + pub payer: TestKeypair, + pub insurance_mint: Pubkey, + pub withdraw_destination: Pubkey, +} +#[async_trait::async_trait(?Send)] +impl ClientInstruction for GroupChangeInsuranceFund { + type Accounts = mango_v4::accounts::GroupChangeInsuranceFund; + type Instruction = mango_v4::instruction::GroupChangeInsuranceFund; + 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 {}; + + let group = account_loader.load::(&self.group).await.unwrap(); + + let new_insurance_vault = Pubkey::find_program_address( + &[ + b"InsuranceVault".as_ref(), + self.group.as_ref(), + self.insurance_mint.as_ref(), + ], + &program_id, + ) + .0; + + let accounts = Self::Accounts { + group: self.group, + admin: self.admin.pubkey(), + insurance_vault: group.insurance_vault, + withdraw_destination: self.withdraw_destination, + new_insurance_mint: self.insurance_mint, + new_insurance_vault, + payer: self.payer.pubkey(), + token_program: Token::id(), + system_program: System::id(), + rent: sysvar::rent::Rent::id(), + }; + + let instruction = make_instruction(program_id, &accounts, &instruction); + (accounts, instruction) + } + + fn signers(&self) -> Vec { + vec![self.admin, self.payer] + } +} + pub struct IxGateSetInstruction { pub group: Pubkey, pub admin: TestKeypair, @@ -1960,21 +2012,17 @@ impl ClientInstruction for GroupCloseInstruction { type Instruction = mango_v4::instruction::GroupClose; async fn to_instruction( &self, - _account_loader: &(impl ClientAccountLoader + 'async_trait), + account_loader: &(impl ClientAccountLoader + 'async_trait), ) -> (Self::Accounts, instruction::Instruction) { let program_id = mango_v4::id(); let instruction = Self::Instruction {}; - let insurance_vault = Pubkey::find_program_address( - &[b"InsuranceVault".as_ref(), self.group.as_ref()], - &program_id, - ) - .0; + let group = account_loader.load::(&self.group).await.unwrap(); let accounts = Self::Accounts { group: self.group, admin: self.admin.pubkey(), - insurance_vault, + insurance_vault: group.insurance_vault, sol_destination: self.sol_destination, token_program: Token::id(), }; @@ -3259,21 +3307,11 @@ impl ClientInstruction for TokenLiqBankruptcyInstruction { .load_mango_account(&self.liqor) .await .unwrap(); - let health_check_metas = derive_liquidation_remaining_account_metas( - account_loader, - &liqee, - &liqor, - QUOTE_TOKEN_INDEX, - 0, - liab_mint_info.token_index, - 0, - ) - .await; let group_key = liqee.fixed.group; let group: Group = account_loader.load(&group_key).await.unwrap(); - let quote_mint_info = Pubkey::find_program_address( + let insurance_mint_info = Pubkey::find_program_address( &[ b"MintInfo".as_ref(), liqee.fixed.group.as_ref(), @@ -3282,13 +3320,19 @@ impl ClientInstruction for TokenLiqBankruptcyInstruction { &program_id, ) .0; - let quote_mint_info: MintInfo = account_loader.load("e_mint_info).await.unwrap(); + let insurance_mint_info: MintInfo = + account_loader.load(&insurance_mint_info).await.unwrap(); - let insurance_vault = Pubkey::find_program_address( - &[b"InsuranceVault".as_ref(), group_key.as_ref()], - &program_id, + let health_check_metas = derive_liquidation_remaining_account_metas( + account_loader, + &liqee, + &liqor, + insurance_mint_info.token_index, + 0, + liab_mint_info.token_index, + 0, ) - .0; + .await; let accounts = Self::Accounts { group: group_key, @@ -3296,8 +3340,8 @@ impl ClientInstruction for TokenLiqBankruptcyInstruction { liqor: self.liqor, liqor_owner: self.liqor_owner.pubkey(), liab_mint_info: self.liab_mint_info, - quote_vault: quote_mint_info.first_vault(), - insurance_vault, + quote_vault: insurance_mint_info.first_vault(), + insurance_vault: group.insurance_vault, token_program: Token::id(), }; @@ -4350,7 +4394,6 @@ impl ClientInstruction for PerpLiqNegativePnlOrBankruptcyInstruction { }; 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 @@ -4359,23 +4402,36 @@ impl ClientInstruction for PerpLiqNegativePnlOrBankruptcyInstruction { .load_mango_account(&self.liqee) .await .unwrap(); + + let group_key = liqee.fixed.group; + let group: Group = account_loader.load(&group_key).await.unwrap(); + + let insurance_mint_info = Pubkey::find_program_address( + &[ + b"MintInfo".as_ref(), + liqee.fixed.group.as_ref(), + group.insurance_mint.as_ref(), + ], + &program_id, + ) + .0; + let insurance_mint_info: MintInfo = + account_loader.load(&insurance_mint_info).await.unwrap(); + let health_check_metas = derive_liquidation_remaining_account_metas( account_loader, &liqee, &liqor, - TokenIndex::MAX, + insurance_mint_info.token_index, 0, TokenIndex::MAX, 0, ) .await; - let group = account_loader.load::(&group_key).await.unwrap(); let settle_mint_info = get_mint_info_by_token_index(account_loader, &liqee, perp_market.settle_token_index) .await; - let insurance_mint_info = - get_mint_info_by_token_index(account_loader, &liqee, QUOTE_TOKEN_INDEX).await; let accounts = Self::Accounts { group: group_key, diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 8adf901c1..07ea47a68 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -363,6 +363,24 @@ export class MangoClient { return await this.sendAndConfirmTransactionForGroup(group, [ix]); } + public async groupChangeInsuranceFund( + group: Group, + withdrawDestination: PublicKey, + newInsuranceMint: PublicKey, + ): Promise { + const ix = await this.program.methods + .groupChangeInsuranceFund() + .accounts({ + group: group.publicKey, + admin: (this.program.provider as AnchorProvider).wallet.publicKey, + insuranceVault: group.insuranceVault, + withdrawDestination, + newInsuranceMint, + }) + .instruction(); + return await this.sendAndConfirmTransactionForGroup(group, [ix]); + } + public async ixGateSet( group: Group, ixGateParams: IxGateParams, diff --git a/ts/client/src/clientIxParamBuilder.ts b/ts/client/src/clientIxParamBuilder.ts index c2e1ec13e..9bdf3d3e7 100644 --- a/ts/client/src/clientIxParamBuilder.ts +++ b/ts/client/src/clientIxParamBuilder.ts @@ -313,6 +313,7 @@ export interface IxGateParams { SequenceCheck: boolean; HealthCheck: boolean; OpenbookV2CancelAllOrders: boolean; + GroupChangeInsuranceFund: boolean; } // Default with all ixs enabled, use with buildIxGate @@ -396,6 +397,7 @@ export const TrueIxGateParams: IxGateParams = { SequenceCheck: true, HealthCheck: true, OpenbookV2CancelAllOrders: true, + GroupChangeInsuranceFund: true, }; // build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(), @@ -489,6 +491,7 @@ export function buildIxGate(p: IxGateParams): BN { toggleIx(ixGate, p, 'SequenceCheck', 73); toggleIx(ixGate, p, 'HealthCheck', 74); toggleIx(ixGate, p, 'OpenbookV2CancelAllOrders', 75); + toggleIx(ixGate, p, 'GroupChangeInsuranceFund', 76); return ixGate; } diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index faa35eee4..0d48f2e66 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -326,6 +326,86 @@ export type MangoV4 = { } ] }, + { + "name": "groupChangeInsuranceFund", + "accounts": [ + { + "name": "group", + "isMut": true, + "isSigner": false, + "relations": [ + "insurance_vault", + "admin" + ] + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceVault", + "isMut": true, + "isSigner": false + }, + { + "name": "withdrawDestination", + "isMut": true, + "isSigner": false + }, + { + "name": "newInsuranceMint", + "isMut": false, + "isSigner": false + }, + { + "name": "newInsuranceVault", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "InsuranceVault" + }, + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Mint", + "path": "new_insurance_mint" + } + ] + } + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "ixGateSet", "accounts": [ @@ -11410,6 +11490,9 @@ export type MangoV4 = { }, { "name": "OpenbookV2CancelAllOrders" + }, + { + "name": "GroupChangeInsuranceFund" } ] } @@ -15204,6 +15287,86 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "groupChangeInsuranceFund", + "accounts": [ + { + "name": "group", + "isMut": true, + "isSigner": false, + "relations": [ + "insurance_vault", + "admin" + ] + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceVault", + "isMut": true, + "isSigner": false + }, + { + "name": "withdrawDestination", + "isMut": true, + "isSigner": false + }, + { + "name": "newInsuranceMint", + "isMut": false, + "isSigner": false + }, + { + "name": "newInsuranceVault", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "InsuranceVault" + }, + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Mint", + "path": "new_insurance_mint" + } + ] + } + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "ixGateSet", "accounts": [ @@ -26288,6 +26451,9 @@ export const IDL: MangoV4 = { }, { "name": "OpenbookV2CancelAllOrders" + }, + { + "name": "GroupChangeInsuranceFund" } ] }