diff --git a/client/src/client.rs b/client/src/client.rs index 910c7df44..cff5ae093 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -315,8 +315,7 @@ impl MangoClient { pub fn derive_liquidation_health_check_remaining_account_metas( &self, liqee: &MangoAccountValue, - asset_token_index: TokenIndex, - liab_token_index: TokenIndex, + writable_banks: &[TokenIndex], ) -> anyhow::Result> { // figure out all the banks/oracles that need to be passed for the health check let mut banks = vec![]; @@ -331,7 +330,7 @@ impl MangoClient { for token_index in token_indexes { let mint_info = self.context.mint_info(token_index); - let writable_bank = token_index == asset_token_index || token_index == liab_token_index; + let writable_bank = writable_banks.iter().contains(&token_index); banks.push((mint_info.first_bank(), writable_bank)); oracles.push(mint_info.oracle); } @@ -928,8 +927,7 @@ impl MangoClient { let perp = self.context.perp(market_index); let health_remaining_ams = self - .context - .derive_health_check_remaining_account_metas(liqee.1, vec![], false) + .derive_liquidation_health_check_remaining_account_metas(liqee.1, &[]) .unwrap(); self.program() @@ -960,6 +958,57 @@ impl MangoClient { .map_err(prettify_client_error) } + pub fn perp_liq_bankruptcy( + &self, + liqee: (&Pubkey, &MangoAccountValue), + market_index: PerpMarketIndex, + max_liab_transfer: u64, + ) -> anyhow::Result { + let quote_token_index = 0; + let quote_info = self.context.token(quote_token_index); + + let group = account_fetcher_fetch_anchor_account::( + &*self.account_fetcher, + &self.context.group, + )?; + + let perp = self.context.perp(market_index); + + let health_remaining_ams = self + .derive_liquidation_health_check_remaining_account_metas(liqee.1, &[]) + .unwrap(); + + self.program() + .request() + .instruction(Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpLiqBankruptcy { + group: self.group(), + perp_market: perp.address, + liqor: self.mango_account_address, + liqor_owner: self.owner(), + liqee: *liqee.0, + quote_bank: quote_info.mint_info.first_bank(), + quote_vault: quote_info.mint_info.first_vault(), + insurance_vault: group.insurance_vault, + token_program: Token::id(), + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::PerpLiqBankruptcy { max_liab_transfer }, + ), + }) + .signer(&self.owner) + .send() + .map_err(prettify_client_error) + } + // // Liquidation // @@ -974,8 +1023,7 @@ impl MangoClient { let health_remaining_ams = self .derive_liquidation_health_check_remaining_account_metas( liqee.1, - asset_token_index, - liab_token_index, + &[asset_token_index, liab_token_index], ) .unwrap(); @@ -1030,8 +1078,7 @@ impl MangoClient { let health_remaining_ams = self .derive_liquidation_health_check_remaining_account_metas( liqee.1, - quote_token_index, - liab_token_index, + &[quote_token_index, liab_token_index], ) .unwrap(); diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 7b09eddcb..0ed792764 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -148,7 +148,7 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn liq_perp_base_position(&self) -> anyhow::Result> { + fn perp_liq_base_position(&self) -> anyhow::Result> { let mut perp_base_positions = self .liqee .active_perp_positions() @@ -225,14 +225,14 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn settle_perp_pnl(&self) -> anyhow::Result> { + fn perp_settle_pnl(&self) -> anyhow::Result> { let spot_health = self.health_cache.spot_health(HealthType::Maint); let mut perp_settleable_pnl = self .liqee .active_perp_positions() - .map(|pp| { + .filter_map(|pp| { if pp.base_position_lots() != 0 { - return Ok(None); + return None; } let pnl = pp.quote_position_native(); let settleable_pnl = if pnl > 0 { @@ -240,12 +240,11 @@ impl<'a> LiquidateHelper<'a> { } else if pnl < 0 && spot_health > 0 { pnl.max(-spot_health) } else { - return Ok(None); + return None; }; - Ok(Some((pp.market_index, settleable_pnl))) + Some((pp.market_index, settleable_pnl)) }) - .filter_map_ok(|v| v) - .collect::>>()?; + .collect::>(); // sort by pnl, descending perp_settleable_pnl.sort_by(|a, b| b.1.cmp(&a.1)); @@ -293,6 +292,44 @@ impl<'a> LiquidateHelper<'a> { return Ok(None); } + fn perp_liq_bankruptcy(&self) -> anyhow::Result> { + if self.health_cache.has_liquidatable_assets() { + return Ok(None); + } + let mut perp_bankruptcies = self + .liqee + .active_perp_positions() + .filter_map(|pp| { + let quote = pp.quote_position_native(); + if quote >= 0 { + return None; + } + Some((pp.market_index, quote)) + }) + .collect::>(); + perp_bankruptcies.sort_by(|a, b| a.1.cmp(&b.1)); + + if perp_bankruptcies.is_empty() { + return Ok(None); + } + let (perp_market_index, _) = perp_bankruptcies.first().unwrap(); + + let sig = self.client.perp_liq_bankruptcy( + (self.pubkey, &self.liqee), + *perp_market_index, + // Always use the max amount, since the health effect is always positive + u64::MAX, + )?; + log::info!( + "Liquidated bankruptcy for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}", + self.pubkey, + perp_market_index, + self.maint_health, + sig + ); + Ok(Some(sig)) + } + fn tokens(&self) -> anyhow::Result> { let mut tokens = self .liqee @@ -354,7 +391,7 @@ impl<'a> LiquidateHelper<'a> { Ok(amount) } - fn liq_spot(&self) -> anyhow::Result> { + fn token_liq(&self) -> anyhow::Result> { if !self.health_cache.has_borrows() || self.health_cache.can_call_spot_bankruptcy() { return Ok(None); } @@ -417,7 +454,7 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(sig)) } - fn bankrupt_spot(&self) -> anyhow::Result> { + fn token_liq_bankruptcy(&self) -> anyhow::Result> { if !self.health_cache.can_call_spot_bankruptcy() { return Ok(None); } @@ -484,7 +521,7 @@ impl<'a> LiquidateHelper<'a> { return Ok(txsig); } - if let Some(txsig) = self.liq_perp_base_position()? { + if let Some(txsig) = self.perp_liq_base_position()? { return Ok(txsig); } @@ -493,21 +530,26 @@ impl<'a> LiquidateHelper<'a> { // It's possible that some positive pnl can't be settled (if there's // no liquid counterparty) and that some negative pnl can't be settled // (if the liqee isn't liquid enough). - if let Some(txsig) = self.settle_perp_pnl()? { + if let Some(txsig) = self.perp_settle_pnl()? { return Ok(txsig); } - if let Some(txsig) = self.liq_spot()? { + if let Some(txsig) = self.token_liq()? { return Ok(txsig); } - // TODO: socialize unsettleable negative pnl - // if let Some(txsig) = self.bankrupt_perp()? { - // return Ok(txsig); - // } - if let Some(txsig) = self.bankrupt_spot()? { + // Socialize/insurance fund unsettleable negative pnl + if let Some(txsig) = self.perp_liq_bankruptcy()? { return Ok(txsig); } + + // Socialize/insurance fund unliquidatable borrows + if let Some(txsig) = self.token_liq_bankruptcy()? { + return Ok(txsig); + } + + // TODO: What about unliquidatable positive perp pnl? + anyhow::bail!( "Don't know what to do with liquidatable account {}, maint_health was {}", self.pubkey, diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index e73ed28be..31150525c 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -792,7 +792,7 @@ impl HealthCache { // can use perp_liq_base_position p.base != 0 // can use perp_settle_pnl - || p.quote > ONE_NATIVE_USDC_IN_USD + || p.quote > ONE_NATIVE_USDC_IN_USD // TODO: we're not guaranteed to be able to settle positive perp pnl! // can use perp_liq_force_cancel_orders || p.has_open_orders });