mango-v4/programs/mango-v4/src/instructions/token_liq_with_token.rs

804 lines
30 KiB
Rust

use anchor_lang::prelude::*;
use fixed::types::I80F48;
use crate::accounts_ix::*;
use crate::error::*;
use crate::health::*;
use crate::logs::{
emit_stack, LoanOriginationFeeInstruction, TokenBalanceLog, TokenLiqWithTokenLogV2,
WithdrawLoanLog,
};
use crate::state::*;
pub fn token_liq_with_token(
ctx: Context<TokenLiqWithToken>,
asset_token_index: TokenIndex,
liab_token_index: TokenIndex,
max_liab_transfer: I80F48,
) -> Result<()> {
let group_pk = &ctx.accounts.group.key();
require!(asset_token_index != liab_token_index, MangoError::SomeError);
let mut account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)
.context("create account retriever")?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
require_keys_neq!(ctx.accounts.liqor.key(), ctx.accounts.liqee.key());
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 mut liqee = ctx.accounts.liqee.load_full_mut()?;
// Initial liqee health check
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever, now_ts)
.context("create liqee health cache")?;
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
liqee_health_cache.require_after_phase1_liquidation()?;
if liqee.check_liquidatable(&liqee_health_cache)? != CheckLiquidatable::Liquidatable {
return Ok(());
}
//
// Transfer some liab_token from liqor to liqee and
// transfer some asset_token from liqee to liqor.
//
liquidation_action(
&mut account_retriever,
liab_token_index,
asset_token_index,
&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,
)?;
// Check liqor's health
if !liqor.fixed.is_in_health_region() {
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(
account_retriever: &mut ScanningAccountRetriever,
liab_token_index: TokenIndex,
asset_token_index: TokenIndex,
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: I80F48,
) -> Result<()> {
let liq_end_type = HealthType::LiquidationEnd;
// Get the mut banks and oracle prices
//
// This must happen _after_ the health computation, since immutable borrows of
// the bank are not allowed at the same time.
let (asset_bank, asset_oracle_price, opt_liab_bank_and_price) =
account_retriever.banks_mut_and_oracles(asset_token_index, liab_token_index)?;
let (liab_bank, liab_oracle_price) = opt_liab_bank_and_price.unwrap();
// The main complication here is that we can't keep the liqee_asset_position and liqee_liab_position
// borrows alive at the same time. Possibly adding get_mut_pair() would be helpful.
let (liqee_asset_position, liqee_asset_raw_index) =
liqee.token_position_and_raw_index(asset_token_index)?;
let liqee_asset_native = liqee_asset_position.native(asset_bank);
require_gt!(liqee_asset_native, 0);
require!(
asset_bank.allows_asset_liquidation(),
MangoError::TokenAssetLiquidationDisabled
);
let (liqee_liab_position, liqee_liab_raw_index) =
liqee.token_position_and_raw_index(liab_token_index)?;
let liqee_liab_native = liqee_liab_position.native(liab_bank);
require_gt!(0, liqee_liab_native);
// The liqor will likely close the borrow by buying liab tokens somewhere and get rid of the
// asset tokens by selling them. Both transactions may incur slippage, so to make sure liqors
// are willing to perform the liquidation, they receive a liquidation fee.
//
// Liquidation fees work by giving the liqor more assets than the oracle price would indicate.
//
// Specifically we choose
// assets =
// liabs * liab_oracle_price/asset_oracle_price * (1 + liab_liq_fee) * (1 + asset_liq_fee)
// Which is equivalent to using an increased liab oracle price for the conversion.
// For simplicity we write
// assets = liabs * liab_oracle_price / asset_oracle_price * fee_factor
// assets = liabs * liab_oracle_price_adjusted / asset_oracle_price
// = liabs * lopa / aop
let fee_factor_liqor =
(I80F48::ONE + liab_bank.liquidation_fee) * (I80F48::ONE + asset_bank.liquidation_fee);
let fee_factor_total =
(I80F48::ONE + liab_bank.liquidation_fee + liab_bank.platform_liquidation_fee)
* (I80F48::ONE + asset_bank.liquidation_fee + asset_bank.platform_liquidation_fee);
let liab_oracle_price_adjusted = liab_oracle_price * fee_factor_total;
let init_asset_weight = asset_bank.init_asset_weight;
let init_liab_weight = liab_bank.init_liab_weight;
// The price the LiquidationEnd health computation uses for a liability of one native liab token
let liab_liq_end_price = liqee_health_cache
.token_info(liab_token_index)
.unwrap()
.prices
.liab(liq_end_type);
// Health price for an asset of one native asset token
let asset_liq_end_price = liqee_health_cache
.token_info(asset_token_index)
.unwrap()
.prices
.asset(liq_end_type);
let liqee_health_token_balances = liqee_health_cache.effective_token_balances(liq_end_type);
// At this point we've established that the liqee has a negative liab token position.
// However, the hupnl from perp markets can bring the health contribution for the token
// to a positive value.
// We'll only liquidate while the health token position is negative and rely on perp
// liquidation to offset the remainder by converting the upnl to a real spot position.
// At the same time an account with a very negative hupnl should not cause a large
// token liquidation: it'd be a way for perp losses to escape into the spot world. Only
// liquidate while the actual spot position is negative.
let liqee_liab_health_balance = liqee_health_token_balances
[liqee_health_cache.token_info_index(liab_token_index)?]
.spot_and_perp;
let max_liab_liquidation = max_liab_transfer
.min(-liqee_liab_native)
.min(-liqee_liab_health_balance)
.max(I80F48::ZERO);
// Similarly to the above, we should only reduce the asset token position while the
// health token balance and the actual token balance stay positive. Otherwise we'd be
// creating a new liability once perp upnl is settled.
let liqee_asset_health_balance = liqee_health_token_balances
[liqee_health_cache.token_info_index(asset_token_index)?]
.spot_and_perp;
let max_asset_transfer = liqee_asset_native
.min(liqee_asset_health_balance)
.max(I80F48::ZERO);
require_gt!(max_liab_liquidation, 0);
require_gt!(max_asset_transfer, 0);
// How much asset would need to be exchanged to liab in order to bring health to 0?
//
// That means: what is x (unit: native liab tokens) such that
// init_health
// + x * ilw * llep health gain from reducing liabs
// - y * iaw * alep health loss from paying asset
// = 0
// where
// ilw = init_liab_weight,
// llep = liab_liq_end_price,
// lopa = liab_oracle_price_adjusted, (see above)
// iaw = init_asset_weight,
// alep = asset_liq_end_price,
// aop = asset_oracle_price
// ff = fee_factor
// and the asset cost of getting x native units of liab is:
// y = x * lopa / aop (native asset tokens, see above)
//
// Result: x = -init_health / (ilw * llep - iaw * lopa * alep / aop)
//
// Simplified for alep == aop:
assert!(asset_liq_end_price == asset_oracle_price);
let liab_needed = -liqee_liq_end_health
/ (liab_liq_end_price * init_liab_weight - liab_oracle_price_adjusted * init_asset_weight);
// How much liab can we get at most for the asset balance?
let liab_possible = max_asset_transfer * asset_oracle_price / liab_oracle_price_adjusted;
// The amount of liab native tokens we will transfer
let liab_transfer = liab_needed
.min(liab_possible)
.min(max_liab_liquidation)
.max(I80F48::ZERO);
// The amount of asset native tokens we will give up for them
let asset_transfer_base = liab_transfer * liab_oracle_price / asset_oracle_price;
let asset_transfer_to_liqor = asset_transfer_base * fee_factor_liqor;
let asset_transfer_from_liqee = asset_transfer_base * fee_factor_total;
let asset_liquidation_fee = asset_transfer_from_liqee - asset_transfer_to_liqor;
asset_bank.collected_fees_native += asset_liquidation_fee;
asset_bank.collected_liquidation_fees += asset_liquidation_fee;
// During liquidation, we mustn't leave small positive balances in the liqee. Those
// could break bankruptcy-detection. Thus we dust them even if the token position
// is nominally in-use.
// Apply the balance changes to the liqor and liqee accounts
let liqee_liab_position = liqee.token_position_mut_by_raw_index(liqee_liab_raw_index);
let liqee_liab_active =
liab_bank.deposit_with_dusting(liqee_liab_position, liab_transfer, now_ts)?;
let liqee_liab_indexed_position = liqee_liab_position.indexed_position;
let (liqor_liab_position, liqor_liab_raw_index, _) =
liqor.ensure_token_position(liab_token_index)?;
let liqor_liab_withdraw_result =
liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer, now_ts)?;
let liqor_liab_indexed_position = liqor_liab_position.indexed_position;
let liqee_liab_native_after = liqee_liab_position.native(liab_bank);
let (liqor_asset_position, liqor_asset_raw_index, _) =
liqor.ensure_token_position(asset_token_index)?;
let liqor_asset_active =
asset_bank.deposit(liqor_asset_position, asset_transfer_to_liqor, now_ts)?;
let liqor_asset_indexed_position = liqor_asset_position.indexed_position;
let liqee_asset_position = liqee.token_position_mut_by_raw_index(liqee_asset_raw_index);
let liqee_asset_active = asset_bank.withdraw_without_fee_with_dusting(
liqee_asset_position,
asset_transfer_from_liqee,
now_ts,
)?;
let liqee_asset_indexed_position = liqee_asset_position.indexed_position;
let liqee_assets_native_after = liqee_asset_position.native(asset_bank);
// Update the health cache
liqee_health_cache
.adjust_token_balance(liab_bank, liqee_liab_native_after - liqee_liab_native)?;
liqee_health_cache
.adjust_token_balance(asset_bank, liqee_assets_native_after - liqee_asset_native)?;
msg!(
"liquidated {} liab for {} asset",
liab_transfer,
asset_transfer_from_liqee,
);
// liqee asset
emit_stack(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqee_key,
token_index: asset_token_index,
indexed_position: liqee_asset_indexed_position.to_bits(),
deposit_index: asset_bank.deposit_index.to_bits(),
borrow_index: asset_bank.borrow_index.to_bits(),
});
// liqee liab
emit_stack(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqee_key,
token_index: liab_token_index,
indexed_position: liqee_liab_indexed_position.to_bits(),
deposit_index: liab_bank.deposit_index.to_bits(),
borrow_index: liab_bank.borrow_index.to_bits(),
});
// liqor asset
emit_stack(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqor_key,
token_index: asset_token_index,
indexed_position: liqor_asset_indexed_position.to_bits(),
deposit_index: asset_bank.deposit_index.to_bits(),
borrow_index: asset_bank.borrow_index.to_bits(),
});
// liqor liab
emit_stack(TokenBalanceLog {
mango_group: liqee.fixed.group,
mango_account: liqor_key,
token_index: liab_token_index,
indexed_position: liqor_liab_indexed_position.to_bits(),
deposit_index: liab_bank.deposit_index.to_bits(),
borrow_index: liab_bank.borrow_index.to_bits(),
});
if liqor_liab_withdraw_result
.loan_origination_fee
.is_positive()
{
emit_stack(WithdrawLoanLog {
mango_group: liqee.fixed.group,
mango_account: liqor_key,
token_index: liab_token_index,
loan_amount: liqor_liab_withdraw_result.loan_amount.to_bits(),
loan_origination_fee: liqor_liab_withdraw_result.loan_origination_fee.to_bits(),
instruction: LoanOriginationFeeInstruction::LiqTokenWithToken,
price: Some(liab_oracle_price.to_bits()),
});
}
// Since we use a scanning account retriever, it's safe to deactivate inactive token positions
if !liqee_asset_active {
liqee.deactivate_token_position_and_log(liqee_asset_raw_index, liqee_key);
}
if !liqee_liab_active {
liqee.deactivate_token_position_and_log(liqee_liab_raw_index, liqee_key);
}
if !liqor_asset_active {
liqor.deactivate_token_position_and_log(liqor_asset_raw_index, liqor_key);
}
if !liqor_liab_withdraw_result.position_is_active {
liqor.deactivate_token_position_and_log(liqor_liab_raw_index, liqor_key)
}
// Check liqee health again
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
liqee
.fixed
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
emit_stack(TokenLiqWithTokenLogV2 {
mango_group: liqee.fixed.group,
liqee: liqee_key,
liqor: liqor_key,
asset_token_index,
liab_token_index,
asset_transfer_from_liqee: asset_transfer_from_liqee.to_bits(),
asset_transfer_to_liqor: asset_transfer_to_liqor.to_bits(),
asset_liquidation_fee: asset_liquidation_fee.to_bits(),
liab_transfer: liab_transfer.to_bits(),
asset_price: asset_oracle_price.to_bits(),
liab_price: liab_oracle_price.to_bits(),
bankruptcy: !liqee_health_cache.has_phase2_liquidatable()
& liqee_liq_end_health.is_negative(),
});
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::health::{self, test::*};
#[derive(Clone)]
struct TestSetup {
group: Pubkey,
asset_bank: TestAccount<Bank>,
liab_bank: TestAccount<Bank>,
other_bank: TestAccount<Bank>,
asset_oracle: TestAccount<StubOracle>,
liab_oracle: TestAccount<StubOracle>,
other_oracle: TestAccount<StubOracle>,
perp_market_asset: TestAccount<PerpMarket>,
perp_oracle_asset: TestAccount<StubOracle>,
perp_market_liab: TestAccount<PerpMarket>,
perp_oracle_liab: TestAccount<StubOracle>,
liqee: MangoAccountValue,
liqor: MangoAccountValue,
}
impl TestSetup {
fn new() -> Self {
let group = Pubkey::new_unique();
let (asset_bank, asset_oracle) = mock_bank_and_oracle(group, 0, 1.0, 0.0, 0.0);
let (liab_bank, liab_oracle) = mock_bank_and_oracle(group, 1, 1.0, 0.0, 0.0);
let (_bank3, perp_oracle_asset) = mock_bank_and_oracle(group, 4, 1.0, 0.5, 0.3);
let mut perp_market_asset = mock_perp_market(
group,
perp_oracle_asset.pubkey,
1.0,
9,
(0.2, 0.1),
(0.2, 0.1),
);
perp_market_asset.data().settle_token_index = 1;
perp_market_asset.data().base_lot_size = 1;
let (_bank4, perp_oracle_liab) = mock_bank_and_oracle(group, 5, 1.0, 0.5, 0.3);
let mut perp_market_liab = mock_perp_market(
group,
perp_oracle_liab.pubkey,
1.0,
10,
(0.2, 0.1),
(0.2, 0.1),
);
perp_market_liab.data().settle_token_index = 0;
perp_market_liab.data().base_lot_size = 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(0).unwrap();
liqee.ensure_token_position(1).unwrap();
liqee.ensure_token_position(2).unwrap();
liqee.ensure_perp_position(9, 1).unwrap();
liqee.ensure_perp_position(10, 0).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();
}
Self {
group,
asset_bank,
liab_bank,
other_bank,
asset_oracle,
liab_oracle,
other_oracle,
perp_market_asset,
perp_oracle_asset,
perp_market_liab,
perp_oracle_liab,
liqee,
liqor,
}
}
fn liqee_health_cache(&self) -> HealthCache {
let mut setup = self.clone();
let ais = vec![
setup.asset_bank.as_account_info(),
setup.liab_bank.as_account_info(),
setup.other_bank.as_account_info(),
setup.asset_oracle.as_account_info(),
setup.liab_oracle.as_account_info(),
setup.other_oracle.as_account_info(),
setup.perp_market_asset.as_account_info(),
setup.perp_market_liab.as_account_info(),
setup.perp_oracle_asset.as_account_info(),
setup.perp_oracle_liab.as_account_info(),
];
let retriever =
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap()
}
fn run(&self, max_liab_transfer: I80F48) -> Result<Self> {
let mut setup = self.clone();
let ais = vec![
setup.asset_bank.as_account_info(),
setup.liab_bank.as_account_info(),
setup.other_bank.as_account_info(),
setup.asset_oracle.as_account_info(),
setup.liab_oracle.as_account_info(),
setup.other_oracle.as_account_info(),
setup.perp_market_asset.as_account_info(),
setup.perp_market_liab.as_account_info(),
setup.perp_oracle_asset.as_account_info(),
setup.perp_oracle_liab.as_account_info(),
];
let mut retriever =
ScanningAccountRetriever::new_with_staleness(&ais, &setup.group, None).unwrap();
let mut liqee_health_cache =
health::new_health_cache(&setup.liqee.borrow(), &retriever, 0).unwrap();
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
liquidation_action(
&mut retriever,
1,
0,
&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,
)?;
drop(retriever);
drop(ais);
Ok(setup)
}
}
fn asset_p(account: &mut MangoAccountValue) -> &mut TokenPosition {
account.token_position_mut(0).unwrap().0
}
fn liab_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 asset_perp_p(account: &mut MangoAccountValue) -> &mut PerpPosition {
account.perp_position_mut(10).unwrap()
}
fn liab_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}");
};
}
// Check that stable price and weight scaling does not affect liquidation targets
#[test]
fn test_liq_with_token_stable_price() {
let mut setup = TestSetup::new();
{
let ab = setup.asset_bank.data();
ab.stable_price_model.stable_price = 0.5;
ab.deposit_weight_scale_start_quote = 505.0;
let lb = setup.liab_bank.data();
lb.stable_price_model.stable_price = 1.25;
lb.borrow_weight_scale_start_quote = 3.75;
lb.init_liab_weight = I80F48::from_num(1.4);
lb.maint_liab_weight = I80F48::from_num(1.2);
}
{
let asset_bank = setup.asset_bank.data();
asset_bank
.change_without_fee(asset_p(&mut setup.liqee), I80F48::from_num(10.0), 0)
.unwrap();
asset_bank
.change_without_fee(asset_p(&mut setup.liqor), I80F48::from_num(1000.0), 0)
.unwrap();
let liab_bank = setup.liab_bank.data();
liab_bank
.change_without_fee(liab_p(&mut setup.liqor), I80F48::from_num(1000.0), 0)
.unwrap();
liab_bank
.change_without_fee(liab_p(&mut setup.liqee), I80F48::from_num(-9.0), 0)
.unwrap();
}
let hc = setup.liqee_health_cache();
let asset_scale = 505.0 / 1010.0;
let liab_scale = 9.0 * 1.25 / 3.75;
assert_eq_f!(
hc.health(HealthType::Init),
10.0 * 0.5 * asset_scale - 9.0 * 1.25 * 1.4 * liab_scale,
0.1
);
assert_eq_f!(hc.health(HealthType::LiquidationEnd), 10.0 - 9.0 * 1.4, 0.1);
assert_eq_f!(hc.health(HealthType::Maint), 10.0 - 9.0 * 1.2, 0.1);
let mut result = setup.run(I80F48::from(100)).unwrap();
let liqee_asset = asset_p(&mut result.liqee);
assert_eq_f!(liqee_asset.native(&result.asset_bank.data()), 3.5, 0.01);
let liqee_liab = liab_p(&mut result.liqee);
assert_eq_f!(liqee_liab.native(&result.liab_bank.data()), -2.5, 0.01);
let hc = result.liqee_health_cache();
assert_eq_f!(hc.health(HealthType::LiquidationEnd), 0.0, 0.01);
}
#[test]
fn test_liq_with_token_while_perp() {
let test_cases = vec![
(
"nothing",
(0.9, 0.9, 0.9),
(0.0, 0.0, 0.0, 0.0, 0.0),
(false, 0.0, 0.0),
100,
),
(
"no liabs1",
(0.9, 0.9, 0.9),
(1.0, 0.0, 0.0, 0.0, 0.0),
(false, 0.0, 0.0),
100,
),
(
"no liabs2",
(0.9, 0.9, 0.9),
(1.0, 0.0, 1.0, -1.0, 0.0),
(false, 0.0, 0.0),
100,
),
(
"no assets1",
(0.9, 0.9, 0.9),
(0.0, 0.0, -1.0, 0.0, 0.0),
(false, 0.0, 0.0),
100,
),
(
"no assets2",
(0.9, 0.9, 0.9),
(99.999, -100.0, -1.0, 0.0, 0.0), // depositing 100 throws this off due to rounding
(false, 0.0, 0.0),
100,
),
(
"no perps1",
(0.9, 0.9, 0.9),
(10.0, 0.0, -11.0, 0.0, 0.0),
(true, 0.0, -1.0),
100,
),
(
"no perps1, limited",
(0.9, 0.9, 0.9),
(10.0, 0.0, -11.0, 0.0, 0.0),
(true, 8.0, -9.0),
2,
),
(
"no perps2",
(0.8, 0.8, 0.8),
(10.0, 0.0, -9.0, 0.0, 0.0),
(true, 3.0, -2.0), // 3 * 0.8 - 2 * 1.2 = 0
100,
),
(
"no perps2, other health1",
(0.8, 0.8, 0.8),
(10.0, 0.0, -9.0, 0.0, 0.4),
(true, 4.0, -3.0), // 4 * 0.8 - 3 * 1.2 + 0.4 = 0
100,
),
(
"no perps2, other health2",
(0.8, 0.8, 0.8),
(10.0, 0.0, -9.0, 0.0, -0.4),
(true, 2.0, -1.0), // 2 * 0.8 - 1 * 1.2 - 0.4 = 0
100,
),
(
"perp assets1",
(0.8, 0.8, 0.5),
(5.0, 6.0, -9.0, 0.0, 0.0),
(true, 0.0, -4.0), // (0 + 0.5 * 6) * 0.8 - 4 * 1.2 is still negative
100,
),
(
"perp assets2",
(0.8, 0.8, 0.5),
(5.0, 14.0, -9.0, 0.0, 0.0),
(true, 2.0, -6.0), // (2 + 0.5 * 14) * 0.8 - 6 * 1.2 = 0
100,
),
(
"perp assets3",
(0.8, 0.8, 0.5),
(0.0, 14.0, -9.0, 0.0, 0.0),
(false, 0.0, -9.0),
100,
),
(
"perp liabs1",
(0.8, 0.8, 0.5),
(10.0, 0.0, -4.0, -5.0, 0.0),
(true, 6.0, 0.0), // 6 * 0.8 - (0 - 5) * 1.2 is still negative
100,
),
(
"perp liabs2",
(0.8, 0.8, 0.5),
(10.0, 0.0, -8.0, -1.0, 0.0),
(true, 3.0, -1.0), // 3 * 0.8 - (-1 - 1) * 1.2 = 0
100,
),
(
"perp liabs3",
(0.8, 0.8, 0.5),
(10.0, 0.0, 0.0, -9.0, 0.0),
(false, 10.0, 0.0),
100,
),
];
for (
name,
(asset_token_weight, liab_token_weight, perp_overall_weight),
// starting position in asset spot/perp and liab spot/perp
(init_asset_token, init_asset_perp, init_liab_token, init_liab_perp, init_other),
// the expected liqee end position
(exp_success, exp_asset_token, exp_liab_token),
// maximum liquidation the liqor requests
max_liab_transfer,
) in test_cases
{
println!("test: {name}");
let mut setup = TestSetup::new();
{
let t = setup.asset_bank.data();
t.init_asset_weight = I80F48::from_num(asset_token_weight);
t.init_liab_weight = I80F48::from_num(2.0 - asset_token_weight);
let t = setup.liab_bank.data();
t.init_asset_weight = I80F48::from_num(liab_token_weight);
t.init_liab_weight = I80F48::from_num(2.0 - liab_token_weight);
let p = setup.perp_market_asset.data();
p.init_overall_asset_weight = I80F48::from_num(perp_overall_weight);
let p = setup.perp_market_liab.data();
p.init_overall_asset_weight = I80F48::from_num(perp_overall_weight);
}
{
asset_perp_p(&mut setup.liqee).quote_position_native =
I80F48::from_num(init_asset_perp);
liab_perp_p(&mut setup.liqee).quote_position_native =
I80F48::from_num(init_liab_perp);
let liab_bank = setup.liab_bank.data();
liab_bank
.change_without_fee(
liab_p(&mut setup.liqee),
I80F48::from_num(init_liab_token),
0,
)
.unwrap();
liab_bank
.change_without_fee(liab_p(&mut setup.liqor), I80F48::from_num(1000.0), 0)
.unwrap();
let asset_bank = setup.asset_bank.data();
asset_bank
.change_without_fee(
asset_p(&mut setup.liqee),
I80F48::from_num(init_asset_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(I80F48::from_num(max_liab_transfer));
if !exp_success {
assert!(result.is_err());
continue;
}
let mut result = result.unwrap();
let liab_bank = result.liab_bank.data();
assert_eq_f!(
liab_p(&mut result.liqee).native(liab_bank),
exp_liab_token,
0.01
);
let asset_bank = result.asset_bank.data();
assert_eq_f!(
asset_p(&mut result.liqee).native(asset_bank),
exp_asset_token,
0.01
);
}
}
}