Audit v0.22 fixes (#887)

- apply recurring settle allowance constraint also in
  available_settle_limit
- bank constraints on util0, util1
- cleanup
- perp liq: take over oneshot and recurring limits separately
This commit is contained in:
Christian Kamm 2024-02-21 09:00:57 +01:00 committed by GitHub
parent 46c6e86206
commit efe4a1ae3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 98 additions and 29 deletions

View File

@ -8,7 +8,7 @@ use crate::health::*;
use crate::state::*;
use crate::accounts_ix::*;
use crate::logs::{emit_perp_balances, emit_stack, PerpLiqBaseOrPositivePnlLogV2, TokenBalanceLog};
use crate::logs::{emit_perp_balances, emit_stack, PerpLiqBaseOrPositivePnlLogV3, TokenBalanceLog};
/// This instruction deals with increasing health by:
/// - reducing the liqee's base position
@ -100,7 +100,8 @@ pub fn perp_liq_base_or_positive_pnl(
quote_transfer_liqor,
platform_fee,
pnl_transfer,
pnl_settle_limit_transfer,
pnl_settle_limit_transfer_recurring,
pnl_settle_limit_transfer_oneshot,
) = liquidation_action(
&mut perp_market,
&mut settle_bank,
@ -158,7 +159,7 @@ pub fn perp_liq_base_or_positive_pnl(
}
if base_transfer != 0 || pnl_transfer != 0 {
emit_stack(PerpLiqBaseOrPositivePnlLogV2 {
emit_stack(PerpLiqBaseOrPositivePnlLogV3 {
mango_group: ctx.accounts.group.key(),
perp_market_index: perp_market.perp_market_index,
liqor: ctx.accounts.liqor.key(),
@ -168,7 +169,8 @@ pub fn perp_liq_base_or_positive_pnl(
quote_transfer_liqor: quote_transfer_liqor.to_bits(),
quote_platform_fee: platform_fee.to_bits(),
pnl_transfer: pnl_transfer.to_bits(),
pnl_settle_limit_transfer: pnl_settle_limit_transfer.to_bits(),
pnl_settle_limit_transfer_recurring,
pnl_settle_limit_transfer_oneshot,
price: oracle_price.to_bits(),
});
}
@ -215,7 +217,7 @@ pub(crate) fn liquidation_action(
now_ts: u64,
max_base_transfer: i64,
max_pnl_transfer: u64,
) -> Result<(i64, I80F48, I80F48, I80F48, I80F48, I80F48)> {
) -> Result<(i64, I80F48, I80F48, I80F48, I80F48, i64, i64)> {
let liq_end_type = HealthType::LiquidationEnd;
let perp_market_index = perp_market.perp_market_index;
@ -574,7 +576,9 @@ pub(crate) fn liquidation_action(
// Let the liqor take over positive pnl until the account health is positive,
// but only while the health_unsettled_pnl is positive (otherwise it would decrease liqee health!)
//
let limit_transfer = if pnl_transfer > 0 {
let limit_transfer_recurring: i64;
let limit_transfer_oneshot: i64;
if pnl_transfer > 0 {
// Allow taking over *more* than the liqee_positive_settle_limit. In exchange, the liqor
// also can't settle fully immediately and just takes over a fractional chunk of the limit.
//
@ -582,22 +586,45 @@ pub(crate) fn liquidation_action(
// base position to zero and would need to deal with that in bankruptcy. Also, the settle
// limit changes with the base position price, so it'd be hard to say when this liquidation
// step is done.
let limit_transfer = {
{
// take care, liqee_limit may be i64::MAX
let liqee_limit: i128 = liqee_positive_settle_limit.into();
let liqee_oneshot_positive = liqee_perp_position
.oneshot_settle_pnl_allowance
.ceil()
.to_num::<i128>()
.max(0);
let liqee_recurring = (liqee_limit - liqee_oneshot_positive).max(0);
let liqee_pnl = liqee_perp_position
.unsettled_pnl(perp_market, oracle_price)?
.max(I80F48::ONE);
.ceil()
.to_num::<i128>()
.max(1);
let settle = pnl_transfer.floor().to_num::<i128>();
let total = liqee_pnl.ceil().to_num::<i128>();
let liqor_limit: i64 = (liqee_limit * settle / total).try_into().unwrap();
I80F48::from(liqor_limit).min(pnl_transfer).max(I80F48::ONE)
let total = liqee_pnl.max(settle);
let transfer_recurring: i64 = (liqee_recurring * settle / total).try_into().unwrap();
let transfer_oneshot: i64 = (liqee_oneshot_positive * settle / total)
.try_into()
.unwrap();
// never transfer more than pnl_transfer rounded up
// and transfer at least 1, to compensate for rounding down `settle` and int div
let max_transfer = pnl_transfer.ceil().to_num::<i64>();
limit_transfer_recurring = transfer_recurring.min(max_transfer).max(1);
// make is so the sum of recurring and oneshot doesn't exceed max_transfer
limit_transfer_oneshot = transfer_oneshot
.min(max_transfer - limit_transfer_recurring)
.max(0);
};
// The liqor pays less than the full amount to receive the positive pnl
let token_transfer = pnl_transfer * spot_gain_per_settled;
liqor_perp_position.record_liquidation_pnl_takeover(pnl_transfer, limit_transfer);
liqor_perp_position.record_liquidation_pnl_takeover(
pnl_transfer,
limit_transfer_recurring,
limit_transfer_oneshot,
);
liqee_perp_position.record_settle(pnl_transfer, &perp_market);
// Update the accounts' perp_spot_transfer statistics.
@ -615,15 +642,15 @@ pub(crate) fn liquidation_action(
liqee_health_cache.adjust_token_balance(&settle_bank, token_transfer)?;
msg!(
"pnl {} was transferred to liqor for quote {} with settle limit {}",
"pnl {} was transferred to liqor for quote {} with settle limit {} recurring/{} oneshot",
pnl_transfer,
token_transfer,
limit_transfer
limit_transfer_recurring,
limit_transfer_oneshot,
);
limit_transfer
} else {
I80F48::ZERO
limit_transfer_oneshot = 0;
limit_transfer_recurring = 0;
};
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
@ -635,7 +662,8 @@ pub(crate) fn liquidation_action(
quote_transfer_liqor,
platform_fee,
pnl_transfer,
limit_transfer,
limit_transfer_recurring,
limit_transfer_oneshot,
))
}
@ -1072,7 +1100,8 @@ mod tests {
// The settle limit taken over matches the quote pos when removing the
// quote gains from giving away base lots
assert_eq_f!(
I80F48::from_num(liqor_perp.recurring_settle_pnl_allowance),
I80F48::from_num(liqor_perp.recurring_settle_pnl_allowance)
+ liqor_perp.oneshot_settle_pnl_allowance,
liqor_perp.quote_position_native.to_num::<f64>()
+ liqor_perp.base_position_lots as f64,
1.1

View File

@ -527,6 +527,22 @@ pub struct PerpLiqBaseOrPositivePnlLogV2 {
pub price: i128,
}
#[event]
pub struct PerpLiqBaseOrPositivePnlLogV3 {
pub mango_group: Pubkey,
pub perp_market_index: u16,
pub liqor: Pubkey,
pub liqee: Pubkey,
pub base_transfer_liqee: i64,
pub quote_transfer_liqee: i128,
pub quote_transfer_liqor: i128,
pub quote_platform_fee: i128,
pub pnl_transfer: i128,
pub pnl_settle_limit_transfer_recurring: i64,
pub pnl_settle_limit_transfer_oneshot: i64,
pub price: i128,
}
#[event]
pub struct PerpLiqBankruptcyLog {
pub mango_group: Pubkey,

View File

@ -389,8 +389,9 @@ impl Bank {
pub fn verify(&self) -> Result<()> {
require_gte!(self.oracle_config.conf_filter, 0.0);
require_gte!(self.util0, I80F48::ZERO);
require_gte!(self.util1, self.util0);
require_gte!(I80F48::ONE, self.util1);
require_gte!(self.rate0, I80F48::ZERO);
require_gte!(self.util1, I80F48::ZERO);
require_gte!(self.rate1, I80F48::ZERO);
require_gte!(self.max_rate, I80F48::ZERO);
require_gte!(self.loan_fee_rate, 0.0);

View File

@ -570,7 +570,7 @@ impl PerpPosition {
.unwrap();
let upnl_abs = upnl.abs().ceil().to_num::<i64>();
self.recurring_settle_pnl_allowance =
self.recurring_settle_pnl_allowance.max(0).min(upnl_abs);
self.recurring_settle_pnl_allowance.min(upnl_abs).max(0);
self.recurring_settle_pnl_allowance - before
}
@ -667,12 +667,21 @@ impl PerpPosition {
}
let base_native = self.base_position_native(market);
let position_value = (market.stable_price() * base_native).abs().to_num::<f64>();
let unrealized = (market.settle_pnl_limit_factor as f64 * position_value).clamp_to_i64();
let position_value = market.stable_price() * base_native;
let position_value_abs = position_value.abs().to_num::<f64>();
let unrealized =
(market.settle_pnl_limit_factor as f64 * position_value_abs).clamp_to_i64();
let upnl_abs = (self.quote_position_native() + position_value)
.abs()
.ceil()
.to_num::<i64>();
let mut max_pnl = unrealized
// abs() because of potential migration
+ self.recurring_settle_pnl_allowance.abs();
// .abs() because of potential migration
// .min() to do the same as apply_recurring_settle_pnl_allowance_constraint
+ self.recurring_settle_pnl_allowance.abs().min(upnl_abs);
let mut min_pnl = -max_pnl;
let oneshot = self.oneshot_settle_pnl_allowance;
@ -777,10 +786,16 @@ impl PerpPosition {
self.oneshot_settle_pnl_allowance += change;
}
/// Adds to the quote position and adds a recurring ("realized trade") settle limit
pub fn record_liquidation_pnl_takeover(&mut self, change: I80F48, recurring_limit: I80F48) {
/// Takes over a quote position along with recurring and oneshot settle limit allowance
pub fn record_liquidation_pnl_takeover(
&mut self,
change: I80F48,
recurring_limit: i64,
oneshot_limit: i64,
) {
self.change_quote_position(change);
self.recurring_settle_pnl_allowance += recurring_limit.abs().ceil().to_num::<i64>();
self.recurring_settle_pnl_allowance += recurring_limit;
self.oneshot_settle_pnl_allowance += I80F48::from(oneshot_limit);
}
}
@ -1401,6 +1416,14 @@ mod tests {
pos.settle_pnl_limit_settled_in_current_window_native = 0;
market.stable_price_model.stable_price = 1.0;
assert_eq!(pos.available_settle_limit(&market), (-31, 36));
// because the upnl is 0 the recurring allowance doesn't count
assert_eq!(
pos.unsettled_pnl(&market, I80F48::from_num(1.0)).unwrap(),
I80F48::ZERO
);
assert_eq!(pos.available_settle_limit(&market), (-20, 25));
pos.quote_position_native += I80F48::from(7);
assert_eq!(pos.available_settle_limit(&market), (-27, 32));
}
}