mango-v4/programs/mango-v4/tests/cases/test_bankrupt_tokens.rs

614 lines
18 KiB
Rust

use super::*;
#[tokio::test]
async fn test_bankrupt_tokens_socialize_loss() -> 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 GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let borrow_token1 = &tokens[0];
let borrow_token2 = &tokens[1];
let collateral_token1 = &tokens[2];
let collateral_token2 = &tokens[3];
// 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,
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.
send_tx(
solana,
TokenDepositInstruction {
amount: 20,
reduce_only: false,
account: vault_account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
//
// SETUP: Make an account with some collateral and some borrows
//
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
let deposit1_amount = 1000;
let deposit2_amount = 20;
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 = 350;
let borrow1_amount_bank0 = 10;
let borrow1_amount_bank1 = borrow1_amount - borrow1_amount_bank0;
let borrow2_amount = 50;
send_tx(
solana,
TokenWithdrawInstruction {
amount: borrow1_amount_bank1,
allow_borrow: true,
account,
owner,
token_account: payer_mint_accounts[0],
bank_index: 1,
},
)
.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: 1,
},
)
.await
.unwrap();
//
// SETUP: Change the oracle to make health go very negative
//
set_bank_stub_oracle_price(solana, group, borrow_token1, admin, 20.0).await;
//
// SETUP: liquidate all the collateral against borrow1
//
// 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_token1.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);
assert_eq!(
account_position(solana, account, borrow_token1.bank).await,
(-350.0f64 + (1000.0 / 20.0 / 1.04)).round() as i64
);
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_token1.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 borrow1_after_liq = -350.0f64 + (1000.0 / 20.0 / 1.04) + (20.0 / 20.0 / 1.04);
assert_eq!(
account_position(solana, account, borrow_token1.bank).await,
borrow1_after_liq.round() as i64
);
let liqee = get_mango_account(solana, account).await;
assert!(liqee.being_liquidated());
//
// TEST: socialize loss on borrow1 and 2
//
let vault_before = account_position(solana, vault_account, borrow_token1.bank).await;
send_tx(
solana,
TokenLiqBankruptcyInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
liab_mint_info: borrow_token1.mint_info,
max_liab_transfer: I80F48::from_num(100000.0),
},
)
.await
.unwrap();
assert_eq!(
account_position(solana, vault_account, borrow_token1.bank).await,
vault_before + (borrow1_after_liq.round() as i64)
);
let liqee = get_mango_account(solana, account).await;
assert!(liqee.being_liquidated());
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
// both bank's borrows were completely wiped: no one else borrowed
let borrow1_bank0: Bank = solana.get_account(borrow_token1.bank).await;
let borrow1_bank1: Bank = solana.get_account(borrow_token1.bank).await;
assert_eq!(borrow1_bank0.native_borrows(), 0);
assert_eq!(borrow1_bank1.native_borrows(), 0);
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(100000.0),
},
)
.await
.unwrap();
assert_eq!(
account_position(solana, vault_account, borrow_token2.bank).await,
(vault_amount - borrow2_amount) as i64
);
let liqee = get_mango_account(solana, account).await;
assert!(!liqee.being_liquidated());
assert!(account_position_closed(solana, account, borrow_token2.bank).await);
Ok(())
}
#[tokio::test]
async fn test_bankrupt_tokens_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];
// 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();
}
// deposit some funds, to the vaults aren't empty
let vault_account = send_tx(
solana,
AccountCreateInstruction {
account_num: 2,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.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();
}
// 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.
send_tx(
solana,
TokenDepositInstruction {
amount: 20,
reduce_only: false,
account: vault_account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
//
// SETUP: Make an account with some collateral and some borrows
//
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
)
.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: 1,
},
)
.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: 1,
},
)
.await
.unwrap();
//
// SETUP: Change the oracle to make health go very negative
//
set_bank_stub_oracle_price(solana, group, borrow_token2, admin, 20.0).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
//
// bankruptcy of an USDC liability: just transfers funds from insurance vault to liqee,
// the liqor is uninvolved
let insurance_vault_before = solana.token_account_balance(insurance_vault).await;
let liqor_before = account_position(solana, vault_account, borrow_token1.bank).await;
send_tx(
solana,
TokenLiqBankruptcyInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
liab_mint_info: borrow_token1.mint_info,
max_liab_transfer: I80F48::from_num(100000.0),
},
)
.await
.unwrap();
let liqee = get_mango_account(solana, account).await;
assert!(liqee.being_liquidated());
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
assert_eq!(
solana.token_account_balance(insurance_vault).await,
// the loan origination fees push the borrow above 50.0 and cause this rounding
insurance_vault_before - borrow1_amount - 1
);
assert_eq!(
account_position(solana, vault_account, borrow_token1.bank).await,
liqor_before
);
// bankruptcy of a non-USDC liability: USDC 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, borrow_token1.bank).await;
let liab_transfer: f64 = 500.0 / 20.0;
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, borrow_token1.bank).await);
assert_eq!(
account_position(solana, account, borrow_token2.bank).await,
(liab_before + liab_transfer) as i64
);
let usdc_amount = (liab_transfer * 20.0 * 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, borrow_token1.bank).await,
liqor_before + usdc_amount as i64
);
// bankruptcy of a non-USDC liability: USDC to liqor, liability to liqee
// liquidating fully and then doing socialized loss because the insurance fund is exhausted
let insurance_vault_before = solana.token_account_balance(insurance_vault).await;
let liqor_before = account_position(solana, vault_account, borrow_token1.bank).await;
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(1000000.0),
},
)
.await
.unwrap();
let liqee = get_mango_account(solana, account).await;
assert!(!liqee.being_liquidated());
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
assert!(account_position_closed(solana, account, borrow_token2.bank).await);
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
assert_eq!(
account_position(solana, vault_account, borrow_token1.bank).await,
liqor_before + insurance_vault_before as i64
);
Ok(())
}