From 845a32a7c235e906b860da9e651fc74be26ca414 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 7 Sep 2022 16:10:49 +0200 Subject: [PATCH] Add PerpLiqBasePosition instruction --- programs/mango-v4/src/error.rs | 2 + programs/mango-v4/src/instructions/mod.rs | 2 + .../instructions/perp_liq_base_position.rs | 191 ++++++++++++++++++ programs/mango-v4/src/lib.rs | 9 +- .../src/state/mango_account_components.rs | 8 + 5 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 programs/mango-v4/src/instructions/perp_liq_base_position.rs diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index d7e633045..44ca70aee 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -49,6 +49,8 @@ pub enum MangoError { PerpPositionDoesNotExist, #[msg("max settle amount must be greater than zero")] MaxSettleAmountMustBeGreaterThanZero, + #[msg("the perp position has open orders or unprocessed fill events")] + HasOpenPerpOrders, } pub trait Contextable { diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index bb83769ed..bc4cb064b 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -20,6 +20,7 @@ pub use perp_consume_events::*; pub use perp_create_market::*; pub use perp_deactivate_position::*; pub use perp_edit_market::*; +pub use perp_liq_base_position::*; pub use perp_place_order::*; pub use perp_settle_fees::*; pub use perp_settle_pnl::*; @@ -67,6 +68,7 @@ mod perp_consume_events; mod perp_create_market; mod perp_deactivate_position; mod perp_edit_market; +mod perp_liq_base_position; mod perp_place_order; mod perp_settle_fees; mod perp_settle_pnl; diff --git a/programs/mango-v4/src/instructions/perp_liq_base_position.rs b/programs/mango-v4/src/instructions/perp_liq_base_position.rs new file mode 100644 index 000000000..32a1acc89 --- /dev/null +++ b/programs/mango-v4/src/instructions/perp_liq_base_position.rs @@ -0,0 +1,191 @@ +use anchor_lang::prelude::*; +use checked_math as cm; +use fixed::types::I80F48; + +use crate::accounts_zerocopy::*; +use crate::error::*; +use crate::state::*; + +#[derive(Accounts)] +pub struct PerpLiqBasePosition<'info> { + pub group: AccountLoader<'info, Group>, + + #[account(mut, has_one = group, has_one = oracle)] + pub perp_market: AccountLoader<'info, PerpMarket>, + + /// CHECK: Oracle can have different account types, constrained by address in perp_market + pub oracle: UncheckedAccount<'info>, + + #[account( + mut, + has_one = group + // liqor_owner is checked at #1 + )] + pub liqor: AccountLoaderDynamic<'info, MangoAccount>, + pub liqor_owner: Signer<'info>, + + #[account(mut, has_one = group)] + pub liqee: AccountLoaderDynamic<'info, MangoAccount>, +} + +pub fn perp_liq_base_position( + ctx: Context, + max_base_transfer: i64, +) -> Result<()> { + let group_pk = &ctx.accounts.group.key(); + + let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk) + .context("create account retriever")?; + + let mut liqor = ctx.accounts.liqor.load_mut()?; + // account constraint #1 + require!( + liqor + .fixed + .is_owner_or_delegate(ctx.accounts.liqor_owner.key()), + MangoError::SomeError + ); + require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated); + + let mut liqee = ctx.accounts.liqee.load_mut()?; + + // 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); + + // Once maint_health falls below 0, we want to start liquidating, + // we want to allow liquidation to continue until init_health is positive, + // to prevent constant oscillation between the two states + if liqee.being_liquidated() { + if liqee + .fixed + .maybe_recover_from_being_liquidated(liqee_init_health) + { + msg!("Liqee init_health above zero"); + return Ok(()); + } + } else { + let maint_health = liqee_health_cache.health(HealthType::Maint); + require!( + maint_health < I80F48::ZERO, + MangoError::HealthMustBeNegative + ); + liqee.fixed.set_being_liquidated(true); + } + + let mut perp_market = ctx.accounts.perp_market.load_mut()?; + let perp_market_index = perp_market.perp_market_index; + let base_lot_size = I80F48::from(perp_market.base_lot_size); + + // Get oracle price for market. Price is validated inside + let oracle_price = + perp_market.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?; + let price_per_lot = cm!(base_lot_size * oracle_price); + + // Fetch perp positions for accounts, creating for the liqor if needed + let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?; + let liqor_perp_position = liqor.ensure_perp_position(perp_market_index)?.0; + let liqee_base_lots = liqee_perp_position.base_position_lots(); + + require!( + !liqee_perp_position.has_open_orders(), + MangoError::HasOpenPerpOrders + ); + + // Settle funding + liqee_perp_position.settle_funding(&perp_market); + liqor_perp_position.settle_funding(&perp_market); + + // 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" + ); + + // 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 - perp_market.init_asset_weight - perp_market.liquidation_fee)); + + // 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)); + + (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 - quote_liab_weight + perp_market.liquidation_fee)); + + // (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)); + + (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. + liqee_perp_position.change_base_and_quote_positions( + &mut perp_market, + -base_transfer, + -quote_transfer, + ); + liqor_perp_position.change_base_and_quote_positions( + &mut perp_market, + base_transfer, + quote_transfer, + ); + + // Check liqee health again + liqee_health_cache.recompute_perp_info(liqee_perp_position, &perp_market)?; + let liqee_init_health = liqee_health_cache.health(HealthType::Init); + liqee + .fixed + .maybe_recover_from_being_liquidated(liqee_init_health); + + drop(perp_market); + + // Check liqor's health + if !liqor.fixed.is_in_health_region() { + let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever) + .context("compute liqor health")?; + require!(liqor_health >= 0, MangoError::HealthMustBePositive); + } + + Ok(()) +} diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 724229cac..03273ff2b 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -506,12 +506,19 @@ pub mod mango_v4 { pub fn perp_settle_fees(ctx: Context, max_settle_amount: u64) -> Result<()> { instructions::perp_settle_fees(ctx, max_settle_amount) } + + pub fn perp_liq_base_position( + ctx: Context, + max_base_transfer: i64, + ) -> Result<()> { + instructions::perp_liq_base_position(ctx, max_base_transfer) + } + // TODO // perp_force_cancel_order // liquidate_token_and_perp - // liquidate_perp_and_perp // settle_* - settle_funds diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index d5f249f3b..da597fc3f 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -326,6 +326,14 @@ impl PerpPosition { cm!(self.quote_position_native += quote_change_native); } + /// Does the perp position have any open orders or fill events? + pub fn has_open_orders(&self) -> bool { + self.asks_base_lots != 0 + || self.bids_base_lots != 0 + || self.taker_base_lots != 0 + || self.taker_quote_lots != 0 + } + /// Calculate the average entry price of the position pub fn avg_entry_price(&self) -> I80F48 { if self.base_position_lots == 0 {