Change liquidation end target to a new, third health type (#447)
Due to the safety features in v4, the init health can differ from maint health a lot more than it used to in v3. This is because of stable-price adjusted oracle prices used in init health, and the weight scaling based on total deposits and borrows used in init health. The effect is that once an account becomes liquidatable, it could be liquidated a lot until it reaches init>=0. The original idea of liquidating until init>=0 was just to provide some buffer, such that liquidated accounts wouldn't immediately become liquidatable again. This patch decouples the buffer idea explicit from init health by creating a new LiquidationEnd health type. Liquidation proceeds until the LiquidationEnd health becomes positive. Co-authored-by: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com>
This commit is contained in:
parent
b2b029e21e
commit
1f66bef88f
|
@ -32,34 +32,41 @@ impl Prices {
|
|||
/// The liability price to use for the given health type
|
||||
#[inline(always)]
|
||||
pub fn liab(&self, health_type: HealthType) -> I80F48 {
|
||||
if health_type == HealthType::Maint {
|
||||
self.oracle
|
||||
} else {
|
||||
self.oracle.max(self.stable)
|
||||
match health_type {
|
||||
HealthType::Maint | HealthType::LiquidationEnd => self.oracle,
|
||||
HealthType::Init => self.oracle.max(self.stable),
|
||||
}
|
||||
}
|
||||
|
||||
/// The asset price to use for the given health type
|
||||
#[inline(always)]
|
||||
pub fn asset(&self, health_type: HealthType) -> I80F48 {
|
||||
if health_type == HealthType::Maint {
|
||||
self.oracle
|
||||
} else {
|
||||
self.oracle.min(self.stable)
|
||||
match health_type {
|
||||
HealthType::Maint | HealthType::LiquidationEnd => self.oracle,
|
||||
HealthType::Init => self.oracle.min(self.stable),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// There are two types of health, initial health used for opening new positions and maintenance
|
||||
/// health used for liquidations. They are both calculated as a weighted sum of the assets
|
||||
/// minus the liabilities but the maint. health uses slightly larger weights for assets and
|
||||
/// slightly smaller weights for the liabilities. Zero is used as the bright line for both
|
||||
/// i.e. if your init health falls below zero, you cannot open new positions and if your maint. health
|
||||
/// falls below zero you will be liquidated.
|
||||
/// There are three types of health:
|
||||
/// - initial health ("init"): users can only open new positions if it's >= 0
|
||||
/// - maintenance health ("maint"): users get liquidated if it's < 0
|
||||
/// - liquidation end health: once liquidation started (see being_liquidated), it
|
||||
/// only stops once this is >= 0
|
||||
///
|
||||
/// The ordering is
|
||||
/// init health <= liquidation end health <= maint health
|
||||
///
|
||||
/// The different health types are realized by using different weights and prices:
|
||||
/// - init health: init weights with scaling, stable-price adjusted prices
|
||||
/// - liq end health: init weights without scaling, oracle prices
|
||||
/// - maint health: maint weights, oracle prices
|
||||
///
|
||||
#[derive(PartialEq, Copy, Clone, AnchorSerialize, AnchorDeserialize)]
|
||||
pub enum HealthType {
|
||||
Init,
|
||||
Maint,
|
||||
Maint, // aka LiquidationStart
|
||||
LiquidationEnd,
|
||||
}
|
||||
|
||||
/// Computes health for a mango account given a set of account infos
|
||||
|
@ -88,8 +95,10 @@ pub struct TokenInfo {
|
|||
pub token_index: TokenIndex,
|
||||
pub maint_asset_weight: I80F48,
|
||||
pub init_asset_weight: I80F48,
|
||||
pub init_scaled_asset_weight: I80F48,
|
||||
pub maint_liab_weight: I80F48,
|
||||
pub init_liab_weight: I80F48,
|
||||
pub init_scaled_liab_weight: I80F48,
|
||||
pub prices: Prices,
|
||||
pub balance_native: I80F48,
|
||||
}
|
||||
|
@ -98,7 +107,8 @@ impl TokenInfo {
|
|||
#[inline(always)]
|
||||
fn asset_weight(&self, health_type: HealthType) -> I80F48 {
|
||||
match health_type {
|
||||
HealthType::Init => self.init_asset_weight,
|
||||
HealthType::Init => self.init_scaled_asset_weight,
|
||||
HealthType::LiquidationEnd => self.init_asset_weight,
|
||||
HealthType::Maint => self.maint_asset_weight,
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +116,8 @@ impl TokenInfo {
|
|||
#[inline(always)]
|
||||
fn liab_weight(&self, health_type: HealthType) -> I80F48 {
|
||||
match health_type {
|
||||
HealthType::Init => self.init_liab_weight,
|
||||
HealthType::Init => self.init_scaled_liab_weight,
|
||||
HealthType::LiquidationEnd => self.init_liab_weight,
|
||||
HealthType::Maint => self.maint_liab_weight,
|
||||
}
|
||||
}
|
||||
|
@ -284,7 +295,7 @@ impl PerpInfo {
|
|||
pub fn weigh_health_contribution(&self, unweighted: I80F48, health_type: HealthType) -> I80F48 {
|
||||
if unweighted > 0 {
|
||||
let asset_weight = match health_type {
|
||||
HealthType::Init => self.init_overall_asset_weight,
|
||||
HealthType::Init | HealthType::LiquidationEnd => self.init_overall_asset_weight,
|
||||
HealthType::Maint => self.maint_overall_asset_weight,
|
||||
};
|
||||
|
||||
|
@ -299,20 +310,22 @@ impl PerpInfo {
|
|||
let order_execution_case = |orders_base_lots: i64, order_price: I80F48| {
|
||||
let net_base_native =
|
||||
I80F48::from(cm!((self.base_lots + orders_base_lots) * self.base_lot_size));
|
||||
let (weight, base_price) = match (health_type, net_base_native.is_negative()) {
|
||||
(HealthType::Init, true) => {
|
||||
(self.init_base_liab_weight, self.prices.liab(health_type))
|
||||
let weight = match (health_type, net_base_native.is_negative()) {
|
||||
(HealthType::Init, true) | (HealthType::LiquidationEnd, true) => {
|
||||
self.init_base_liab_weight
|
||||
}
|
||||
(HealthType::Init, false) => {
|
||||
(self.init_base_asset_weight, self.prices.asset(health_type))
|
||||
}
|
||||
(HealthType::Maint, true) => {
|
||||
(self.maint_base_liab_weight, self.prices.liab(health_type))
|
||||
}
|
||||
(HealthType::Maint, false) => {
|
||||
(self.maint_base_asset_weight, self.prices.asset(health_type))
|
||||
(HealthType::Init, false) | (HealthType::LiquidationEnd, false) => {
|
||||
self.init_base_asset_weight
|
||||
}
|
||||
(HealthType::Maint, true) => self.maint_base_liab_weight,
|
||||
(HealthType::Maint, false) => self.maint_base_asset_weight,
|
||||
};
|
||||
let base_price = if net_base_native.is_negative() {
|
||||
self.prices.liab(health_type)
|
||||
} else {
|
||||
self.prices.asset(health_type)
|
||||
};
|
||||
|
||||
// Total value of the order-execution adjusted base position
|
||||
let base_health = cm!(net_base_native * weight * base_price);
|
||||
|
||||
|
@ -407,9 +420,10 @@ impl HealthCache {
|
|||
|
||||
// Note: resetting the weights here assumes that the change has been applied to
|
||||
// the passed in bank already
|
||||
entry.init_asset_weight =
|
||||
entry.init_scaled_asset_weight =
|
||||
bank.scaled_init_asset_weight(entry.prices.asset(HealthType::Init));
|
||||
entry.init_liab_weight = bank.scaled_init_liab_weight(entry.prices.liab(HealthType::Init));
|
||||
entry.init_scaled_liab_weight =
|
||||
bank.scaled_init_liab_weight(entry.prices.liab(HealthType::Init));
|
||||
|
||||
// Work around the fact that -((-x) * y) == x * y does not hold for I80F48:
|
||||
// We need to make sure that if balance is before * price, then change = -before
|
||||
|
@ -714,12 +728,17 @@ pub fn new_health_cache(
|
|||
oracle: oracle_price,
|
||||
stable: bank.stable_price(),
|
||||
};
|
||||
// Use the liab price for computing weight scaling, because it's pessimistic and
|
||||
// causes the most unfavorable scaling.
|
||||
let liab_price = prices.liab(HealthType::Init);
|
||||
token_infos.push(TokenInfo {
|
||||
token_index: bank.token_index,
|
||||
maint_asset_weight: bank.maint_asset_weight,
|
||||
init_asset_weight: bank.scaled_init_asset_weight(prices.asset(HealthType::Init)),
|
||||
init_asset_weight: bank.init_asset_weight,
|
||||
init_scaled_asset_weight: bank.scaled_init_asset_weight(liab_price),
|
||||
maint_liab_weight: bank.maint_liab_weight,
|
||||
init_liab_weight: bank.scaled_init_liab_weight(prices.liab(HealthType::Init)),
|
||||
init_liab_weight: bank.init_liab_weight,
|
||||
init_scaled_liab_weight: bank.scaled_init_liab_weight(liab_price),
|
||||
prices,
|
||||
balance_native: native,
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ use super::*;
|
|||
impl HealthCache {
|
||||
pub fn is_liquidatable(&self) -> bool {
|
||||
if self.being_liquidated {
|
||||
self.health(HealthType::Init).is_negative()
|
||||
self.health(HealthType::LiquidationEnd).is_negative()
|
||||
} else {
|
||||
self.health(HealthType::Maint).is_negative()
|
||||
}
|
||||
|
@ -162,7 +162,7 @@ impl HealthCache {
|
|||
// This is just the highest final slope we can get. If the health weights are
|
||||
// scaled because the collateral or borrow limits are exceeded, health will decrease
|
||||
// more quickly than this number.
|
||||
let final_health_slope = -source.init_liab_weight * source.prices.liab(health_type)
|
||||
let final_health_slope = -source.init_scaled_liab_weight * source.prices.liab(health_type)
|
||||
+ target.init_asset_weight * target.prices.asset(health_type) * price;
|
||||
if final_health_slope >= 0 {
|
||||
// TODO: not true if weights scaled with deposits/borrows
|
||||
|
@ -422,7 +422,7 @@ impl HealthCache {
|
|||
// At most withdraw all deposits plus enough borrows to bring health to zero
|
||||
// (ensure this works with zero asset weight)
|
||||
let limit = token.balance_native.max(I80F48::ZERO)
|
||||
+ self.health(health_type).max(I80F48::ZERO) / token.init_liab_weight;
|
||||
+ self.health(health_type).max(I80F48::ZERO) / token.init_scaled_liab_weight;
|
||||
if limit <= 0 {
|
||||
return Ok(I80F48::ZERO);
|
||||
}
|
||||
|
@ -621,8 +621,10 @@ mod tests {
|
|||
token_index: 0,
|
||||
maint_asset_weight: I80F48::from_num(1.0 - x),
|
||||
init_asset_weight: I80F48::from_num(1.0 - x),
|
||||
init_scaled_asset_weight: I80F48::from_num(1.0 - x),
|
||||
maint_liab_weight: I80F48::from_num(1.0 + x),
|
||||
init_liab_weight: I80F48::from_num(1.0 + x),
|
||||
init_scaled_liab_weight: I80F48::from_num(1.0 + x),
|
||||
prices: Prices::new_single_price(I80F48::from_num(price)),
|
||||
balance_native: I80F48::ZERO,
|
||||
}
|
||||
|
|
|
@ -366,7 +366,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
// Check health before balance adjustments
|
||||
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
||||
let health_cache = new_health_cache(&account.borrow(), &retriever)?;
|
||||
let pre_health = account.check_health_pre(&health_cache)?;
|
||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||
|
||||
// Prices for logging and net borrow checks
|
||||
let mut oracle_prices = vec![];
|
||||
|
@ -464,7 +464,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
|||
// Check health after account position changes
|
||||
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
||||
let health_cache = new_health_cache(&account.borrow(), &retriever)?;
|
||||
account.check_health_post(&health_cache, pre_health)?;
|
||||
account.check_health_post(&health_cache, pre_init_health)?;
|
||||
|
||||
// Deactivate inactive token accounts after health check
|
||||
for raw_token_index in deactivated_token_positions {
|
||||
|
|
|
@ -94,8 +94,9 @@ pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>(
|
|||
|
||||
// Compute pre-health and store it on the account
|
||||
let health_cache = new_health_cache(&account.borrow(), &account_retriever)?;
|
||||
let pre_health = account.check_health_pre(&health_cache)?;
|
||||
account.fixed.health_region_begin_init_health = pre_health.ceil().checked_to_num().unwrap();
|
||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||
account.fixed.health_region_begin_init_health =
|
||||
pre_init_health.ceil().checked_to_num().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -115,8 +116,8 @@ pub fn health_region_end<'key, 'accounts, 'remaining, 'info>(
|
|||
.context("create account retriever")?;
|
||||
let health_cache = new_health_cache(&account.borrow(), &account_retriever)?;
|
||||
|
||||
let pre_health = I80F48::from(account.fixed.health_region_begin_init_health);
|
||||
account.check_health_post(&health_cache, pre_health)?;
|
||||
let pre_init_health = I80F48::from(account.fixed.health_region_begin_init_health);
|
||||
account.check_health_post(&health_cache, pre_init_health)?;
|
||||
account.fixed.health_region_begin_init_health = 0;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -99,7 +99,7 @@ pub fn perp_liq_base_or_positive_pnl(
|
|||
new_health_cache(&liqee.borrow(), &account_retriever)
|
||||
.context("create liqee health cache")?
|
||||
};
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
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)? {
|
||||
|
@ -150,7 +150,7 @@ pub fn perp_liq_base_or_positive_pnl(
|
|||
&mut liqor.borrow_mut(),
|
||||
&mut liqee.borrow_mut(),
|
||||
&mut liqee_health_cache,
|
||||
liqee_init_health,
|
||||
liqee_liq_end_health,
|
||||
now_ts,
|
||||
max_base_transfer,
|
||||
max_pnl_transfer,
|
||||
|
@ -215,15 +215,15 @@ pub fn perp_liq_base_or_positive_pnl(
|
|||
}
|
||||
|
||||
// Check liqee health again
|
||||
let liqee_init_health_after = liqee_health_cache.health(HealthType::Init);
|
||||
let liqee_liq_end_health_after = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||
liqee
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(liqee_init_health_after);
|
||||
require_gte!(liqee_init_health_after, liqee_init_health);
|
||||
.maybe_recover_from_being_liquidated(liqee_liq_end_health_after);
|
||||
require_gte!(liqee_liq_end_health_after, liqee_liq_end_health);
|
||||
msg!(
|
||||
"liqee health: {} -> {}",
|
||||
liqee_init_health,
|
||||
liqee_init_health_after
|
||||
"liqee liq end health: {} -> {}",
|
||||
liqee_liq_end_health,
|
||||
liqee_liq_end_health_after
|
||||
);
|
||||
|
||||
drop(settle_bank);
|
||||
|
@ -247,7 +247,7 @@ pub(crate) fn liquidation_action(
|
|||
liqor: &mut MangoAccountRefMut,
|
||||
liqee: &mut MangoAccountRefMut,
|
||||
liqee_health_cache: &mut HealthCache,
|
||||
liqee_init_health: I80F48,
|
||||
liqee_liq_end_health: I80F48,
|
||||
now_ts: u64,
|
||||
max_base_transfer: i64,
|
||||
max_pnl_transfer: u64,
|
||||
|
@ -290,7 +290,7 @@ pub(crate) fn liquidation_action(
|
|||
let quote_init_asset_weight = I80F48::ONE;
|
||||
direction = -1;
|
||||
fee_factor = cm!(I80F48::ONE - perp_market.base_liquidation_fee);
|
||||
let asset_price = perp_info.prices.asset(HealthType::Init);
|
||||
let asset_price = perp_info.prices.asset(HealthType::LiquidationEnd);
|
||||
unweighted_health_per_lot = cm!(-asset_price
|
||||
* base_lot_size
|
||||
* perp_market.init_base_asset_weight
|
||||
|
@ -307,7 +307,7 @@ pub(crate) fn liquidation_action(
|
|||
let quote_init_liab_weight = I80F48::ONE;
|
||||
direction = 1;
|
||||
fee_factor = cm!(I80F48::ONE + perp_market.base_liquidation_fee);
|
||||
let liab_price = perp_info.prices.liab(HealthType::Init);
|
||||
let liab_price = perp_info.prices.liab(HealthType::LiquidationEnd);
|
||||
unweighted_health_per_lot = cm!(liab_price
|
||||
* base_lot_size
|
||||
* perp_market.init_base_liab_weight
|
||||
|
@ -340,12 +340,12 @@ pub(crate) fn liquidation_action(
|
|||
//
|
||||
let mut base_reduction = 0;
|
||||
let mut current_unweighted_perp_health =
|
||||
perp_info.unweighted_health_contribution(HealthType::Init);
|
||||
let initial_weighted_perp_health =
|
||||
perp_info.weigh_health_contribution(current_unweighted_perp_health, HealthType::Init);
|
||||
perp_info.unweighted_health_contribution(HealthType::LiquidationEnd);
|
||||
let initial_weighted_perp_health = perp_info
|
||||
.weigh_health_contribution(current_unweighted_perp_health, HealthType::LiquidationEnd);
|
||||
let mut current_expected_perp_health = expected_perp_health(current_unweighted_perp_health);
|
||||
let mut current_expected_health =
|
||||
cm!(liqee_init_health + current_expected_perp_health - initial_weighted_perp_health);
|
||||
cm!(liqee_liq_end_health + current_expected_perp_health - initial_weighted_perp_health);
|
||||
|
||||
let mut reduce_base = |step: &str,
|
||||
health_amount: I80F48,
|
||||
|
@ -446,10 +446,10 @@ pub(crate) fn liquidation_action(
|
|||
// Step 4: Let the liqor take over positive pnl until the account health is positive,
|
||||
// but only while the unweighted perp health is positive (otherwise it would decrease liqee health!)
|
||||
//
|
||||
let final_weighted_perp_health =
|
||||
perp_info.weigh_health_contribution(current_unweighted_perp_health, HealthType::Init);
|
||||
let final_weighted_perp_health = perp_info
|
||||
.weigh_health_contribution(current_unweighted_perp_health, HealthType::LiquidationEnd);
|
||||
let current_actual_health =
|
||||
cm!(liqee_init_health - initial_weighted_perp_health + final_weighted_perp_health);
|
||||
cm!(liqee_liq_end_health - initial_weighted_perp_health + final_weighted_perp_health);
|
||||
let pnl_transfer_possible =
|
||||
current_actual_health < 0 && current_unweighted_perp_health > 0 && max_pnl_transfer > 0;
|
||||
let (pnl_transfer, limit_transfer) = if pnl_transfer_possible {
|
||||
|
@ -596,7 +596,7 @@ mod tests {
|
|||
let mut setup = self.clone();
|
||||
|
||||
let mut liqee_health_cache = setup.liqee_health_cache();
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||
|
||||
liquidation_action(
|
||||
setup.perp_market.data(),
|
||||
|
@ -604,7 +604,7 @@ mod tests {
|
|||
&mut setup.liqor.borrow_mut(),
|
||||
&mut setup.liqee.borrow_mut(),
|
||||
&mut liqee_health_cache,
|
||||
liqee_init_health,
|
||||
liqee_liq_end_health,
|
||||
0,
|
||||
max_base,
|
||||
max_pnl,
|
||||
|
@ -848,18 +848,21 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
// Checks that the stable price does _not_ affect the liquidation target amount
|
||||
#[test]
|
||||
fn test_liq_base_or_positive_pnl_stable_price() {
|
||||
let mut setup = TestSetup::new();
|
||||
{
|
||||
let pm = setup.perp_market.data();
|
||||
pm.stable_price_model.stable_price = 0.5;
|
||||
pm.init_base_asset_weight = I80F48::from_num(0.6);
|
||||
pm.maint_base_asset_weight = I80F48::from_num(0.8);
|
||||
}
|
||||
{
|
||||
perp_p(&mut setup.liqee).record_trade(
|
||||
setup.perp_market.data(),
|
||||
10,
|
||||
I80F48::from_num(-10),
|
||||
30,
|
||||
I80F48::from_num(-30),
|
||||
);
|
||||
|
||||
let settle_bank = setup.settle_bank.data();
|
||||
|
@ -884,19 +887,29 @@ mod tests {
|
|||
let hc = setup.liqee_health_cache();
|
||||
assert_eq_f!(
|
||||
hc.health(HealthType::Init),
|
||||
5.0 + (-10.0 + 10.0 * 0.5 * 0.8),
|
||||
5.0 + (-30.0 + 30.0 * 0.5 * 0.6), // init + stable
|
||||
0.1
|
||||
);
|
||||
assert_eq_f!(
|
||||
hc.health(HealthType::LiquidationEnd),
|
||||
5.0 + (-30.0 + 30.0 * 0.6), // init + oracle
|
||||
0.1
|
||||
);
|
||||
assert_eq_f!(
|
||||
hc.health(HealthType::Maint),
|
||||
5.0 + (-30.0 + 30.0 * 0.8), // maint + oracle
|
||||
0.1
|
||||
);
|
||||
|
||||
let mut result = setup.run(100, 0).unwrap();
|
||||
|
||||
let liqee_perp = perp_p(&mut result.liqee);
|
||||
assert_eq!(liqee_perp.base_position_lots(), 8);
|
||||
assert_eq!(liqee_perp.base_position_lots(), 12);
|
||||
|
||||
let hc = result.liqee_health_cache();
|
||||
assert_eq_f!(
|
||||
hc.health(HealthType::Init),
|
||||
5.0 + (-8.0 + 8.0 * 0.5 * 0.8),
|
||||
hc.health(HealthType::LiquidationEnd),
|
||||
5.0 + (-12.0 + 12.0 * 0.6),
|
||||
0.1
|
||||
);
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ pub fn perp_liq_force_cancel_orders(
|
|||
//
|
||||
// Health check at the end
|
||||
//
|
||||
let init_health = health_cache.health(HealthType::Init);
|
||||
let init_health = health_cache.health(HealthType::LiquidationEnd);
|
||||
account
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(init_health);
|
||||
|
|
|
@ -111,7 +111,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
|
|||
.context("create account retriever")?;
|
||||
new_health_cache(&liqee.borrow(), &retriever)?
|
||||
};
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||
let liqee_settle_health = liqee_health_cache.perp_settle_health();
|
||||
liqee_health_cache.require_after_phase2_liquidation()?;
|
||||
|
||||
|
@ -218,7 +218,8 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
|
|||
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
|
||||
let liqee_pnl = liqee_perp_position.unsettled_pnl(&perp_market, oracle_price)?;
|
||||
|
||||
let max_liab_transfer_from_liqee = (-liqee_pnl).min(-liqee_init_health).max(I80F48::ZERO);
|
||||
let max_liab_transfer_from_liqee =
|
||||
(-liqee_pnl).min(-liqee_liq_end_health).max(I80F48::ZERO);
|
||||
let liab_transfer = max_liab_transfer_from_liqee
|
||||
.min(max_liab_transfer)
|
||||
.max(I80F48::ZERO);
|
||||
|
@ -343,10 +344,10 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
|
|||
|
||||
// Check liqee health again: bankruptcy would improve health
|
||||
liqee_health_cache.recompute_perp_info(liqee_perp_position, &perp_market)?;
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||
liqee
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(liqee_init_health);
|
||||
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
|
||||
|
||||
drop(perp_market);
|
||||
drop(settle_bank);
|
||||
|
|
|
@ -108,8 +108,8 @@ pub fn perp_place_order(
|
|||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health_cache =
|
||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
||||
let pre_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_health))
|
||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_init_health))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -168,10 +168,10 @@ pub fn perp_place_order(
|
|||
//
|
||||
// Health check
|
||||
//
|
||||
if let Some((mut health_cache, pre_health)) = pre_health_opt {
|
||||
if let Some((mut health_cache, pre_init_health)) = pre_health_opt {
|
||||
let perp_position = account.perp_position(perp_market_index)?;
|
||||
health_cache.recompute_perp_info(perp_position, &perp_market)?;
|
||||
account.check_health_post(&health_cache, pre_health)?;
|
||||
account.check_health_post(&health_cache, pre_init_health)?;
|
||||
}
|
||||
|
||||
Ok(order_id_opt)
|
||||
|
|
|
@ -82,7 +82,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
account_b.token_position(settle_token_index)?;
|
||||
}
|
||||
|
||||
let a_init_health;
|
||||
let a_liq_end_health;
|
||||
let a_maint_health;
|
||||
let b_settle_health;
|
||||
{
|
||||
|
@ -91,7 +91,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
.context("create account retriever")?;
|
||||
b_settle_health = new_health_cache(&account_b.borrow(), &retriever)?.perp_settle_health();
|
||||
let a_cache = new_health_cache(&account_a.borrow(), &retriever)?;
|
||||
a_init_health = a_cache.health(HealthType::Init);
|
||||
a_liq_end_health = a_cache.health(HealthType::LiquidationEnd);
|
||||
a_maint_health = a_cache.health(HealthType::Maint);
|
||||
};
|
||||
|
||||
|
@ -181,7 +181,7 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
b_settle_health,
|
||||
);
|
||||
|
||||
let fee = compute_settle_fee(&perp_market, a_init_health, a_maint_health, settlement)?;
|
||||
let fee = compute_settle_fee(&perp_market, a_liq_end_health, a_maint_health, settlement)?;
|
||||
|
||||
a_perp_position.record_settle(settlement);
|
||||
b_perp_position.record_settle(-settlement);
|
||||
|
@ -285,21 +285,23 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
|
|||
|
||||
pub fn compute_settle_fee(
|
||||
perp_market: &PerpMarket,
|
||||
source_init_health: I80F48,
|
||||
source_liq_end_health: I80F48,
|
||||
source_maint_health: I80F48,
|
||||
settlement: I80F48,
|
||||
) -> Result<I80F48> {
|
||||
assert!(source_maint_health >= source_liq_end_health);
|
||||
|
||||
// A percentage fee is paid to the settler when the source account's health is low.
|
||||
// That's because the settlement could avoid it getting liquidated: settling will
|
||||
// increase its health by actualizing positive perp pnl.
|
||||
let low_health_fee = if source_init_health < 0 {
|
||||
let low_health_fee = if source_liq_end_health < 0 {
|
||||
let fee_fraction = I80F48::from_num(perp_market.settle_fee_fraction_low_health);
|
||||
if source_maint_health < 0 {
|
||||
cm!(settlement * fee_fraction)
|
||||
} else {
|
||||
cm!(settlement
|
||||
* fee_fraction
|
||||
* (-source_init_health / (source_maint_health - source_init_health)))
|
||||
* (-source_liq_end_health / (source_maint_health - source_liq_end_health)))
|
||||
}
|
||||
} else {
|
||||
I80F48::ZERO
|
||||
|
|
|
@ -241,10 +241,10 @@ pub fn serum3_liq_force_cancel_orders(
|
|||
//
|
||||
// Health check at the end
|
||||
//
|
||||
let init_health = health_cache.health(HealthType::Init);
|
||||
let liq_end_health = health_cache.health(HealthType::LiquidationEnd);
|
||||
account
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(init_health);
|
||||
.maybe_recover_from_being_liquidated(liq_end_health);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -260,8 +260,8 @@ pub fn serum3_place_order(
|
|||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health_cache =
|
||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
||||
let pre_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_health))
|
||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_init_health))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -383,10 +383,10 @@ pub fn serum3_place_order(
|
|||
//
|
||||
// Health check
|
||||
//
|
||||
if let Some((mut health_cache, pre_health)) = pre_health_opt {
|
||||
if let Some((mut health_cache, pre_init_health)) = pre_health_opt {
|
||||
vault_difference.adjust_health_cache(&mut health_cache, &payer_bank)?;
|
||||
oo_difference.adjust_health_cache(&mut health_cache, &serum_market)?;
|
||||
account.check_health_post(&health_cache, pre_health)?;
|
||||
account.check_health_post(&health_cache, pre_init_health)?;
|
||||
}
|
||||
|
||||
// TODO: enforce min_vault_to_deposits_ratio
|
||||
|
|
|
@ -193,7 +193,7 @@ impl<'a, 'info> DepositCommon<'a, 'info> {
|
|||
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
|
||||
// Being in a health region always means being_liquidated is false, so it's safe to gate the check.
|
||||
if !account.fixed.is_in_health_region() {
|
||||
let health = cache.health(HealthType::Init);
|
||||
let health = cache.health(HealthType::LiquidationEnd);
|
||||
msg!("health: {}", health);
|
||||
|
||||
let was_being_liquidated = account.being_liquidated();
|
||||
|
|
|
@ -108,7 +108,7 @@ pub fn token_liq_bankruptcy(
|
|||
liqee_health_cache.require_after_phase2_liquidation()?;
|
||||
liqee.fixed.set_being_liquidated(true);
|
||||
|
||||
let (liab_bank, liab_price, opt_quote_bank_and_price) =
|
||||
let (liab_bank, liab_oracle_price, opt_quote_bank_and_price) =
|
||||
account_retriever.banks_mut_and_oracles(liab_token_index, QUOTE_TOKEN_INDEX)?;
|
||||
let mut liab_deposit_index = liab_bank.deposit_index;
|
||||
let liab_borrow_index = liab_bank.borrow_index;
|
||||
|
@ -123,7 +123,7 @@ pub fn token_liq_bankruptcy(
|
|||
} else {
|
||||
cm!(I80F48::ONE + liab_bank.liquidation_fee)
|
||||
};
|
||||
let liab_price_adjusted = cm!(liab_price * liab_fee_factor);
|
||||
let liab_price_adjusted = cm!(liab_oracle_price * liab_fee_factor);
|
||||
|
||||
let liab_transfer_unrounded = remaining_liab_loss.min(max_liab_transfer);
|
||||
|
||||
|
@ -193,8 +193,12 @@ pub fn token_liq_bankruptcy(
|
|||
// transfer liab from liqee to liqor
|
||||
let (liqor_liab, liqor_liab_raw_token_index, _) =
|
||||
liqor.ensure_token_position(liab_token_index)?;
|
||||
let (liqor_liab_active, loan_origination_fee) =
|
||||
liab_bank.withdraw_with_fee(liqor_liab, liab_transfer, now_ts, liab_price)?;
|
||||
let (liqor_liab_active, loan_origination_fee) = liab_bank.withdraw_with_fee(
|
||||
liqor_liab,
|
||||
liab_transfer,
|
||||
now_ts,
|
||||
liab_oracle_price,
|
||||
)?;
|
||||
|
||||
// liqor liab
|
||||
emit!(TokenBalanceLog {
|
||||
|
@ -307,10 +311,10 @@ pub fn token_liq_bankruptcy(
|
|||
.adjust_token_balance(&liab_bank, cm!(end_liab_native - initial_liab_native))?;
|
||||
|
||||
// Check liqee health again
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||
liqee
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(liqee_init_health);
|
||||
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
|
||||
|
||||
if !liqee_liab_active {
|
||||
liqee.deactivate_token_position_and_log(liqee_raw_token_index, ctx.accounts.liqee.key());
|
||||
|
@ -322,7 +326,7 @@ pub fn token_liq_bankruptcy(
|
|||
liqor: ctx.accounts.liqor.key(),
|
||||
liab_token_index,
|
||||
initial_liab_native: initial_liab_native.to_bits(),
|
||||
liab_price: liab_price.to_bits(),
|
||||
liab_price: liab_oracle_price.to_bits(),
|
||||
insurance_token_index: QUOTE_TOKEN_INDEX,
|
||||
insurance_transfer: insurance_transfer_i80f48.to_bits(),
|
||||
socialized_loss: socialized_loss.to_bits(),
|
||||
|
|
|
@ -67,7 +67,7 @@ pub fn token_liq_with_token(
|
|||
// Initial liqee health check
|
||||
let mut liqee_health_cache = new_health_cache(&liqee.borrow(), &account_retriever)
|
||||
.context("create liqee health cache")?;
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
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)? {
|
||||
|
@ -88,7 +88,7 @@ pub fn token_liq_with_token(
|
|||
&mut liqee.borrow_mut(),
|
||||
ctx.accounts.liqee.key(),
|
||||
&mut liqee_health_cache,
|
||||
liqee_init_health,
|
||||
liqee_liq_end_health,
|
||||
now_ts,
|
||||
max_liab_transfer,
|
||||
)?;
|
||||
|
@ -112,7 +112,7 @@ pub(crate) fn liquidation_action(
|
|||
liqee: &mut MangoAccountRefMut,
|
||||
liqee_key: Pubkey,
|
||||
liqee_health_cache: &mut HealthCache,
|
||||
liqee_init_health: I80F48,
|
||||
liqee_liq_end_health: I80F48,
|
||||
now_ts: u64,
|
||||
max_liab_transfer: I80F48,
|
||||
) -> Result<()> {
|
||||
|
@ -154,43 +154,43 @@ pub(crate) fn liquidation_action(
|
|||
let init_asset_weight = asset_bank.init_asset_weight;
|
||||
let init_liab_weight = liab_bank.init_liab_weight;
|
||||
|
||||
// The price the health computation uses for a liability of one native liab token
|
||||
let liab_init_liab_price = liqee_health_cache
|
||||
// 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(HealthType::Init);
|
||||
.liab(HealthType::LiquidationEnd);
|
||||
// Health price for an asset of one native asset token
|
||||
let asset_init_asset_price = liqee_health_cache
|
||||
let asset_liq_end_price = liqee_health_cache
|
||||
.token_info(asset_token_index)
|
||||
.unwrap()
|
||||
.prices
|
||||
.asset(HealthType::Init);
|
||||
.asset(HealthType::LiquidationEnd);
|
||||
|
||||
// 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 * lilp health gain from reducing liabs
|
||||
// - y * iaw * aiap health loss from paying asset
|
||||
// + x * ilw * llep health gain from reducing liabs
|
||||
// - y * iaw * alep health loss from paying asset
|
||||
// = 0
|
||||
// where
|
||||
// ilw = init_liab_weight,
|
||||
// lilp = liab_init_liab_price,
|
||||
// llep = liab_liq_end_price,
|
||||
// lopa = liab_oracle_price_adjusted, (see above)
|
||||
// iap = init_asset_weight,
|
||||
// aiap = asset_init_asset_price,
|
||||
// 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 * lilp - iaw * lopa * aiap / aop)
|
||||
let liab_needed = cm!(-liqee_init_health
|
||||
/ (liab_init_liab_price * init_liab_weight
|
||||
// Result: x = -init_health / (ilw * llep - iaw * lopa * alep / aop)
|
||||
let liab_needed = cm!(-liqee_liq_end_health
|
||||
/ (liab_liq_end_price * init_liab_weight
|
||||
- liab_oracle_price_adjusted
|
||||
* init_asset_weight
|
||||
* (asset_init_asset_price / asset_oracle_price)));
|
||||
* (asset_liq_end_price / asset_oracle_price)));
|
||||
|
||||
// How much liab can we get at most for the asset balance?
|
||||
let liab_possible = cm!(liqee_asset_native * asset_oracle_price / liab_oracle_price_adjusted);
|
||||
|
@ -316,10 +316,10 @@ pub(crate) fn liquidation_action(
|
|||
}
|
||||
|
||||
// Check liqee health again
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||
liqee
|
||||
.fixed
|
||||
.maybe_recover_from_being_liquidated(liqee_init_health);
|
||||
.maybe_recover_from_being_liquidated(liqee_liq_end_health);
|
||||
|
||||
emit!(TokenLiqWithTokenLog {
|
||||
mango_group: liqee.fixed.group,
|
||||
|
@ -331,7 +331,8 @@ pub(crate) fn liquidation_action(
|
|||
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_init_health.is_negative()
|
||||
bankruptcy: !liqee_health_cache.has_phase2_liquidatable()
|
||||
& liqee_liq_end_health.is_negative()
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
@ -413,7 +414,7 @@ mod tests {
|
|||
|
||||
let mut liqee_health_cache =
|
||||
health::new_health_cache(&setup.liqee.borrow(), &retriever).unwrap();
|
||||
let liqee_init_health = liqee_health_cache.health(HealthType::Init);
|
||||
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
|
||||
|
||||
liquidation_action(
|
||||
&mut retriever,
|
||||
|
@ -424,7 +425,7 @@ mod tests {
|
|||
&mut setup.liqee.borrow_mut(),
|
||||
Pubkey::new_unique(),
|
||||
&mut liqee_health_cache,
|
||||
liqee_init_health,
|
||||
liqee_liq_end_health,
|
||||
0,
|
||||
max_liab_transfer,
|
||||
)?;
|
||||
|
@ -452,14 +453,19 @@ mod tests {
|
|||
};
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
@ -492,7 +498,7 @@ mod tests {
|
|||
liab_bank
|
||||
.change_without_fee(
|
||||
liab_p(&mut setup.liqee),
|
||||
I80F48::from_num(-5.0),
|
||||
I80F48::from_num(-9.0),
|
||||
0,
|
||||
I80F48::from(1),
|
||||
)
|
||||
|
@ -500,16 +506,24 @@ mod tests {
|
|||
}
|
||||
|
||||
let hc = setup.liqee_health_cache();
|
||||
assert_eq_f!(hc.health(HealthType::Init), 10.0 * 0.5 - 5.0 * 1.25, 0.1);
|
||||
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()), 8.335, 0.01);
|
||||
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()), -3.335, 0.01);
|
||||
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::Init), 0.0, 0.01);
|
||||
assert_eq_f!(hc.health(HealthType::LiquidationEnd), 0.0, 0.01);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,8 +79,8 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
|
||||
let health_cache =
|
||||
new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?;
|
||||
let pre_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_health))
|
||||
let pre_init_health = account.check_health_pre(&health_cache)?;
|
||||
Some((health_cache, pre_init_health))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -159,9 +159,9 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
//
|
||||
// Health check
|
||||
//
|
||||
if let Some((mut health_cache, pre_health)) = pre_health_opt {
|
||||
if let Some((mut health_cache, pre_init_health)) = pre_health_opt {
|
||||
health_cache.adjust_token_balance(&bank, cm!(native_position_after - native_position))?;
|
||||
account.check_health_post(&health_cache, pre_health)?;
|
||||
account.check_health_post(&health_cache, pre_init_health)?;
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -247,10 +247,10 @@ impl MangoAccountFixed {
|
|||
self.in_health_region = u8::from(b);
|
||||
}
|
||||
|
||||
pub fn maybe_recover_from_being_liquidated(&mut self, init_health: I80F48) -> bool {
|
||||
pub fn maybe_recover_from_being_liquidated(&mut self, liq_end_health: I80F48) -> bool {
|
||||
// This is used as threshold to flip flag instead of 0 because of dust issues
|
||||
let one_native_usdc = I80F48::ONE;
|
||||
if self.being_liquidated() && init_health > -one_native_usdc {
|
||||
if self.being_liquidated() && liq_end_health > -one_native_usdc {
|
||||
self.set_being_liquidated(false);
|
||||
true
|
||||
} else {
|
||||
|
@ -941,30 +941,37 @@ impl<
|
|||
}
|
||||
|
||||
pub fn check_health_pre(&mut self, health_cache: &HealthCache) -> Result<I80F48> {
|
||||
let pre_health = health_cache.health(HealthType::Init);
|
||||
msg!("pre_health: {}", pre_health);
|
||||
let pre_init_health = health_cache.health(HealthType::Init);
|
||||
msg!("pre_init_health: {}", pre_init_health);
|
||||
|
||||
// We can skip computing LiquidationEnd health if Init health > 0, because
|
||||
// LiquidationEnd health >= Init health.
|
||||
self.fixed_mut()
|
||||
.maybe_recover_from_being_liquidated(pre_health);
|
||||
.maybe_recover_from_being_liquidated(pre_init_health);
|
||||
if self.fixed().being_liquidated() {
|
||||
let liq_end_health = health_cache.health(HealthType::LiquidationEnd);
|
||||
self.fixed_mut()
|
||||
.maybe_recover_from_being_liquidated(liq_end_health);
|
||||
}
|
||||
require!(
|
||||
!self.fixed().being_liquidated(),
|
||||
MangoError::BeingLiquidated
|
||||
);
|
||||
Ok(pre_health)
|
||||
|
||||
Ok(pre_init_health)
|
||||
}
|
||||
|
||||
pub fn check_health_post(
|
||||
&mut self,
|
||||
health_cache: &HealthCache,
|
||||
pre_health: I80F48,
|
||||
pre_init_health: I80F48,
|
||||
) -> Result<()> {
|
||||
let post_health = health_cache.health(HealthType::Init);
|
||||
msg!("post_health: {}", post_health);
|
||||
let post_init_health = health_cache.health(HealthType::Init);
|
||||
msg!("post_init_health: {}", post_init_health);
|
||||
require!(
|
||||
post_health >= 0 || post_health > pre_health,
|
||||
post_init_health >= 0 || post_init_health > pre_init_health,
|
||||
MangoError::HealthMustBePositiveOrIncrease
|
||||
);
|
||||
self.fixed_mut()
|
||||
.maybe_recover_from_being_liquidated(post_health);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -973,10 +980,10 @@ impl<
|
|||
// we want to allow liquidation to continue until init_health is positive,
|
||||
// to prevent constant oscillation between the two states
|
||||
if self.being_liquidated() {
|
||||
let init_health = health_cache.health(HealthType::Init);
|
||||
let liq_end_health = health_cache.health(HealthType::LiquidationEnd);
|
||||
if self
|
||||
.fixed_mut()
|
||||
.maybe_recover_from_being_liquidated(init_health)
|
||||
.maybe_recover_from_being_liquidated(liq_end_health)
|
||||
{
|
||||
msg!("Liqee init_health above zero");
|
||||
return Ok(false);
|
||||
|
|
|
@ -356,7 +356,7 @@ pub async fn check_prev_instruction_post_health(solana: &SolanaCookie, account:
|
|||
let logs = solana.program_log();
|
||||
let post_health_str = logs
|
||||
.iter()
|
||||
.find_map(|line| line.strip_prefix("post_health: "))
|
||||
.find_map(|line| line.strip_prefix("post_init_health: "))
|
||||
.unwrap();
|
||||
let post_health = post_health_str.parse::<f64>().unwrap();
|
||||
|
||||
|
|
|
@ -39,8 +39,8 @@ export interface BankForHealth {
|
|||
price: I80F48;
|
||||
stablePriceModel: StablePriceModel;
|
||||
|
||||
scaledInitAssetWeight(): I80F48;
|
||||
scaledInitLiabWeight(): I80F48;
|
||||
scaledInitAssetWeight(price: I80F48): I80F48;
|
||||
scaledInitLiabWeight(price: I80F48): I80F48;
|
||||
}
|
||||
|
||||
export class Bank implements BankForHealth {
|
||||
|
@ -309,8 +309,8 @@ export class Bank implements BankForHealth {
|
|||
);
|
||||
}
|
||||
|
||||
scaledInitAssetWeight(): I80F48 {
|
||||
const depositsQuote = this.nativeDeposits().mul(this.price);
|
||||
scaledInitAssetWeight(price: I80F48): I80F48 {
|
||||
const depositsQuote = this.nativeDeposits().mul(price);
|
||||
if (
|
||||
this.depositWeightScaleStartQuote >= Number.MAX_SAFE_INTEGER ||
|
||||
depositsQuote.lte(I80F48.fromNumber(this.depositWeightScaleStartQuote))
|
||||
|
@ -322,8 +322,8 @@ export class Bank implements BankForHealth {
|
|||
);
|
||||
}
|
||||
|
||||
scaledInitLiabWeight(): I80F48 {
|
||||
const borrowsQuote = this.nativeBorrows().mul(this.price);
|
||||
scaledInitLiabWeight(price: I80F48): I80F48 {
|
||||
const borrowsQuote = this.nativeBorrows().mul(price);
|
||||
if (
|
||||
this.borrowWeightScaleStartQuote >= Number.MAX_SAFE_INTEGER ||
|
||||
borrowsQuote.lte(I80F48.fromNumber(this.borrowWeightScaleStartQuote))
|
||||
|
|
|
@ -1165,14 +1165,22 @@ export class Prices {
|
|||
constructor(public oracle: I80F48, public stable: I80F48) {}
|
||||
|
||||
public liab(healthType: HealthType | undefined): I80F48 {
|
||||
if (healthType === HealthType.maint || healthType === undefined) {
|
||||
if (
|
||||
healthType === HealthType.maint ||
|
||||
healthType === HealthType.liquidationEnd ||
|
||||
healthType === undefined
|
||||
) {
|
||||
return this.oracle;
|
||||
}
|
||||
return this.oracle.max(this.stable);
|
||||
}
|
||||
|
||||
public asset(healthType: HealthType | undefined): I80F48 {
|
||||
if (healthType === HealthType.maint || healthType === undefined) {
|
||||
if (
|
||||
healthType === HealthType.maint ||
|
||||
healthType === HealthType.liquidationEnd ||
|
||||
healthType === undefined
|
||||
) {
|
||||
return this.oracle;
|
||||
}
|
||||
return this.oracle.min(this.stable);
|
||||
|
@ -1184,8 +1192,10 @@ export class TokenInfo {
|
|||
public tokenIndex: TokenIndex,
|
||||
public maintAssetWeight: I80F48,
|
||||
public initAssetWeight: I80F48,
|
||||
public initScaledAssetWeight: I80F48,
|
||||
public maintLiabWeight: I80F48,
|
||||
public initLiabWeight: I80F48,
|
||||
public initScaledLiabWeight: I80F48,
|
||||
public prices: Prices,
|
||||
public balanceNative: I80F48,
|
||||
) {}
|
||||
|
@ -1195,8 +1205,10 @@ export class TokenInfo {
|
|||
dto.tokenIndex as TokenIndex,
|
||||
I80F48.from(dto.maintAssetWeight),
|
||||
I80F48.from(dto.initAssetWeight),
|
||||
I80F48.from(dto.initScaledAssetWeight),
|
||||
I80F48.from(dto.maintLiabWeight),
|
||||
I80F48.from(dto.initLiabWeight),
|
||||
I80F48.from(dto.initScaledLiabWeight),
|
||||
new Prices(
|
||||
I80F48.from(dto.prices.oracle),
|
||||
I80F48.from(dto.prices.stable),
|
||||
|
@ -1206,30 +1218,44 @@ export class TokenInfo {
|
|||
}
|
||||
|
||||
static fromBank(bank: BankForHealth, nativeBalance?: I80F48): TokenInfo {
|
||||
const p = new Prices(
|
||||
bank.price,
|
||||
I80F48.fromNumber(bank.stablePriceModel.stablePrice),
|
||||
);
|
||||
// Use the liab price for computing weight scaling, because it's pessimistic and
|
||||
// causes the most unfavorable scaling.
|
||||
const liabPrice = p.liab(HealthType.init);
|
||||
return new TokenInfo(
|
||||
bank.tokenIndex,
|
||||
bank.maintAssetWeight,
|
||||
bank.scaledInitAssetWeight(),
|
||||
bank.initAssetWeight,
|
||||
bank.scaledInitAssetWeight(liabPrice),
|
||||
bank.maintLiabWeight,
|
||||
bank.scaledInitLiabWeight(),
|
||||
new Prices(
|
||||
bank.price,
|
||||
I80F48.fromNumber(bank.stablePriceModel.stablePrice),
|
||||
),
|
||||
bank.initLiabWeight,
|
||||
bank.scaledInitLiabWeight(liabPrice),
|
||||
p,
|
||||
nativeBalance ? nativeBalance : ZERO_I80F48(),
|
||||
);
|
||||
}
|
||||
|
||||
assetWeight(healthType: HealthType): I80F48 {
|
||||
return healthType == HealthType.init
|
||||
? this.initAssetWeight
|
||||
: this.maintAssetWeight;
|
||||
if (healthType == HealthType.init) {
|
||||
return this.initScaledAssetWeight;
|
||||
} else if (healthType == HealthType.liquidationEnd) {
|
||||
return this.initAssetWeight;
|
||||
}
|
||||
// healthType == HealthType.maint
|
||||
return this.maintAssetWeight;
|
||||
}
|
||||
|
||||
liabWeight(healthType: HealthType): I80F48 {
|
||||
return healthType == HealthType.init
|
||||
? this.initLiabWeight
|
||||
: this.maintLiabWeight;
|
||||
if (healthType == HealthType.init) {
|
||||
return this.initScaledLiabWeight;
|
||||
} else if (healthType == HealthType.liquidationEnd) {
|
||||
return this.initLiabWeight;
|
||||
}
|
||||
// healthType == HealthType.maint
|
||||
return this.maintLiabWeight;
|
||||
}
|
||||
|
||||
healthContribution(healthType?: HealthType): I80F48 {
|
||||
|
@ -1508,7 +1534,7 @@ export class PerpInfo {
|
|||
const contrib = this.unweightedHealthContribution(healthType);
|
||||
if (contrib.gt(ZERO_I80F48())) {
|
||||
const assetWeight =
|
||||
healthType == HealthType.init
|
||||
healthType == HealthType.init || healthType == HealthType.liquidationEnd
|
||||
? this.initOverallAssetWeight
|
||||
: this.maintOverallAssetWeight;
|
||||
return assetWeight.mul(contrib);
|
||||
|
@ -1527,24 +1553,31 @@ export class PerpInfo {
|
|||
);
|
||||
|
||||
let weight, basePrice;
|
||||
if (healthType == HealthType.init) {
|
||||
if (
|
||||
healthType == HealthType.init ||
|
||||
healthType == HealthType.liquidationEnd
|
||||
) {
|
||||
if (netBaseNative.isNeg()) {
|
||||
weight = pi.initBaseLiabWeight;
|
||||
basePrice = pi.prices.liab(healthType);
|
||||
} else {
|
||||
weight = pi.initBaseAssetWeight;
|
||||
basePrice = pi.prices.asset(healthType);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
// healthType == HealthType.maint
|
||||
else {
|
||||
if (netBaseNative.isNeg()) {
|
||||
weight = pi.maintBaseLiabWeight;
|
||||
basePrice = pi.prices.liab(healthType);
|
||||
} else {
|
||||
weight = pi.maintBaseAssetWeight;
|
||||
basePrice = pi.prices.asset(healthType);
|
||||
}
|
||||
}
|
||||
|
||||
if (netBaseNative.isNeg()) {
|
||||
basePrice = pi.prices.liab(healthType);
|
||||
} else {
|
||||
basePrice = pi.prices.asset(healthType);
|
||||
}
|
||||
|
||||
// Total value of the order-execution adjusted base position
|
||||
const baseHealth = netBaseNative.mul(weight).mul(basePrice);
|
||||
|
||||
|
@ -1615,8 +1648,10 @@ export class TokenInfoDto {
|
|||
tokenIndex: number;
|
||||
maintAssetWeight: I80F48Dto;
|
||||
initAssetWeight: I80F48Dto;
|
||||
initScaledAssetWeight: I80F48Dto;
|
||||
maintLiabWeight: I80F48Dto;
|
||||
initLiabWeight: I80F48Dto;
|
||||
initScaledLiabWeight: I80F48Dto;
|
||||
prices: { oracle: I80F48Dto; stable: I80F48Dto };
|
||||
balanceNative: I80F48Dto;
|
||||
|
||||
|
@ -1624,16 +1659,20 @@ export class TokenInfoDto {
|
|||
tokenIndex: number,
|
||||
maintAssetWeight: I80F48Dto,
|
||||
initAssetWeight: I80F48Dto,
|
||||
initScaledAssetWeight: I80F48Dto,
|
||||
maintLiabWeight: I80F48Dto,
|
||||
initLiabWeight: I80F48Dto,
|
||||
initScaledLiabWeight: I80F48Dto,
|
||||
prices: { oracle: I80F48Dto; stable: I80F48Dto },
|
||||
balanceNative: I80F48Dto,
|
||||
) {
|
||||
this.tokenIndex = tokenIndex;
|
||||
this.maintAssetWeight = maintAssetWeight;
|
||||
this.initAssetWeight = initAssetWeight;
|
||||
this.initScaledAssetWeight = initScaledAssetWeight;
|
||||
this.maintLiabWeight = maintLiabWeight;
|
||||
this.initLiabWeight = initLiabWeight;
|
||||
this.initScaledLiabWeight = initScaledLiabWeight;
|
||||
this.prices = prices;
|
||||
this.balanceNative = balanceNative;
|
||||
}
|
||||
|
|
|
@ -1580,4 +1580,5 @@ export class PerpOoDto {
|
|||
export class HealthType {
|
||||
static maint = { maint: {} };
|
||||
static init = { init: {} };
|
||||
static liquidationEnd = { liquidationEnd: {} };
|
||||
}
|
||||
|
|
|
@ -4974,6 +4974,12 @@ export type MangoV4 = {
|
|||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "initScaledAssetWeight",
|
||||
"type": {
|
||||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "maintLiabWeight",
|
||||
"type": {
|
||||
|
@ -4986,6 +4992,12 @@ export type MangoV4 = {
|
|||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "initScaledLiabWeight",
|
||||
"type": {
|
||||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prices",
|
||||
"type": {
|
||||
|
@ -6383,12 +6395,20 @@ export type MangoV4 = {
|
|||
{
|
||||
"name": "HealthType",
|
||||
"docs": [
|
||||
"There are two types of health, initial health used for opening new positions and maintenance",
|
||||
"health used for liquidations. They are both calculated as a weighted sum of the assets",
|
||||
"minus the liabilities but the maint. health uses slightly larger weights for assets and",
|
||||
"slightly smaller weights for the liabilities. Zero is used as the bright line for both",
|
||||
"i.e. if your init health falls below zero, you cannot open new positions and if your maint. health",
|
||||
"falls below zero you will be liquidated."
|
||||
"There are three types of health:",
|
||||
"- initial health (\"init\"): users can only open new positions if it's >= 0",
|
||||
"- maintenance health (\"maint\"): users get liquidated if it's < 0",
|
||||
"- liquidation end health: once liquidation started (see being_liquidated), it",
|
||||
"only stops once this is >= 0",
|
||||
"",
|
||||
"The ordering is",
|
||||
"init health <= liquidation end health <= maint health",
|
||||
"",
|
||||
"The different health types are realized by using different weights and prices:",
|
||||
"- init health: init weights with scaling, stable-price adjusted prices",
|
||||
"- liq end health: init weights without scaling, oracle prices",
|
||||
"- maint health: maint weights, oracle prices",
|
||||
""
|
||||
],
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
|
@ -6398,6 +6418,9 @@ export type MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "Maint"
|
||||
},
|
||||
{
|
||||
"name": "LiquidationEnd"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -13292,6 +13315,12 @@ export const IDL: MangoV4 = {
|
|||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "initScaledAssetWeight",
|
||||
"type": {
|
||||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "maintLiabWeight",
|
||||
"type": {
|
||||
|
@ -13304,6 +13333,12 @@ export const IDL: MangoV4 = {
|
|||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "initScaledLiabWeight",
|
||||
"type": {
|
||||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prices",
|
||||
"type": {
|
||||
|
@ -14701,12 +14736,20 @@ export const IDL: MangoV4 = {
|
|||
{
|
||||
"name": "HealthType",
|
||||
"docs": [
|
||||
"There are two types of health, initial health used for opening new positions and maintenance",
|
||||
"health used for liquidations. They are both calculated as a weighted sum of the assets",
|
||||
"minus the liabilities but the maint. health uses slightly larger weights for assets and",
|
||||
"slightly smaller weights for the liabilities. Zero is used as the bright line for both",
|
||||
"i.e. if your init health falls below zero, you cannot open new positions and if your maint. health",
|
||||
"falls below zero you will be liquidated."
|
||||
"There are three types of health:",
|
||||
"- initial health (\"init\"): users can only open new positions if it's >= 0",
|
||||
"- maintenance health (\"maint\"): users get liquidated if it's < 0",
|
||||
"- liquidation end health: once liquidation started (see being_liquidated), it",
|
||||
"only stops once this is >= 0",
|
||||
"",
|
||||
"The ordering is",
|
||||
"init health <= liquidation end health <= maint health",
|
||||
"",
|
||||
"The different health types are realized by using different weights and prices:",
|
||||
"- init health: init weights with scaling, stable-price adjusted prices",
|
||||
"- liq end health: init weights without scaling, oracle prices",
|
||||
"- maint health: maint weights, oracle prices",
|
||||
""
|
||||
],
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
|
@ -14716,6 +14759,9 @@ export const IDL: MangoV4 = {
|
|||
},
|
||||
{
|
||||
"name": "Maint"
|
||||
},
|
||||
{
|
||||
"name": "LiquidationEnd"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue