Fix perp liq computation; doc liq fee in token liq

This commit is contained in:
Christian Kamm 2022-11-30 10:36:12 +01:00
parent 524fe110e3
commit 9757f7a509
3 changed files with 223 additions and 81 deletions

View File

@ -106,66 +106,63 @@ pub fn perp_liq_base_position(
// Take over the liqee's base in exchange for quote
require_msg!(liqee_base_lots != 0, "liqee base position is zero");
let (base_transfer, quote_transfer) = if liqee_base_lots > 0 {
require_msg!(
max_base_transfer > 0,
"max_base_transfer must be positive when liqee's base_position is positive"
);
let (base_transfer, quote_transfer) =
if liqee_base_lots > 0 {
require_msg!(
max_base_transfer > 0,
"max_base_transfer must be positive when liqee's base_position is positive"
);
// health gets reduced by `base * price * perp_init_asset_weight`
// and increased by `base * price * (1 - liq_fee) * quote_init_asset_weight`
let quote_asset_weight = I80F48::ONE;
let health_per_lot = cm!(price_per_lot
* (quote_asset_weight * (I80F48::ONE - perp_market.liquidation_fee)
- perp_market.init_asset_weight));
// health gets reduced by `base * price * perp_init_asset_weight`
// and increased by `base * price * (1 - liq_fee) * quote_init_asset_weight`
let quote_init_asset_weight = I80F48::ONE;
let fee_factor = cm!(I80F48::ONE - perp_market.liquidation_fee);
let health_per_lot = cm!(price_per_lot
* (-perp_market.init_asset_weight + quote_init_asset_weight * fee_factor));
// number of lots to transfer to bring health to zero, rounded up
let base_transfer_for_zero: i64 = cm!(-liqee_init_health / health_per_lot)
.checked_ceil()
.unwrap()
.checked_to_num()
.unwrap();
// number of lots to transfer to bring health to zero, rounded up
let base_transfer_for_zero: i64 = cm!(-liqee_init_health / health_per_lot)
.checked_ceil()
.unwrap()
.checked_to_num()
.unwrap();
let base_transfer = base_transfer_for_zero
.min(liqee_base_lots)
.min(max_base_transfer)
.max(0);
let quote_transfer = cm!(-I80F48::from(base_transfer)
* price_per_lot
* (I80F48::ONE - perp_market.liquidation_fee));
let base_transfer = base_transfer_for_zero
.min(liqee_base_lots)
.min(max_base_transfer)
.max(0);
let quote_transfer = cm!(-I80F48::from(base_transfer) * price_per_lot * fee_factor);
(base_transfer, quote_transfer) // base > 0, quote < 0
} else {
// liqee_base_lots < 0
require_msg!(
max_base_transfer < 0,
"max_base_transfer must be negative when liqee's base_position is positive"
);
(base_transfer, quote_transfer) // base > 0, quote < 0
} else {
// liqee_base_lots < 0
require_msg!(
max_base_transfer < 0,
"max_base_transfer must be negative when liqee's base_position is positive"
);
// health gets increased by `base * price * perp_init_liab_weight`
// and reduced by `base * price * (1 + liq_fee) * quote_init_liab_weight`
let quote_liab_weight = I80F48::ONE;
let health_per_lot = cm!(price_per_lot
* (perp_market.init_liab_weight * (I80F48::ONE + perp_market.liquidation_fee)
- quote_liab_weight));
// health gets increased by `base * price * perp_init_liab_weight`
// and reduced by `base * price * (1 + liq_fee) * quote_init_liab_weight`
let quote_init_liab_weight = I80F48::ONE;
let fee_factor = cm!(I80F48::ONE + perp_market.liquidation_fee);
let health_per_lot = cm!(price_per_lot
* (perp_market.init_liab_weight - quote_init_liab_weight * fee_factor));
// (negative) number of lots to transfer to bring health to zero, rounded away from zero
let base_transfer_for_zero: i64 = cm!(liqee_init_health / health_per_lot)
.checked_floor()
.unwrap()
.checked_to_num()
.unwrap();
// (negative) number of lots to transfer to bring health to zero, rounded away from zero
let base_transfer_for_zero: i64 = cm!(liqee_init_health / health_per_lot)
.checked_floor()
.unwrap()
.checked_to_num()
.unwrap();
let base_transfer = base_transfer_for_zero
.max(liqee_base_lots)
.max(max_base_transfer)
.min(0);
let quote_transfer = cm!(-I80F48::from(base_transfer)
* price_per_lot
* (I80F48::ONE + perp_market.liquidation_fee));
let base_transfer = base_transfer_for_zero
.max(liqee_base_lots)
.max(max_base_transfer)
.min(0);
let quote_transfer = cm!(-I80F48::from(base_transfer) * price_per_lot * fee_factor);
(base_transfer, quote_transfer) // base < 0, quote > 0
};
(base_transfer, quote_transfer) // base < 0, quote > 0
};
// Execute the transfer. This is essentially a forced trade and updates the
// liqee and liqors entry and break even prices.

View File

@ -101,9 +101,18 @@ pub fn token_liq_with_token(
let liqee_liab_native = liqee_liab_position.native(liab_bank);
require!(liqee_liab_native.is_negative(), MangoError::SomeError);
// TODO why sum of both tokens liquidation fees? Add comment
let fee_factor = I80F48::ONE + asset_bank.liquidation_fee + liab_bank.liquidation_fee;
let liab_price_adjusted = liab_price * fee_factor;
// Liquidation fees work by giving the liqor more assets than the oracle price would
// indicate. Specifically we choose
// assets =
// liabs * liab_price/asset_price * (1 + liab_liq_fee + asset_liq_fee)
// Which means that we use a increased liab price and reduced asset price for the conversion.
// It would be more fully correct to use (1+liab_liq_fee)*(1+asset_liq_fee), but for small
// fee amounts that is nearly identical.
// For simplicity we write
// assets = liabs * liab_price / asset_price * fee_factor
// assets = liabs * liab_price_adjusted / asset_price
let fee_factor = cm!(I80F48::ONE + asset_bank.liquidation_fee + liab_bank.liquidation_fee);
let liab_price_adjusted = cm!(liab_price * fee_factor);
let init_asset_weight = asset_bank.init_asset_weight;
let init_liab_weight = liab_bank.init_liab_weight;

View File

@ -296,20 +296,6 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
)
.await;
send_tx(
solana,
TokenDepositInstruction {
amount: 1,
account,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer,
bank_index: 0,
},
)
.await
.unwrap();
account
};
let account_0 = make_account(0).await;
@ -365,7 +351,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
//
// SETUP: Change the oracle to make health go negative for account_0
//
set_bank_stub_oracle_price(solana, group, base_token, admin, 0.5).await;
set_bank_stub_oracle_price(solana, group, base_token, admin, 0.6).await;
// verify health is bad: can't withdraw
assert!(send_tx(
@ -375,7 +361,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
allow_borrow: false,
account: account_0,
owner,
token_account: payer_mint_accounts[1],
token_account: payer_mint_accounts[0],
bank_index: 0,
}
)
@ -383,7 +369,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
.is_err());
//
// TEST: Liquidate base position
// TEST: Liquidate base position with limit
//
send_tx(
solana,
@ -398,7 +384,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
.await
.unwrap();
let liq_amount = 10.0 * 100.0 * 0.5 * (1.0 - 0.05);
let liq_amount = 10.0 * 100.0 * 0.6 * (1.0 - 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 10);
assert!(assert_equal(
@ -414,10 +400,57 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
0.1
));
//
// TEST: Liquidate base position max
//
send_tx(
solana,
PerpLiqBasePositionInstruction {
liqor,
liqor_owner: owner,
liqee: account_0,
perp_market,
max_base_transfer: i64::MAX,
},
)
.await
.unwrap();
let liq_amount_2 = 4.0 * 100.0 * 0.6 * (1.0 - 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 + 4);
assert!(assert_equal(
liqor_data.perps[0].quote_position_native(),
-liq_amount - liq_amount_2,
0.1
));
let liqee_data = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 6);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
-20.0 * 100.0 + liq_amount + liq_amount_2,
0.1
));
// verify health is good again
send_tx(
solana,
TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account: account_0,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await
.unwrap();
//
// SETUP: Change the oracle to make health go negative for account_1
//
set_bank_stub_oracle_price(solana, group, base_token, admin, 2.0).await;
set_bank_stub_oracle_price(solana, group, base_token, admin, 1.3).await;
// verify health is bad: can't withdraw
assert!(send_tx(
@ -427,7 +460,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
allow_borrow: false,
account: account_1,
owner,
token_account: payer_mint_accounts[1],
token_account: payer_mint_accounts[0],
bank_index: 0,
}
)
@ -437,6 +470,38 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
//
// TEST: Liquidate base position
//
send_tx(
solana,
PerpLiqBasePositionInstruction {
liqor,
liqor_owner: owner,
liqee: account_1,
perp_market,
max_base_transfer: -10,
},
)
.await
.unwrap();
let liq_amount_3 = 10.0 * 100.0 * 1.3 * (1.0 + 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 14 - 10);
assert!(assert_equal(
liqor_data.perps[0].quote_position_native(),
-liq_amount - liq_amount_2 + liq_amount_3,
0.1
));
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), -10);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
20.0 * 100.0 - liq_amount_3,
0.1
));
//
// TEST: Liquidate base position max
//
send_tx(
solana,
PerpLiqBasePositionInstruction {
@ -450,19 +515,71 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
.await
.unwrap();
let liq_amount_2 = 20.0 * 100.0 * 2.0 * (1.0 + 0.05);
let liq_amount_4 = 5.0 * 100.0 * 1.3 * (1.0 + 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), 10 - 20);
assert_eq!(liqor_data.perps[0].base_position_lots(), 4 - 5);
assert!(assert_equal(
liqor_data.perps[0].quote_position_native(),
-liq_amount + liq_amount_2,
-liq_amount - liq_amount_2 + liq_amount_3 + liq_amount_4,
0.1
));
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), -5);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
20.0 * 100.0 - liq_amount_3 - liq_amount_4,
0.1
));
// verify health is good again
send_tx(
solana,
TokenWithdrawInstruction {
amount: 1,
allow_borrow: false,
account: account_1,
owner,
token_account: payer_mint_accounts[0],
bank_index: 0,
},
)
.await
.unwrap();
//
// SETUP: liquidate base position to 0, so bankruptcy can be tested
//
set_bank_stub_oracle_price(solana, group, base_token, admin, 2.0).await;
//
// TEST: Liquidate base position max
//
send_tx(
solana,
PerpLiqBasePositionInstruction {
liqor,
liqor_owner: owner,
liqee: account_1,
perp_market,
max_base_transfer: i64::MIN,
},
)
.await
.unwrap();
let liq_amount_5 = 5.0 * 100.0 * 2.0 * (1.0 + 0.05);
let liqor_data = solana.get_account::<MangoAccount>(liqor).await;
assert_eq!(liqor_data.perps[0].base_position_lots(), -1 - 5);
assert!(assert_equal(
liqor_data.perps[0].quote_position_native(),
-liq_amount - liq_amount_2 + liq_amount_3 + liq_amount_4 + liq_amount_5,
0.1
));
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);
assert!(assert_equal(
liqee_data.perps[0].quote_position_native(),
20.0 * 100.0 - liq_amount_2,
20.0 * 100.0 - liq_amount_3 - liq_amount_4 - liq_amount_5,
0.1
));
@ -482,6 +599,24 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
.await
.is_err());
//
// SETUP: We want pnl settling to cause a negative quote position,
// thus we deposit some base token collateral
//
send_tx(
solana,
TokenDepositInstruction {
amount: 1,
account: account_1,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer,
bank_index: 0,
},
)
.await
.unwrap();
//
// TEST: Can settle-pnl even though health is negative
//
@ -499,8 +634,9 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
.await
.unwrap();
let liqee_settle_health_before = 1000.0 + 1.0 * 2.0 * 0.8;
let remaining_pnl = 20.0 * 100.0 - liq_amount_2 + liqee_settle_health_before;
let liqee_settle_health_before = 999.0 + 1.0 * 2.0 * 0.8;
let remaining_pnl =
20.0 * 100.0 - liq_amount_3 - liq_amount_4 - liq_amount_5 + liqee_settle_health_before;
assert!(remaining_pnl < 0.0);
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);