mango-v4/programs/mango-v4/src/instructions/perp_liq_negative_pnl_or_ba...

825 lines
32 KiB
Rust

use std::ops::DerefMut;
use anchor_lang::prelude::*;
use anchor_spl::token::{self, TokenAccount};
use fixed::types::I80F48;
use crate::accounts_ix::*;
use crate::accounts_zerocopy::AccountInfoRef;
use crate::error::*;
use crate::health::*;
use crate::logs::{
emit_perp_balances, emit_stack, PerpLiqBankruptcyLog, PerpLiqNegativePnlOrBankruptcyLog,
TokenBalanceLog,
};
use crate::state::*;
pub fn perp_liq_negative_pnl_or_bankruptcy(
ctx: Context<PerpLiqNegativePnlOrBankruptcyV2>,
max_liab_transfer: u64,
) -> Result<()> {
let mango_group = ctx.accounts.group.key();
let now_slot = Clock::get()?.slot;
let now_ts = Clock::get()?.unix_timestamp.try_into().unwrap();
let perp_market_index;
let settle_token_index;
let perp_oracle_price;
let settle_token_oracle_price;
let insurance_token_oracle_price;
{
let perp_market = ctx.accounts.perp_market.load()?;
perp_market_index = perp_market.perp_market_index;
settle_token_index = perp_market.settle_token_index;
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
perp_oracle_price = perp_market
.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), Some(now_slot))?;
let settle_bank = ctx.accounts.settle_bank.load()?;
let settle_oracle_ref = &AccountInfoRef::borrow(ctx.accounts.settle_oracle.as_ref())?;
settle_token_oracle_price = settle_bank.oracle_price(
&OracleAccountInfos::from_reader(settle_oracle_ref),
Some(now_slot),
)?;
drop(settle_bank); // could be the same as insurance_bank
let insurance_bank = ctx.accounts.insurance_bank.load()?;
let insurance_oracle_ref = &AccountInfoRef::borrow(ctx.accounts.insurance_oracle.as_ref())?;
// We're not getting the insurance token price from the HealthCache because
// the liqee isn't guaranteed to have an insurance fund token position.
insurance_token_oracle_price = insurance_bank.oracle_price(
&OracleAccountInfos::from_reader(insurance_oracle_ref),
Some(now_slot),
)?;
}
require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key());
let mut liqee = ctx.accounts.liqee.load_full_mut()?;
let mut liqor = ctx.accounts.liqor.load_full_mut()?;
// account constraint #1
require!(
liqor
.fixed
.is_owner_or_delegate(ctx.accounts.liqor_owner.key()),
MangoError::SomeError
);
require_msg_typed!(
!liqor.fixed.being_liquidated(),
MangoError::BeingLiquidated,
"liqor account"
);
let retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)
.context("create account retriever")?;
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &retriever, now_ts)?;
drop(retriever);
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
// Guarantees that perp base position is 0 and perp quote position is <= 0.
liqee_health_cache.require_after_phase2_liquidation()?;
if liqee.check_liquidatable(&liqee_health_cache)? != CheckLiquidatable::Liquidatable {
return Ok(());
}
// check positions exist/create them, done early for nicer error messages
{
liqee.perp_position(perp_market_index)?;
liqee.token_position(settle_token_index)?;
liqor.ensure_perp_position(perp_market_index, settle_token_index)?;
liqor.ensure_token_position(settle_token_index)?;
}
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let (settlement, insurance_transfer) = {
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
let mut insurance_bank_opt =
if ctx.accounts.settle_bank.key() != ctx.accounts.insurance_bank.key() {
Some(ctx.accounts.insurance_bank.load_mut()?)
} else {
None
};
liquidation_action(
ctx.accounts.group.key(),
&mut perp_market,
perp_oracle_price,
&mut settle_bank,
settle_token_oracle_price,
insurance_bank_opt.as_mut().map(|v| v.deref_mut()),
insurance_token_oracle_price,
&ctx.accounts.insurance_vault,
&mut liqor.borrow_mut(),
ctx.accounts.liqor.key(),
&mut liqee.borrow_mut(),
ctx.accounts.liqee.key(),
&mut liqee_health_cache,
liqee_liq_end_health,
now_ts,
max_liab_transfer,
)?
};
// Execute the insurance fund transfer if needed
if insurance_transfer > 0 {
let group = ctx.accounts.group.load()?;
let group_seeds = group_seeds!(group);
token::transfer(
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
insurance_transfer,
)?;
}
//
// Log positions afterwards
//
if settlement > 0 {
let settle_bank = ctx.accounts.settle_bank.load()?;
let liqor_token_position = liqor.token_position(settle_token_index)?;
emit_stack(TokenBalanceLog {
mango_group,
mango_account: ctx.accounts.liqor.key(),
token_index: settle_token_index,
indexed_position: liqor_token_position.indexed_position.to_bits(),
deposit_index: settle_bank.deposit_index.to_bits(),
borrow_index: settle_bank.borrow_index.to_bits(),
});
let liqee_token_position = liqee.token_position(settle_token_index)?;
emit_stack(TokenBalanceLog {
mango_group,
mango_account: ctx.accounts.liqee.key(),
token_index: settle_token_index,
indexed_position: liqee_token_position.indexed_position.to_bits(),
deposit_index: settle_bank.deposit_index.to_bits(),
borrow_index: settle_bank.borrow_index.to_bits(),
});
}
if insurance_transfer > 0 {
let insurance_bank = ctx.accounts.insurance_bank.load()?;
let liqor_token_position = liqor.token_position(insurance_bank.token_index)?;
emit_stack(TokenBalanceLog {
mango_group,
mango_account: ctx.accounts.liqor.key(),
token_index: insurance_bank.token_index,
indexed_position: liqor_token_position.indexed_position.to_bits(),
deposit_index: insurance_bank.deposit_index.to_bits(),
borrow_index: insurance_bank.borrow_index.to_bits(),
});
}
let liqee_perp_position = liqee.perp_position(perp_market_index)?;
let liqor_perp_position = liqor.perp_position(perp_market_index)?;
emit_perp_balances(
mango_group,
ctx.accounts.liqor.key(),
liqor_perp_position,
&perp_market,
);
emit_perp_balances(
mango_group,
ctx.accounts.liqee.key(),
liqee_perp_position,
&perp_market,
);
// Check liqee health again: bankruptcy would improve health
liqee_health_cache.recompute_perp_info(liqee_perp_position, &perp_market)?;
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
liqee
.fixed
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
drop(perp_market);
// Check liqor's health
if !liqor.fixed.is_in_health_region() {
let account_retriever =
ScanningAccountRetriever::new(ctx.remaining_accounts, &mango_group)?;
let liqor_health = compute_health(
&liqor.borrow(),
HealthType::Init,
&account_retriever,
now_ts,
)
.context("compute liqor health")?;
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
}
Ok(())
}
pub(crate) fn liquidation_action(
group_key: Pubkey,
perp_market: &mut PerpMarket,
perp_oracle_price: I80F48,
settle_bank: &mut Bank,
settle_token_oracle_price: I80F48,
insurance_bank_opt: Option<&mut Bank>,
insurance_token_oracle_price: I80F48,
insurance_vault: &TokenAccount,
liqor: &mut MangoAccountRefMut,
liqor_key: Pubkey,
liqee: &mut MangoAccountRefMut,
liqee_key: Pubkey,
liqee_health_cache: &mut HealthCache,
liqee_liq_end_health: I80F48,
now_ts: u64,
max_liab_transfer: u64,
) -> Result<(I80F48, u64)> {
let perp_market_index = perp_market.perp_market_index;
let settle_token_index = perp_market.settle_token_index;
let liqee_max_settle = liqee_health_cache.perp_max_settle(settle_token_index)?;
let liqee_health_token_balances =
liqee_health_cache.effective_token_balances(HealthType::LiquidationEnd);
//
// Step 1: Allow the liqor to take over ("settle") negative liqee pnl.
//
// The only limitation is the liqee's perp_max_settle and its perp pnl settle limit.
// This does not change liqee health.
//
let settlement;
let max_settlement_liqee;
let mut liqee_pnl;
{
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
liqee_perp_position.settle_funding(&perp_market);
liqor_perp_position.settle_funding(&perp_market);
liqee_pnl = liqee_perp_position.unsettled_pnl(&perp_market, perp_oracle_price)?;
require_gt!(0, liqee_pnl, MangoError::ProfitabilityMismatch);
// Get settleable pnl on the liqee
liqee_perp_position.update_settle_limit(&perp_market, now_ts);
let liqee_settleable_pnl =
liqee_perp_position.apply_pnl_settle_limit(&perp_market, liqee_pnl);
max_settlement_liqee = liqee_max_settle
.min(-liqee_settleable_pnl)
.max(I80F48::ZERO);
settlement = max_settlement_liqee
.min(I80F48::from(max_liab_transfer))
.max(I80F48::ZERO);
if settlement > 0 {
liqor_perp_position.record_liquidation_quote_change(-settlement);
liqee_perp_position.record_settle(-settlement, &perp_market);
// Update the accounts' perp_spot_transfer statistics.
let settlement_i64 = settlement.round_to_zero().to_num::<i64>();
liqor_perp_position.perp_spot_transfers += settlement_i64;
liqee_perp_position.perp_spot_transfers -= settlement_i64;
liqor.fixed.perp_spot_transfers += settlement_i64;
liqee.fixed.perp_spot_transfers -= settlement_i64;
// Transfer token balance
let liqor_token_position = liqor.token_position_mut(settle_token_index)?.0;
let liqee_token_position = liqee.token_position_mut(settle_token_index)?.0;
settle_bank.deposit(liqor_token_position, settlement, now_ts)?;
settle_bank.withdraw_without_fee(liqee_token_position, settlement, now_ts)?;
liqee_health_cache.adjust_token_balance(&settle_bank, -settlement)?;
emit_stack(PerpLiqNegativePnlOrBankruptcyLog {
mango_group: group_key,
liqee: liqee_key,
liqor: liqor_key,
perp_market_index,
settlement: settlement.to_bits(),
});
liqee_pnl += settlement;
msg!("liquidated pnl = {}", settlement);
}
};
let max_liab_transfer = I80F48::from(max_liab_transfer) - settlement;
// Step 2: bankruptcy
//
// If the liqee still has negative pnl and couldn't possibly be settled further, allow bankruptcy
// to reduce the negative pnl.
//
// Remaining pnl that brings the account into negative init health is either:
// - taken by the liqor in exchange for spot from the insurance fund, or
// - wiped away and socialized among all perp participants (this does not involve the liqor)
//
let insurance_transfer;
if settlement == max_settlement_liqee && liqee_pnl < 0 {
// Preparation that's needed for both, insurance fund based pnl takeover and socialized loss
let liqee_settle_token_balance = liqee_health_token_balances
[liqee_health_cache.token_info_index(settle_token_index)?]
.spot_and_perp;
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
// recompute for safety
liqee_pnl = liqee_perp_position.unsettled_pnl(&perp_market, perp_oracle_price)?;
// Each unit of pnl increase (towards 0) increases health, but the amount depends on whether
// the health token position is negative or positive.
// Compute how much pnl would need to be increased to reach liq end health 0 (while ignoring
// liqee_pnl and other constraints initially, those are applied below)
let max_for_health = {
let liab_weighted_price = settle_token_oracle_price * settle_bank.init_liab_weight;
let asset_weighted_price = settle_token_oracle_price * settle_bank.init_asset_weight;
spot_amount_given_for_health_zero(
liqee_liq_end_health,
liqee_settle_token_balance,
asset_weighted_price,
liab_weighted_price,
)?
};
let max_liab_transfer_from_liqee = (-liqee_pnl).min(max_for_health).max(I80F48::ZERO);
let max_liab_transfer_to_liqor = max_liab_transfer_from_liqee
.min(max_liab_transfer)
.max(I80F48::ZERO);
// Check if the insurance fund can be used to reimburse the liqor for taking on negative pnl
// Available insurance fund coverage
let insurance_vault_amount = if perp_market.elligible_for_group_insurance_fund() {
insurance_vault.amount
} else {
0
};
let liquidation_fee_factor = I80F48::ONE + perp_market.base_liquidation_fee;
let settle_token_price_with_fee = settle_token_oracle_price * liquidation_fee_factor;
// Amount given to the liqor from the insurance fund
insurance_transfer = (max_liab_transfer_to_liqor * settle_token_price_with_fee
/ insurance_token_oracle_price)
.ceil()
.to_num::<u64>()
.min(insurance_vault_amount);
let insurance_transfer_i80f48 = I80F48::from(insurance_transfer);
let insurance_fund_exhausted = insurance_transfer == insurance_vault_amount;
// Amount of negative perp pnl transfered to the liqor
let insurance_liab_transfer = (insurance_transfer_i80f48 * insurance_token_oracle_price
/ settle_token_price_with_fee)
.min(max_liab_transfer_to_liqor);
// Try using the insurance fund if possible
if insurance_transfer > 0 {
let insurance_bank = insurance_bank_opt.unwrap_or(settle_bank);
require_keys_eq!(insurance_bank.mint, insurance_vault.mint);
// moving insurance assets into the insurance bank vault happens outside
// of this function to ensure this is unittestable!
// credit the liqor with quote tokens
let (liqor_quote, _, _) = liqor.ensure_token_position(insurance_bank.token_index)?;
insurance_bank.deposit(liqor_quote, insurance_transfer_i80f48, now_ts)?;
// transfer perp quote loss from the liqee to the liqor
let liqor_perp_position = liqor.perp_position_mut(perp_market_index)?;
liqee_perp_position.record_settle(-insurance_liab_transfer, &perp_market);
liqor_perp_position.record_liquidation_quote_change(-insurance_liab_transfer);
msg!(
"bankruptcy: {} pnl for {} insurance",
insurance_liab_transfer,
insurance_transfer
);
}
// Socialize loss if the insurance fund is exhausted
// At this point, we don't care about the liqor's requested max_liab_tranfer
let remaining_liab = max_liab_transfer_from_liqee - insurance_liab_transfer;
let mut socialized_loss = I80F48::ZERO;
let (starting_long_funding, starting_short_funding) =
(perp_market.long_funding, perp_market.short_funding);
if insurance_fund_exhausted && remaining_liab > 0 {
perp_market.socialize_loss(-remaining_liab)?;
liqee_perp_position.record_settle(-remaining_liab, &perp_market);
socialized_loss = remaining_liab;
msg!("socialized loss: {}", socialized_loss);
}
emit_stack(PerpLiqBankruptcyLog {
mango_group: group_key,
liqee: liqee_key,
liqor: liqor_key,
perp_market_index: perp_market.perp_market_index,
insurance_transfer: insurance_transfer_i80f48.to_bits(),
socialized_loss: socialized_loss.to_bits(),
starting_long_funding: starting_long_funding.to_bits(),
starting_short_funding: starting_short_funding.to_bits(),
ending_long_funding: perp_market.long_funding.to_bits(),
ending_short_funding: perp_market.short_funding.to_bits(),
});
} else {
insurance_transfer = 0;
};
Ok((settlement, insurance_transfer))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::health::{self, test::*};
#[derive(Clone)]
struct TestSetup {
group: Pubkey,
insurance_bank: TestAccount<Bank>,
settle_bank: TestAccount<Bank>,
other_bank: TestAccount<Bank>,
insurance_oracle: TestAccount<StubOracle>,
settle_oracle: TestAccount<StubOracle>,
other_oracle: TestAccount<StubOracle>,
perp_market: TestAccount<PerpMarket>,
perp_oracle: TestAccount<StubOracle>,
liqee: MangoAccountValue,
liqor: MangoAccountValue,
insurance_vault: spl_token::state::Account,
}
impl TestSetup {
fn new() -> Self {
let group = Pubkey::new_unique();
let (mut insurance_bank, insurance_oracle) =
mock_bank_and_oracle(group, 0, 1.0, 0.0, 0.0);
let (settle_bank, settle_oracle) = mock_bank_and_oracle(group, 1, 1.0, 0.0, 0.0);
let (_bank3, perp_oracle) = mock_bank_and_oracle(group, 4, 1.0, 0.5, 0.3);
let mut perp_market =
mock_perp_market(group, perp_oracle.pubkey, 1.0, 9, (0.2, 0.1), (0.2, 0.1));
perp_market.data().settle_token_index = 1;
perp_market.data().base_lot_size = 1;
perp_market.data().group_insurance_fund = 1;
let (other_bank, other_oracle) = mock_bank_and_oracle(group, 2, 1.0, 0.0, 0.0);
let liqee_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut liqee = MangoAccountValue::from_bytes(&liqee_buffer).unwrap();
{
liqee.ensure_token_position(1).unwrap();
liqee.ensure_token_position(2).unwrap();
liqee.ensure_perp_position(9, 1).unwrap();
}
let liqor_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut liqor = MangoAccountValue::from_bytes(&liqor_buffer).unwrap();
{
liqor.ensure_token_position(0).unwrap();
liqor.ensure_token_position(1).unwrap();
liqor.ensure_perp_position(9, 1).unwrap();
}
let mut insurance_vault = spl_token::state::Account::default();
insurance_vault.state = spl_token::state::AccountState::Initialized;
insurance_vault.mint = insurance_bank.data().mint;
Self {
group,
insurance_bank,
settle_bank,
other_bank,
insurance_oracle,
settle_oracle,
other_oracle,
perp_market,
perp_oracle,
liqee,
liqor,
insurance_vault,
}
}
fn run(&self, max_liab_transfer: u64) -> Result<Self> {
let mut setup = self.clone();
let mut liqee_health_cache;
let liqee_liq_end_health;
{
let ais = vec![
setup.insurance_bank.as_account_info(),
setup.settle_bank.as_account_info(),
setup.other_bank.as_account_info(),
setup.insurance_oracle.as_account_info(),
setup.settle_oracle.as_account_info(),
setup.other_oracle.as_account_info(),
setup.perp_market.as_account_info(),
setup.perp_oracle.as_account_info(),
];
let retriever =
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
liqee_health_cache =
health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap();
liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
}
let insurance_price = {
let insurance_oracle_ai = setup.insurance_oracle.as_account_info();
let insurance_oracle_ref = &AccountInfoRef::borrow(&insurance_oracle_ai)?;
setup
.insurance_bank
.data()
.oracle_price(&OracleAccountInfos::from_reader(insurance_oracle_ref), None)
.unwrap()
};
let settle_price = {
let settle_oracle_ai = setup.settle_oracle.as_account_info();
let settle_oracle_ref = &AccountInfoRef::borrow(&settle_oracle_ai)?;
setup
.settle_bank
.data()
.oracle_price(&OracleAccountInfos::from_reader(settle_oracle_ref), None)
.unwrap()
};
let perp_price = {
let perp_oracle_ai = setup.perp_oracle.as_account_info();
let perp_oracle_ref = &AccountInfoRef::borrow(&perp_oracle_ai)?;
setup
.perp_market
.data()
.oracle_price(&OracleAccountInfos::from_reader(perp_oracle_ref), None)
.unwrap()
};
// There's no way to construct a TokenAccount directly...
let mut buffer = [0u8; 165];
use solana_program::program_pack::Pack;
setup.insurance_vault.pack_into_slice(&mut buffer);
let insurance_vault =
TokenAccount::try_deserialize_unchecked(&mut &buffer[..]).unwrap();
liquidation_action(
setup.group.key(),
setup.perp_market.data(),
perp_price,
setup.settle_bank.data(),
settle_price,
Some(setup.insurance_bank.data()),
insurance_price,
&insurance_vault,
&mut setup.liqor.borrow_mut(),
Pubkey::new_unique(),
&mut setup.liqee.borrow_mut(),
Pubkey::new_unique(),
&mut liqee_health_cache,
liqee_liq_end_health,
0,
max_liab_transfer,
)?;
Ok(setup)
}
}
fn insurance_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(0).unwrap().0
}
fn settle_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(1).unwrap().0
}
fn other_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(2).unwrap().0
}
fn perp_p(account: &mut MangoAccountValue) -> &mut PerpPosition {
account.perp_position_mut(9).unwrap()
}
macro_rules! assert_eq_f {
($value:expr, $expected:expr, $max_error:expr) => {
let value = $value;
let expected = $expected;
let ok = (value.to_num::<f64>() - expected).abs() < $max_error;
assert!(ok, "value: {value}, expected: {expected}");
};
}
#[test]
fn test_liq_negative_pnl_or_bankruptcy() {
let test_cases = vec![
(
"nothing",
(0.9, 1.0, 1.0),
(0.0, 0.0, 0.0, 0, 0),
(false, 0.0, 0.0),
(0.0, 0.0, 0.0, 0.0),
100,
),
(
"settle 1",
(0.9, 2.0, 3.0),
// perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
// max settle = 36 / (0.9 * 2.0) = 20
(40.0, -50.0, -36.0, 6, 100),
(true, 30.0, -40.0),
(10.0, -10.0, 0.0, 0.0),
10,
),
(
"settle 2 (+insurance)",
(0.9, 2.0, 3.0),
// perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
// max settle = 36 / (0.9 * 2.0) = 20
(40.0, -50.0, -36.0, 100, 100),
(true, 20.0, -21.0),
(20.0, -29.0, 6.0, 0.0),
29, // limited by max_liab_transfer
),
(
"settle 3 (+insurance)",
(0.9, 2.0, 3.0),
// perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
// max settle = 36 / (0.9 * 2.0) = 20
(40.0, -50.0, -36.0, 100, 10), // limited by settleable pnl
(true, 30.0, -31.0),
(10.0, -19.0, 6.0, 0.0),
19,
),
(
"settle 4 (+socialized loss)",
(0.9, 2.0, 3.0),
// perp_settle_health = 5 * 2.0 * 0.9 + 30 = 39
// max settle = 5 + (39 - 9) / (2*1.1) = 18.63
(5.0, -20.0, 30.0, 0, 100),
(true, -13.63, 0.0),
(18.63, -18.63, 0.0, 1.36), // socialized loss
100,
),
(
"bankruptcy, no insurance 1",
(0.9, 2.0, 1.0),
(0.0, -5.0, 2.2, 0, 0),
(true, 0.0, -1.0), // -1 * 2.0 * 1.1 = 2.2
(0.0, 0.0, 0.0, 4.0),
0,
),
(
"bankruptcy, no insurance 2",
(0.9, 2.0, 1.0),
(4.0, -5.0, -3.6, 0, 0), // health token balance goes from -1 to +2
(true, 4.0, -2.0),
(0.0, 0.0, 0.0, 3.0),
0,
),
(
"bankruptcy, no insurance 3",
(0.9, 2.0, 1.0),
(4.0, -5.0, -3.6, 0, 0),
(true, 4.0, -2.0),
(0.0, 0.0, 0.0, 3.0),
100, // liqor being willing to take over changes nothing
),
(
"bankruptcy, with insurance 1",
(0.9, 2.0, 3.0),
(40.0, -50.0, -36.0, 6, 0),
(true, 40.0, -20.0),
(0.0, -9.0, 6.0, 21.0), // 6 * 3.0 / 2.0 = 9 taken over, rest socialized loss
100,
),
(
"bankruptcy, with insurance 2",
(0.9, 2.0, 3.0),
(40.0, -50.0, -36.0, 6, 0),
(true, 40.0, -47.0),
(0.0, -3.0, 2.0, 0.0),
3, // liqor is limited, don't socialize loss since insurance not exhausted!
),
(
"bankruptcy, with insurance 3",
(0.9, 2.0, 3.0),
(40.0, -50.0, -36.0, 1000, 0), // insurance fund is big enough to cover fully
(true, 40.0, -20.0),
(0.0, -30.0, 20.0, 0.0),
100,
),
(
"everything 1",
(0.9, 2.0, 3.0),
// perp_settle_health = 40 * 2.0 * 0.9 - 36 = 36
// max settle = 36 / (0.9 * 2.0) = 20
(40.0, -50.0, -36.0, 6, 100),
(true, 20.0, 0.0),
(20.0, -29.0, 6.0, 21.0),
40,
),
(
"everything 2",
(0.9, 2.0, 3.0),
// perp_settle_health = 10 * 2.0 * 0.9 + 9 = 19
// max settle = 10 + 9 / (1.1 * 2.0) = 14.1
(10.0, -50.0, 9.0, 6, 100),
(true, -4.1, 0.0), // perp position always goes to 0 because we use the same weights for init and maint
(14.1, -14.1 - 9.0, 6.0, 50.0 - 14.1 - 9.0),
40,
),
];
for (
name,
(settle_token_weight, settle_price, insurance_price),
// starting position
(init_settle_token, init_perp, init_other, insurance_amount, settle_limit),
// the expected liqee end position
(exp_success, exp_liqee_settle_token, exp_liqee_perp),
// expected liqor end position
(exp_liqor_settle_token, exp_liqor_perp, exp_liqor_insurance, exp_socialized_loss),
// maximum liquidation the liqor requests
max_liab_transfer,
) in test_cases
{
println!("test: {name}");
let mut setup = TestSetup::new();
{
let t = setup.settle_bank.data();
t.init_asset_weight = I80F48::from_num(settle_token_weight);
t.init_liab_weight = I80F48::from_num(2.0 - settle_token_weight);
// maint weights used for perp settle health
t.maint_asset_weight = I80F48::from_num(settle_token_weight);
t.maint_liab_weight = I80F48::from_num(2.0 - settle_token_weight);
t.stable_price_model.stable_price = settle_price;
setup.settle_oracle.data().price = I80F48::from_num(settle_price);
let t = setup.insurance_bank.data();
t.stable_price_model.stable_price = insurance_price;
setup.insurance_oracle.data().price = I80F48::from_num(insurance_price);
let p = setup.perp_market.data();
p.init_overall_asset_weight = I80F48::from_num(0.0);
p.open_interest = 1;
setup.insurance_vault.amount = insurance_amount;
}
{
let p = perp_p(&mut setup.liqee);
p.quote_position_native = I80F48::from_num(init_perp);
p.recurring_settle_pnl_allowance = (settle_limit as i64).abs();
let settle_bank = setup.settle_bank.data();
settle_bank
.change_without_fee(
settle_p(&mut setup.liqee),
I80F48::from_num(init_settle_token),
0,
)
.unwrap();
let other_bank = setup.other_bank.data();
other_bank
.change_without_fee(other_p(&mut setup.liqee), I80F48::from_num(init_other), 0)
.unwrap();
}
let result = setup.run(max_liab_transfer);
if !exp_success {
assert!(result.is_err());
continue;
}
let mut result = result.unwrap();
let settle_bank = result.settle_bank.data();
assert_eq_f!(
settle_p(&mut result.liqee).native(settle_bank),
exp_liqee_settle_token,
0.01
);
assert_eq_f!(
settle_p(&mut result.liqor).native(settle_bank),
exp_liqor_settle_token,
0.01
);
let insurance_bank = result.insurance_bank.data();
assert_eq_f!(
insurance_p(&mut result.liqor).native(insurance_bank),
exp_liqor_insurance,
0.01
);
assert_eq_f!(
perp_p(&mut result.liqee).quote_position_native,
exp_liqee_perp,
0.1
);
assert_eq_f!(
perp_p(&mut result.liqor).quote_position_native,
exp_liqor_perp,
0.1
);
assert_eq_f!(
result.perp_market.data().long_funding,
exp_socialized_loss,
0.1
);
}
}
}