Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
1ea4528557
|
@ -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};
|
||||
|
@ -1864,13 +1863,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
|
||||
|
@ -2018,10 +2017,15 @@ impl MangoClient {
|
|||
liab_token_index: TokenIndex,
|
||||
max_liab_transfer: I80F48,
|
||||
) -> anyhow::Result<PreparedInstructions> {
|
||||
let mango_account = &self.mango_account().await?;
|
||||
let quote_token_index = 0;
|
||||
let group = account_fetcher_fetch_anchor_account::<Group>(
|
||||
&*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
|
||||
|
@ -2034,18 +2038,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::<Group>(
|
||||
&*self.account_fetcher,
|
||||
&self.context.group,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let ix = Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
|
@ -2056,7 +2054,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(),
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>,
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<GroupChangeInsuranceFund>) -> 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(())
|
||||
}
|
|
@ -99,6 +99,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, 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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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::<Bank>()
|
||||
.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::<u64>()
|
||||
.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(),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -115,6 +115,12 @@ pub mod mango_v4 {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn group_change_insurance_fund(ctx: Context<GroupChangeInsuranceFund>) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::group_change_insurance_fund(ctx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
|
||||
#[cfg(feature = "enable-gpl")]
|
||||
instructions::ix_gate_set(ctx, ix_gate)?;
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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::<MangoAccount>(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::<mango_v4::logs::PerpLiqNegativePnlOrBankruptcyLog>()
|
||||
.pop()
|
||||
.map(|v| limit_prec(I80F48::from_bits(v.settlement).to_num::<f64>()))
|
||||
.unwrap_or(0.0);
|
||||
let (insur, loss) = solana
|
||||
.program_log_events::<mango_v4::logs::PerpLiqBankruptcyLog>()
|
||||
.pop()
|
||||
.map(|v| {
|
||||
(
|
||||
I80F48::from_bits(v.insurance_transfer).to_num::<u64>(),
|
||||
limit_prec(I80F48::from_bits(v.socialized_loss).to_num::<f64>()),
|
||||
)
|
||||
})
|
||||
.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(())
|
||||
}
|
||||
|
|
|
@ -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::<Group>(&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<TestKeypair> {
|
||||
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::<Group>(&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>(&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,
|
||||
|
|
|
@ -368,6 +368,24 @@ export class MangoClient {
|
|||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
||||
}
|
||||
|
||||
public async groupChangeInsuranceFund(
|
||||
group: Group,
|
||||
withdrawDestination: PublicKey,
|
||||
newInsuranceMint: PublicKey,
|
||||
): Promise<MangoSignatureStatus> {
|
||||
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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue