Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Christian Kamm 2024-04-23 11:01:06 +02:00
commit 1ea4528557
19 changed files with 1130 additions and 120 deletions

View File

@ -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(),
},

View File

@ -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"
}
]
}

View File

@ -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(),
},
)
}
}

View File

@ -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;

View File

@ -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>,

View File

@ -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(

View File

@ -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(())
}

View File

@ -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;

View File

@ -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;

View File

@ -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(),

View File

@ -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();

View File

@ -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)?;

View File

@ -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.
}

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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(&quote_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,

View File

@ -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,

View File

@ -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;
}

View File

@ -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"
}
]
}