From 98ed7eff140333e1907f1c69318fbadf6ba286e3 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 (cherry picked from commit ec2d10af6e822a9caf74bdc8dc5e635578ca9552) --- lib/client/src/client.rs | 23 +- 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 | 1 + .../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 | 5 +- ts/client/src/mango_v4.ts | 1983 ++++++++++++----- 19 files changed, 2382 insertions(+), 677 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 52e84b591..b27334969 100644 --- a/lib/client/src/client.rs +++ b/lib/client/src/client.rs @@ -24,7 +24,7 @@ use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::health::HealthCache; use mango_v4::state::{ Bank, Group, MangoAccountValue, OracleAccountInfos, PerpMarket, PerpMarketIndex, - PlaceOrderType, SelfTradeBehavior, Serum3MarketIndex, Side, TokenIndex, INSURANCE_TOKEN_INDEX, + PlaceOrderType, SelfTradeBehavior, Serum3MarketIndex, Side, TokenIndex, }; use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTransactionConfig}; @@ -1763,13 +1763,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 @@ -1917,10 +1917,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 @@ -1933,8 +1938,8 @@ 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(); @@ -1955,7 +1960,7 @@ impl MangoClient { liqor: self.mango_account_address, liqor_owner: self.authority(), 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 9acc3ee05..f73a43984 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": [ @@ -11120,6 +11200,9 @@ }, { "name": "HealthCheck" + }, + { + "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 4256824a8..9da159875 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::*; @@ -92,6 +93,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 8fdd0b853..f23baf0f4 100644 --- a/programs/mango-v4/src/instructions/ix_gate_set.rs +++ b/programs/mango-v4/src/instructions/ix_gate_set.rs @@ -98,6 +98,7 @@ pub fn ix_gate_set(ctx: Context, ix_gate: u128) -> Result<()> { log_if_changed(&group, ix_gate, IxGate::TokenForceWithdraw); log_if_changed(&group, ix_gate, IxGate::SequenceCheck); log_if_changed(&group, ix_gate, IxGate::HealthCheck); + 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 1f91a7b53..68b8e9b4c 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::*; @@ -83,6 +84,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 7d545ad97..4d648ca8e 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 ac27e52a7..afb3b892e 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 61f61cde2..665f67b5d 100644 --- a/programs/mango-v4/src/state/group.rs +++ b/programs/mango-v4/src/state/group.rs @@ -248,6 +248,7 @@ pub enum IxGate { TokenForceWithdraw = 72, SequenceCheck = 73, HealthCheck = 74, + 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 649084e43..0888f73bc 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -1914,6 +1914,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, @@ -1957,21 +2009,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(), }; @@ -3253,21 +3301,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(), @@ -3276,13 +3314,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, @@ -3290,8 +3334,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(), }; @@ -4344,7 +4388,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 @@ -4353,23 +4396,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 c7f9f1517..5ecc63c28 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -361,6 +361,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 ddfb1dd51..63fd28056 100644 --- a/ts/client/src/clientIxParamBuilder.ts +++ b/ts/client/src/clientIxParamBuilder.ts @@ -36,7 +36,7 @@ export interface TokenRegisterParams { export const DefaultTokenRegisterParams: TokenRegisterParams = { oracleConfig: { - confFilter: 0, + confFilter: 0.3, maxStalenessSlots: null, }, groupInsuranceFund: false, @@ -312,6 +312,7 @@ export interface IxGateParams { TokenForceWithdraw: boolean; SequenceCheck: boolean; HealthCheck: boolean; + GroupChangeInsuranceFund: boolean; } // Default with all ixs enabled, use with buildIxGate @@ -394,6 +395,7 @@ export const TrueIxGateParams: IxGateParams = { TokenForceWithdraw: true, SequenceCheck: true, HealthCheck: true, + GroupChangeInsuranceFund: true, }; // build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(), @@ -486,6 +488,7 @@ export function buildIxGate(p: IxGateParams): BN { toggleIx(ixGate, p, 'TokenForceWithdraw', 72); toggleIx(ixGate, p, 'SequenceCheck', 73); toggleIx(ixGate, p, 'HealthCheck', 74); + toggleIx(ixGate, p, 'GroupChangeInsuranceFund', 76); return ixGate; } diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 31233e3d9..b9deb4ec9 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -1,5 +1,5 @@ export type MangoV4 = { - "version": "0.24.2", + "version": "0.25.0", "name": "mango_v4", "instructions": [ { @@ -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": [ @@ -1277,36 +1357,6 @@ export type MangoV4 = { ], "args": [] }, - { - "name": "tokenUpdateIndexAndRateResilient", - "accounts": [ - { - "name": "group", - "isMut": false, - "isSigner": false - }, - { - "name": "mintInfo", - "isMut": false, - "isSigner": false, - "relations": [ - "oracle", - "group" - ] - }, - { - "name": "oracle", - "isMut": false, - "isSigner": false - }, - { - "name": "instructions", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, { "name": "accountCreate", "accounts": [ @@ -1471,6 +1521,94 @@ export type MangoV4 = { } ] }, + { + "name": "accountCreateV3", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "MangoAccount" + }, + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "account", + "type": "publicKey", + "path": "owner" + }, + { + "kind": "arg", + "type": "u32", + "path": "account_num" + } + ] + } + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "accountNum", + "type": "u32" + }, + { + "name": "tokenCount", + "type": "u8" + }, + { + "name": "serum3Count", + "type": "u8" + }, + { + "name": "perpCount", + "type": "u8" + }, + { + "name": "perpOoCount", + "type": "u8" + }, + { + "name": "tokenConditionalSwapCount", + "type": "u8" + }, + { + "name": "openbookV2Count", + "type": "u8" + }, + { + "name": "name", + "type": "string" + } + ] + }, { "name": "accountExpand", "accounts": [ @@ -1579,6 +1717,66 @@ export type MangoV4 = { } ] }, + { + "name": "accountExpandV3", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group", + "owner" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "tokenCount", + "type": "u8" + }, + { + "name": "serum3Count", + "type": "u8" + }, + { + "name": "perpCount", + "type": "u8" + }, + { + "name": "perpOoCount", + "type": "u8" + }, + { + "name": "tokenConditionalSwapCount", + "type": "u8" + }, + { + "name": "openbookV2Count", + "type": "u8" + } + ] + }, { "name": "accountSizeMigration", "accounts": [ @@ -6253,15 +6451,15 @@ export type MangoV4 = { { "name": "group", "isMut": true, - "isSigner": false, - "relations": [ - "admin" - ] + "isSigner": false }, { "name": "admin", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "group admin or fast listing admin, checked at #1" + ] }, { "name": "openbookV2Program", @@ -6356,6 +6554,10 @@ export type MangoV4 = { { "name": "name", "type": "string" + }, + { + "name": "oraclePriceBand", + "type": "f32" } ] }, @@ -6365,7 +6567,10 @@ export type MangoV4 = { { "name": "group", "isMut": false, - "isSigner": false + "isSigner": false, + "relations": [ + "admin" + ] }, { "name": "admin", @@ -6393,6 +6598,18 @@ export type MangoV4 = { "type": { "option": "bool" } + }, + { + "name": "nameOpt", + "type": { + "option": "string" + } + }, + { + "name": "oraclePriceBandOpt", + "type": { + "option": "f32" + } } ] }, @@ -6457,11 +6674,6 @@ export type MangoV4 = { "group" ] }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, { "name": "openbookV2Market", "isMut": false, @@ -6483,38 +6695,19 @@ export type MangoV4 = { "isSigner": false }, { - "name": "openOrders", + "name": "openOrdersIndexer", "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "OpenOrders" - }, - { - "kind": "account", - "type": "publicKey", - "path": "openbook_v2_market" - }, - { - "kind": "account", - "type": "publicKey", - "path": "openbook_v2_market_external" - }, - { - "kind": "arg", - "type": "u32", - "path": "account_num" - } - ], - "programId": { - "kind": "account", - "type": "publicKey", - "path": "openbook_v2_program" - } - } + "isSigner": false + }, + { + "name": "openOrdersAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true }, { "name": "payer", @@ -6532,12 +6725,7 @@ export type MangoV4 = { "isSigner": false } ], - "args": [ - { - "name": "accountNum", - "type": "u32" - } - ] + "args": [] }, { "name": "openbookV2CloseOpenOrders", @@ -6581,7 +6769,15 @@ export type MangoV4 = { "isSigner": false }, { - "name": "openOrders", + "name": "openOrdersIndexer", + "isMut": true, + "isSigner": false, + "docs": [ + "can't zerocopy this unfortunately" + ] + }, + { + "name": "openOrdersAccount", "isMut": true, "isSigner": false }, @@ -6589,6 +6785,32 @@ export type MangoV4 = { "name": "solDestination", "isMut": true, "isSigner": false + }, + { + "name": "baseBank", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "quoteBank", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false } ], "args": [] @@ -6619,150 +6841,14 @@ export type MangoV4 = { "isMut": true, "isSigner": false }, - { - "name": "openbookV2Market", - "isMut": false, - "isSigner": false - }, - { - "name": "openbookV2Program", - "isMut": false, - "isSigner": false - }, - { - "name": "openbookV2MarketExternal", - "isMut": true, - "isSigner": false, - "relations": [ - "bids", - "asks", - "event_heap" - ] - }, - { - "name": "bids", - "isMut": true, - "isSigner": false - }, - { - "name": "asks", - "isMut": true, - "isSigner": false - }, - { - "name": "eventHeap", - "isMut": true, - "isSigner": false - }, - { - "name": "marketBaseVault", - "isMut": true, - "isSigner": false - }, - { - "name": "marketQuoteVault", - "isMut": true, - "isSigner": false - }, - { - "name": "marketVaultSigner", - "isMut": false, - "isSigner": false - }, - { - "name": "payerBank", - "isMut": true, - "isSigner": false, - "docs": [ - "The bank that pays for the order, if necessary" - ], - "relations": [ - "group" - ] - }, - { - "name": "payerVault", - "isMut": true, - "isSigner": false, - "docs": [ - "The bank vault that pays for the order, if necessary" - ] - }, - { - "name": "payerOracle", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "side", - "type": "u8" - }, - { - "name": "limitPrice", - "type": "u64" - }, - { - "name": "maxBaseQty", - "type": "u64" - }, - { - "name": "maxNativeQuoteQtyIncludingFees", - "type": "u64" - }, - { - "name": "selfTradeBehavior", - "type": "u8" - }, - { - "name": "orderType", - "type": "u8" - }, - { - "name": "clientOrderId", - "type": "u64" - }, - { - "name": "limit", - "type": "u16" - } - ] - }, - { - "name": "openbookV2PlaceTakerOrder", - "accounts": [ - { - "name": "group", - "isMut": false, - "isSigner": false - }, - { - "name": "account", - "isMut": true, - "isSigner": false, - "relations": [ - "group" - ] - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, { "name": "openbookV2Market", "isMut": false, "isSigner": false, "relations": [ "group", - "openbook_v2_program", - "openbook_v2_market_external" + "openbook_v2_market_external", + "openbook_v2_program" ] }, { @@ -6796,17 +6882,7 @@ export type MangoV4 = { "isSigner": false }, { - "name": "marketRequestQueue", - "isMut": true, - "isSigner": false - }, - { - "name": "marketBaseVault", - "isMut": true, - "isSigner": false - }, - { - "name": "marketQuoteVault", + "name": "marketVault", "isMut": true, "isSigner": false }, @@ -6820,7 +6896,7 @@ export type MangoV4 = { "isMut": true, "isSigner": false, "docs": [ - "The bank that pays for the order, if necessary" + "The bank that pays for the order. Bank oracle also expected in remaining_accounts" ], "relations": [ "group" @@ -6831,13 +6907,19 @@ export type MangoV4 = { "isMut": true, "isSigner": false, "docs": [ - "The bank vault that pays for the order, if necessary" + "The bank vault that pays for the order" ] }, { - "name": "payerOracle", - "isMut": false, - "isSigner": false + "name": "receiverBank", + "isMut": true, + "isSigner": false, + "docs": [ + "The bank that receives the funds upon settlement. Bank oracle also expected in remaining_accounts" + ], + "relations": [ + "group" + ] }, { "name": "tokenProgram", @@ -6848,31 +6930,49 @@ export type MangoV4 = { "args": [ { "name": "side", - "type": "u8" + "type": { + "defined": "OpenbookV2Side" + } }, { - "name": "limitPrice", - "type": "u64" + "name": "priceLots", + "type": "i64" }, { - "name": "maxBaseQty", - "type": "u64" + "name": "maxBaseLots", + "type": "i64" }, { - "name": "maxNativeQuoteQtyIncludingFees", - "type": "u64" - }, - { - "name": "selfTradeBehavior", - "type": "u8" + "name": "maxQuoteLotsIncludingFees", + "type": "i64" }, { "name": "clientOrderId", "type": "u64" }, + { + "name": "orderType", + "type": { + "defined": "OpenbookV2PlaceOrderType" + } + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": "OpenbookV2SelfTradeBehavior" + } + }, + { + "name": "reduceOnly", + "type": "bool" + }, + { + "name": "expiryTimestamp", + "type": "u64" + }, { "name": "limit", - "type": "u16" + "type": "u8" } ] }, @@ -6940,7 +7040,9 @@ export type MangoV4 = { "args": [ { "name": "side", - "type": "u8" + "type": { + "defined": "OpenbookV2Side" + } }, { "name": "orderId", @@ -6966,7 +7068,7 @@ export type MangoV4 = { }, { "name": "authority", - "isMut": false, + "isMut": true, "isSigner": true }, { @@ -6992,7 +7094,11 @@ export type MangoV4 = { { "name": "openbookV2MarketExternal", "isMut": true, - "isSigner": false + "isSigner": false, + "relations": [ + "market_base_vault", + "market_quote_vault" + ] }, { "name": "marketBaseVault", @@ -7052,6 +7158,11 @@ export type MangoV4 = { "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false } ], "args": [ @@ -7077,6 +7188,11 @@ export type MangoV4 = { "group" ] }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, { "name": "openOrders", "isMut": true, @@ -7099,12 +7215,14 @@ export type MangoV4 = { }, { "name": "openbookV2MarketExternal", - "isMut": false, + "isMut": true, "isSigner": false, "relations": [ "bids", "asks", - "event_heap" + "event_heap", + "market_base_vault", + "market_quote_vault" ] }, { @@ -7167,6 +7285,11 @@ export type MangoV4 = { "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false } ], "args": [ @@ -7241,6 +7364,14 @@ export type MangoV4 = { { "name": "limit", "type": "u8" + }, + { + "name": "sideOpt", + "type": { + "option": { + "defined": "OpenbookV2Side" + } + } } ] }, @@ -7631,7 +7762,7 @@ export type MangoV4 = { { "name": "potentialSerumTokens", "docs": [ - "Largest amount of tokens that might be added the the bank based on", + "Largest amount of tokens that might be added the bank based on", "serum open order execution." ], "type": "u64" @@ -7741,12 +7872,29 @@ export type MangoV4 = { ], "type": "f32" }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "potentialOpenbookTokens", + "docs": [ + "Largest amount of tokens that might be added the bank based on", + "oenbook open order execution." + ], + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 1900 + 1888 ] } } @@ -8109,12 +8257,24 @@ export type MangoV4 = { } } }, + { + "name": "padding9", + "type": "u32" + }, + { + "name": "openbookV2", + "type": { + "vec": { + "defined": "OpenbookV2Orders" + } + } + }, { "name": "reservedDynamic", "type": { "array": [ "u8", - 64 + 56 ] } } @@ -8210,6 +8370,10 @@ export type MangoV4 = { "name": "quoteTokenIndex", "type": "u16" }, + { + "name": "marketIndex", + "type": "u16" + }, { "name": "reduceOnly", "type": "u8" @@ -8218,15 +8382,6 @@ export type MangoV4 = { "name": "forceClose", "type": "u8" }, - { - "name": "padding1", - "type": { - "array": [ - "u8", - 2 - ] - } - }, { "name": "name", "type": { @@ -8245,32 +8400,29 @@ export type MangoV4 = { "type": "publicKey" }, { - "name": "marketIndex", - "type": "u16" + "name": "registrationTime", + "type": "u64" + }, + { + "name": "oraclePriceBand", + "docs": [ + "Limit orders must be <= oracle * (1+band) and >= oracle / (1+band)", + "", + "Zero value is the default due to migration and disables the limit,", + "same as f32::MAX." + ], + "type": "f32" }, { "name": "bump", "type": "u8" }, - { - "name": "padding2", - "type": { - "array": [ - "u8", - 5 - ] - } - }, - { - "name": "registrationTime", - "type": "u64" - }, { "name": "reserved", "type": { "array": [ "u8", - 512 + 1027 ] } } @@ -9413,6 +9565,121 @@ export type MangoV4 = { ] } }, + { + "name": "OpenbookV2Orders", + "type": { + "kind": "struct", + "fields": [ + { + "name": "openOrders", + "type": "publicKey" + }, + { + "name": "baseBorrowsWithoutFee", + "docs": [ + "Tracks the amount of borrows that have flowed into the open orders account.", + "These borrows did not have the loan origination fee applied, and that may happen", + "later (in openbook_v2_settle_funds) if we can guarantee that the funds were used.", + "In particular a place-on-book, cancel, settle should not cost fees." + ], + "type": "u64" + }, + { + "name": "quoteBorrowsWithoutFee", + "type": "u64" + }, + { + "name": "highestPlacedBidInv", + "docs": [ + "Track something like the highest open bid / lowest open ask, in native/native units.", + "", + "Tracking it exactly isn't possible since we don't see fills. So instead track", + "the min/max of the _placed_ bids and asks.", + "", + "The value is reset in openbook_v2_place_order when a new order is placed without an", + "existing one on the book.", + "", + "0 is a special \"unset\" state." + ], + "type": "f64" + }, + { + "name": "lowestPlacedAsk", + "type": "f64" + }, + { + "name": "potentialBaseTokens", + "docs": [ + "An overestimate of the amount of tokens that might flow out of the open orders account.", + "", + "The bank still considers these amounts user deposits (see Bank::potential_openbook_tokens)", + "and that value needs to be updated in conjunction with these numbers.", + "", + "This estimation is based on the amount of tokens in the open orders account", + "(see update_bank_potential_tokens() in openbook_v2_place_order and settle)" + ], + "type": "u64" + }, + { + "name": "potentialQuoteTokens", + "type": "u64" + }, + { + "name": "lowestPlacedBidInv", + "docs": [ + "Track lowest bid/highest ask, same way as for highest bid/lowest ask.", + "", + "0 is a special \"unset\" state." + ], + "type": "f64" + }, + { + "name": "highestPlacedAsk", + "type": "f64" + }, + { + "name": "quoteLotSize", + "docs": [ + "Stores the market's lot sizes", + "", + "Needed because the obv2 open orders account tells us about reserved amounts in lots and", + "we want to be able to compute health without also loading the obv2 market." + ], + "type": "i64" + }, + { + "name": "baseLotSize", + "type": "i64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "baseTokenIndex", + "docs": [ + "Store the base/quote token index, so health computations don't need", + "to get passed the static SerumMarket to find which tokens a market", + "uses and look up the correct oracles." + ], + "type": "u16" + }, + { + "name": "quoteTokenIndex", + "type": "u16" + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 162 + ] + } + } + ] + } + }, { "name": "PerpPosition", "type": { @@ -10760,6 +11027,77 @@ export type MangoV4 = { ] } }, + { + "name": "OpenbookV2PlaceOrderType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Limit" + }, + { + "name": "ImmediateOrCancel" + }, + { + "name": "PostOnly" + }, + { + "name": "Market" + }, + { + "name": "PostOnlySlide" + } + ] + } + }, + { + "name": "OpenbookV2PostOrderType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Limit" + }, + { + "name": "PostOnly" + }, + { + "name": "PostOnlySlide" + } + ] + } + }, + { + "name": "OpenbookV2SelfTradeBehavior", + "type": { + "kind": "enum", + "variants": [ + { + "name": "DecrementTake" + }, + { + "name": "CancelProvide" + }, + { + "name": "AbortTransaction" + } + ] + } + }, + { + "name": "OpenbookV2Side", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Bid" + }, + { + "name": "Ask" + } + ] + } + }, { "name": "Serum3SelfTradeBehavior", "docs": [ @@ -10844,6 +11182,26 @@ export type MangoV4 = { ] } }, + { + "name": "SpotMarketIndex", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Serum3", + "fields": [ + "u16" + ] + }, + { + "name": "OpenbookV2", + "fields": [ + "u16" + ] + } + ] + } + }, { "name": "LoanOriginationFeeInstruction", "type": { @@ -10872,6 +11230,15 @@ export type MangoV4 = { }, { "name": "TokenConditionalSwapTrigger" + }, + { + "name": "OpenbookV2LiqForceCancelOrders" + }, + { + "name": "OpenbookV2PlaceOrder" + }, + { + "name": "OpenbookV2SettleFunds" } ] } @@ -11120,6 +11487,9 @@ export type MangoV4 = { }, { "name": "HealthCheck" + }, + { + "name": "GroupChangeInsuranceFund" } ] } @@ -11163,12 +11533,6 @@ export type MangoV4 = { }, { "name": "RaydiumCLMM" - }, - { - "name": "SwitchboardOnDemand" - }, - { - "name": "PythV2" } ] } @@ -12516,6 +12880,61 @@ export type MangoV4 = { } ] }, + { + "name": "OpenbookV2OpenOrdersBalanceLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "mangoAccount", + "type": "publicKey", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "baseTokenIndex", + "type": "u16", + "index": false + }, + { + "name": "quoteTokenIndex", + "type": "u16", + "index": false + }, + { + "name": "baseTotal", + "type": "u64", + "index": false + }, + { + "name": "baseFree", + "type": "u64", + "index": false + }, + { + "name": "quoteTotal", + "type": "u64", + "index": false + }, + { + "name": "quoteFree", + "type": "u64", + "index": false + }, + { + "name": "referrerRebatesAccrued", + "type": "u64", + "index": false + } + ] + }, { "name": "WithdrawLoanOriginationFeeLog", "fields": [ @@ -12882,6 +13301,46 @@ export type MangoV4 = { } ] }, + { + "name": "OpenbookV2RegisterMarketLog", + "fields": [ + { + "name": "mangoGroup", + "type": "publicKey", + "index": false + }, + { + "name": "openbookMarket", + "type": "publicKey", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "baseTokenIndex", + "type": "u16", + "index": false + }, + { + "name": "quoteTokenIndex", + "type": "u16", + "index": false + }, + { + "name": "openbookProgram", + "type": "publicKey", + "index": false + }, + { + "name": "openbookMarketExternal", + "type": "publicKey", + "index": false + } + ] + }, { "name": "PerpLiqBaseOrPositivePnlLog", "fields": [ @@ -14291,8 +14750,8 @@ export type MangoV4 = { }, { "code": 6034, - "name": "HasOpenOrUnsettledSerum3Orders", - "msg": "there are open or unsettled serum3 orders" + "name": "HasOpenOrUnsettledSpotOrders", + "msg": "there are open or unsettled spot orders" }, { "code": 6035, @@ -14426,7 +14885,7 @@ export type MangoV4 = { }, { "code": 6061, - "name": "Serum3PriceBandExceeded", + "name": "SpotPriceBandExceeded", "msg": "the market does not allow limit orders too far from the current oracle value" }, { @@ -14483,12 +14942,22 @@ export type MangoV4 = { "code": 6072, "name": "InvalidHealth", "msg": "invalid health" + }, + { + "code": 6073, + "name": "NoFreeOpenbookV2OpenOrdersIndex", + "msg": "no free openbook v2 open orders index" + }, + { + "code": 6074, + "name": "OpenbookV2OpenOrdersExistAlready", + "msg": "openbook v2 open orders exist already" } ] }; export const IDL: MangoV4 = { - "version": "0.24.2", + "version": "0.25.0", "name": "mango_v4", "instructions": [ { @@ -14815,6 +15284,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": [ @@ -15766,36 +16315,6 @@ export const IDL: MangoV4 = { ], "args": [] }, - { - "name": "tokenUpdateIndexAndRateResilient", - "accounts": [ - { - "name": "group", - "isMut": false, - "isSigner": false - }, - { - "name": "mintInfo", - "isMut": false, - "isSigner": false, - "relations": [ - "oracle", - "group" - ] - }, - { - "name": "oracle", - "isMut": false, - "isSigner": false - }, - { - "name": "instructions", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, { "name": "accountCreate", "accounts": [ @@ -15960,6 +16479,94 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "accountCreateV3", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "MangoAccount" + }, + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "account", + "type": "publicKey", + "path": "owner" + }, + { + "kind": "arg", + "type": "u32", + "path": "account_num" + } + ] + } + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "accountNum", + "type": "u32" + }, + { + "name": "tokenCount", + "type": "u8" + }, + { + "name": "serum3Count", + "type": "u8" + }, + { + "name": "perpCount", + "type": "u8" + }, + { + "name": "perpOoCount", + "type": "u8" + }, + { + "name": "tokenConditionalSwapCount", + "type": "u8" + }, + { + "name": "openbookV2Count", + "type": "u8" + }, + { + "name": "name", + "type": "string" + } + ] + }, { "name": "accountExpand", "accounts": [ @@ -16068,6 +16675,66 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "accountExpandV3", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group", + "owner" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "tokenCount", + "type": "u8" + }, + { + "name": "serum3Count", + "type": "u8" + }, + { + "name": "perpCount", + "type": "u8" + }, + { + "name": "perpOoCount", + "type": "u8" + }, + { + "name": "tokenConditionalSwapCount", + "type": "u8" + }, + { + "name": "openbookV2Count", + "type": "u8" + } + ] + }, { "name": "accountSizeMigration", "accounts": [ @@ -20742,15 +21409,15 @@ export const IDL: MangoV4 = { { "name": "group", "isMut": true, - "isSigner": false, - "relations": [ - "admin" - ] + "isSigner": false }, { "name": "admin", "isMut": false, - "isSigner": true + "isSigner": true, + "docs": [ + "group admin or fast listing admin, checked at #1" + ] }, { "name": "openbookV2Program", @@ -20845,6 +21512,10 @@ export const IDL: MangoV4 = { { "name": "name", "type": "string" + }, + { + "name": "oraclePriceBand", + "type": "f32" } ] }, @@ -20854,7 +21525,10 @@ export const IDL: MangoV4 = { { "name": "group", "isMut": false, - "isSigner": false + "isSigner": false, + "relations": [ + "admin" + ] }, { "name": "admin", @@ -20882,6 +21556,18 @@ export const IDL: MangoV4 = { "type": { "option": "bool" } + }, + { + "name": "nameOpt", + "type": { + "option": "string" + } + }, + { + "name": "oraclePriceBandOpt", + "type": { + "option": "f32" + } } ] }, @@ -20946,11 +21632,6 @@ export const IDL: MangoV4 = { "group" ] }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, { "name": "openbookV2Market", "isMut": false, @@ -20972,38 +21653,19 @@ export const IDL: MangoV4 = { "isSigner": false }, { - "name": "openOrders", + "name": "openOrdersIndexer", "isMut": true, - "isSigner": false, - "pda": { - "seeds": [ - { - "kind": "const", - "type": "string", - "value": "OpenOrders" - }, - { - "kind": "account", - "type": "publicKey", - "path": "openbook_v2_market" - }, - { - "kind": "account", - "type": "publicKey", - "path": "openbook_v2_market_external" - }, - { - "kind": "arg", - "type": "u32", - "path": "account_num" - } - ], - "programId": { - "kind": "account", - "type": "publicKey", - "path": "openbook_v2_program" - } - } + "isSigner": false + }, + { + "name": "openOrdersAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true }, { "name": "payer", @@ -21021,12 +21683,7 @@ export const IDL: MangoV4 = { "isSigner": false } ], - "args": [ - { - "name": "accountNum", - "type": "u32" - } - ] + "args": [] }, { "name": "openbookV2CloseOpenOrders", @@ -21070,7 +21727,15 @@ export const IDL: MangoV4 = { "isSigner": false }, { - "name": "openOrders", + "name": "openOrdersIndexer", + "isMut": true, + "isSigner": false, + "docs": [ + "can't zerocopy this unfortunately" + ] + }, + { + "name": "openOrdersAccount", "isMut": true, "isSigner": false }, @@ -21078,6 +21743,32 @@ export const IDL: MangoV4 = { "name": "solDestination", "isMut": true, "isSigner": false + }, + { + "name": "baseBank", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "quoteBank", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false } ], "args": [] @@ -21108,150 +21799,14 @@ export const IDL: MangoV4 = { "isMut": true, "isSigner": false }, - { - "name": "openbookV2Market", - "isMut": false, - "isSigner": false - }, - { - "name": "openbookV2Program", - "isMut": false, - "isSigner": false - }, - { - "name": "openbookV2MarketExternal", - "isMut": true, - "isSigner": false, - "relations": [ - "bids", - "asks", - "event_heap" - ] - }, - { - "name": "bids", - "isMut": true, - "isSigner": false - }, - { - "name": "asks", - "isMut": true, - "isSigner": false - }, - { - "name": "eventHeap", - "isMut": true, - "isSigner": false - }, - { - "name": "marketBaseVault", - "isMut": true, - "isSigner": false - }, - { - "name": "marketQuoteVault", - "isMut": true, - "isSigner": false - }, - { - "name": "marketVaultSigner", - "isMut": false, - "isSigner": false - }, - { - "name": "payerBank", - "isMut": true, - "isSigner": false, - "docs": [ - "The bank that pays for the order, if necessary" - ], - "relations": [ - "group" - ] - }, - { - "name": "payerVault", - "isMut": true, - "isSigner": false, - "docs": [ - "The bank vault that pays for the order, if necessary" - ] - }, - { - "name": "payerOracle", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "side", - "type": "u8" - }, - { - "name": "limitPrice", - "type": "u64" - }, - { - "name": "maxBaseQty", - "type": "u64" - }, - { - "name": "maxNativeQuoteQtyIncludingFees", - "type": "u64" - }, - { - "name": "selfTradeBehavior", - "type": "u8" - }, - { - "name": "orderType", - "type": "u8" - }, - { - "name": "clientOrderId", - "type": "u64" - }, - { - "name": "limit", - "type": "u16" - } - ] - }, - { - "name": "openbookV2PlaceTakerOrder", - "accounts": [ - { - "name": "group", - "isMut": false, - "isSigner": false - }, - { - "name": "account", - "isMut": true, - "isSigner": false, - "relations": [ - "group" - ] - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, { "name": "openbookV2Market", "isMut": false, "isSigner": false, "relations": [ "group", - "openbook_v2_program", - "openbook_v2_market_external" + "openbook_v2_market_external", + "openbook_v2_program" ] }, { @@ -21285,17 +21840,7 @@ export const IDL: MangoV4 = { "isSigner": false }, { - "name": "marketRequestQueue", - "isMut": true, - "isSigner": false - }, - { - "name": "marketBaseVault", - "isMut": true, - "isSigner": false - }, - { - "name": "marketQuoteVault", + "name": "marketVault", "isMut": true, "isSigner": false }, @@ -21309,7 +21854,7 @@ export const IDL: MangoV4 = { "isMut": true, "isSigner": false, "docs": [ - "The bank that pays for the order, if necessary" + "The bank that pays for the order. Bank oracle also expected in remaining_accounts" ], "relations": [ "group" @@ -21320,13 +21865,19 @@ export const IDL: MangoV4 = { "isMut": true, "isSigner": false, "docs": [ - "The bank vault that pays for the order, if necessary" + "The bank vault that pays for the order" ] }, { - "name": "payerOracle", - "isMut": false, - "isSigner": false + "name": "receiverBank", + "isMut": true, + "isSigner": false, + "docs": [ + "The bank that receives the funds upon settlement. Bank oracle also expected in remaining_accounts" + ], + "relations": [ + "group" + ] }, { "name": "tokenProgram", @@ -21337,31 +21888,49 @@ export const IDL: MangoV4 = { "args": [ { "name": "side", - "type": "u8" + "type": { + "defined": "OpenbookV2Side" + } }, { - "name": "limitPrice", - "type": "u64" + "name": "priceLots", + "type": "i64" }, { - "name": "maxBaseQty", - "type": "u64" + "name": "maxBaseLots", + "type": "i64" }, { - "name": "maxNativeQuoteQtyIncludingFees", - "type": "u64" - }, - { - "name": "selfTradeBehavior", - "type": "u8" + "name": "maxQuoteLotsIncludingFees", + "type": "i64" }, { "name": "clientOrderId", "type": "u64" }, + { + "name": "orderType", + "type": { + "defined": "OpenbookV2PlaceOrderType" + } + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": "OpenbookV2SelfTradeBehavior" + } + }, + { + "name": "reduceOnly", + "type": "bool" + }, + { + "name": "expiryTimestamp", + "type": "u64" + }, { "name": "limit", - "type": "u16" + "type": "u8" } ] }, @@ -21429,7 +21998,9 @@ export const IDL: MangoV4 = { "args": [ { "name": "side", - "type": "u8" + "type": { + "defined": "OpenbookV2Side" + } }, { "name": "orderId", @@ -21455,7 +22026,7 @@ export const IDL: MangoV4 = { }, { "name": "authority", - "isMut": false, + "isMut": true, "isSigner": true }, { @@ -21481,7 +22052,11 @@ export const IDL: MangoV4 = { { "name": "openbookV2MarketExternal", "isMut": true, - "isSigner": false + "isSigner": false, + "relations": [ + "market_base_vault", + "market_quote_vault" + ] }, { "name": "marketBaseVault", @@ -21541,6 +22116,11 @@ export const IDL: MangoV4 = { "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false } ], "args": [ @@ -21566,6 +22146,11 @@ export const IDL: MangoV4 = { "group" ] }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, { "name": "openOrders", "isMut": true, @@ -21588,12 +22173,14 @@ export const IDL: MangoV4 = { }, { "name": "openbookV2MarketExternal", - "isMut": false, + "isMut": true, "isSigner": false, "relations": [ "bids", "asks", - "event_heap" + "event_heap", + "market_base_vault", + "market_quote_vault" ] }, { @@ -21656,6 +22243,11 @@ export const IDL: MangoV4 = { "name": "tokenProgram", "isMut": false, "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false } ], "args": [ @@ -21730,6 +22322,14 @@ export const IDL: MangoV4 = { { "name": "limit", "type": "u8" + }, + { + "name": "sideOpt", + "type": { + "option": { + "defined": "OpenbookV2Side" + } + } } ] }, @@ -22120,7 +22720,7 @@ export const IDL: MangoV4 = { { "name": "potentialSerumTokens", "docs": [ - "Largest amount of tokens that might be added the the bank based on", + "Largest amount of tokens that might be added the bank based on", "serum open order execution." ], "type": "u64" @@ -22230,12 +22830,29 @@ export const IDL: MangoV4 = { ], "type": "f32" }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "potentialOpenbookTokens", + "docs": [ + "Largest amount of tokens that might be added the bank based on", + "oenbook open order execution." + ], + "type": "u64" + }, { "name": "reserved", "type": { "array": [ "u8", - 1900 + 1888 ] } } @@ -22598,12 +23215,24 @@ export const IDL: MangoV4 = { } } }, + { + "name": "padding9", + "type": "u32" + }, + { + "name": "openbookV2", + "type": { + "vec": { + "defined": "OpenbookV2Orders" + } + } + }, { "name": "reservedDynamic", "type": { "array": [ "u8", - 64 + 56 ] } } @@ -22699,6 +23328,10 @@ export const IDL: MangoV4 = { "name": "quoteTokenIndex", "type": "u16" }, + { + "name": "marketIndex", + "type": "u16" + }, { "name": "reduceOnly", "type": "u8" @@ -22707,15 +23340,6 @@ export const IDL: MangoV4 = { "name": "forceClose", "type": "u8" }, - { - "name": "padding1", - "type": { - "array": [ - "u8", - 2 - ] - } - }, { "name": "name", "type": { @@ -22734,32 +23358,29 @@ export const IDL: MangoV4 = { "type": "publicKey" }, { - "name": "marketIndex", - "type": "u16" + "name": "registrationTime", + "type": "u64" + }, + { + "name": "oraclePriceBand", + "docs": [ + "Limit orders must be <= oracle * (1+band) and >= oracle / (1+band)", + "", + "Zero value is the default due to migration and disables the limit,", + "same as f32::MAX." + ], + "type": "f32" }, { "name": "bump", "type": "u8" }, - { - "name": "padding2", - "type": { - "array": [ - "u8", - 5 - ] - } - }, - { - "name": "registrationTime", - "type": "u64" - }, { "name": "reserved", "type": { "array": [ "u8", - 512 + 1027 ] } } @@ -23902,6 +24523,121 @@ export const IDL: MangoV4 = { ] } }, + { + "name": "OpenbookV2Orders", + "type": { + "kind": "struct", + "fields": [ + { + "name": "openOrders", + "type": "publicKey" + }, + { + "name": "baseBorrowsWithoutFee", + "docs": [ + "Tracks the amount of borrows that have flowed into the open orders account.", + "These borrows did not have the loan origination fee applied, and that may happen", + "later (in openbook_v2_settle_funds) if we can guarantee that the funds were used.", + "In particular a place-on-book, cancel, settle should not cost fees." + ], + "type": "u64" + }, + { + "name": "quoteBorrowsWithoutFee", + "type": "u64" + }, + { + "name": "highestPlacedBidInv", + "docs": [ + "Track something like the highest open bid / lowest open ask, in native/native units.", + "", + "Tracking it exactly isn't possible since we don't see fills. So instead track", + "the min/max of the _placed_ bids and asks.", + "", + "The value is reset in openbook_v2_place_order when a new order is placed without an", + "existing one on the book.", + "", + "0 is a special \"unset\" state." + ], + "type": "f64" + }, + { + "name": "lowestPlacedAsk", + "type": "f64" + }, + { + "name": "potentialBaseTokens", + "docs": [ + "An overestimate of the amount of tokens that might flow out of the open orders account.", + "", + "The bank still considers these amounts user deposits (see Bank::potential_openbook_tokens)", + "and that value needs to be updated in conjunction with these numbers.", + "", + "This estimation is based on the amount of tokens in the open orders account", + "(see update_bank_potential_tokens() in openbook_v2_place_order and settle)" + ], + "type": "u64" + }, + { + "name": "potentialQuoteTokens", + "type": "u64" + }, + { + "name": "lowestPlacedBidInv", + "docs": [ + "Track lowest bid/highest ask, same way as for highest bid/lowest ask.", + "", + "0 is a special \"unset\" state." + ], + "type": "f64" + }, + { + "name": "highestPlacedAsk", + "type": "f64" + }, + { + "name": "quoteLotSize", + "docs": [ + "Stores the market's lot sizes", + "", + "Needed because the obv2 open orders account tells us about reserved amounts in lots and", + "we want to be able to compute health without also loading the obv2 market." + ], + "type": "i64" + }, + { + "name": "baseLotSize", + "type": "i64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "baseTokenIndex", + "docs": [ + "Store the base/quote token index, so health computations don't need", + "to get passed the static SerumMarket to find which tokens a market", + "uses and look up the correct oracles." + ], + "type": "u16" + }, + { + "name": "quoteTokenIndex", + "type": "u16" + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 162 + ] + } + } + ] + } + }, { "name": "PerpPosition", "type": { @@ -25249,6 +25985,77 @@ export const IDL: MangoV4 = { ] } }, + { + "name": "OpenbookV2PlaceOrderType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Limit" + }, + { + "name": "ImmediateOrCancel" + }, + { + "name": "PostOnly" + }, + { + "name": "Market" + }, + { + "name": "PostOnlySlide" + } + ] + } + }, + { + "name": "OpenbookV2PostOrderType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Limit" + }, + { + "name": "PostOnly" + }, + { + "name": "PostOnlySlide" + } + ] + } + }, + { + "name": "OpenbookV2SelfTradeBehavior", + "type": { + "kind": "enum", + "variants": [ + { + "name": "DecrementTake" + }, + { + "name": "CancelProvide" + }, + { + "name": "AbortTransaction" + } + ] + } + }, + { + "name": "OpenbookV2Side", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Bid" + }, + { + "name": "Ask" + } + ] + } + }, { "name": "Serum3SelfTradeBehavior", "docs": [ @@ -25333,6 +26140,26 @@ export const IDL: MangoV4 = { ] } }, + { + "name": "SpotMarketIndex", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Serum3", + "fields": [ + "u16" + ] + }, + { + "name": "OpenbookV2", + "fields": [ + "u16" + ] + } + ] + } + }, { "name": "LoanOriginationFeeInstruction", "type": { @@ -25361,6 +26188,15 @@ export const IDL: MangoV4 = { }, { "name": "TokenConditionalSwapTrigger" + }, + { + "name": "OpenbookV2LiqForceCancelOrders" + }, + { + "name": "OpenbookV2PlaceOrder" + }, + { + "name": "OpenbookV2SettleFunds" } ] } @@ -25609,6 +26445,9 @@ export const IDL: MangoV4 = { }, { "name": "HealthCheck" + }, + { + "name": "GroupChangeInsuranceFund" } ] } @@ -28972,6 +29811,16 @@ export const IDL: MangoV4 = { "code": 6072, "name": "InvalidHealth", "msg": "invalid health" + }, + { + "code": 6073, + "name": "NoFreeOpenbookV2OpenOrdersIndex", + "msg": "no free openbook v2 open orders index" + }, + { + "code": 6074, + "name": "OpenbookV2OpenOrdersExistAlready", + "msg": "openbook v2 open orders exist already" } ] };