diff --git a/bin/liquidator/src/liquidate.rs b/bin/liquidator/src/liquidate.rs index fc0b65984..e03d594c3 100644 --- a/bin/liquidator/src/liquidate.rs +++ b/bin/liquidator/src/liquidate.rs @@ -370,11 +370,11 @@ impl<'a> LiquidateHelper<'a> { .filter_map(|(ti, effective)| { // check constraints for liquidatable assets, see also has_possible_spot_liquidations() let tokens = ti.balance_spot.min(effective.spot_and_perp); - let is_valid_asset = tokens >= 1; + let is_valid_asset = tokens >= 1 && ti.allow_asset_liquidation; let quote_value = tokens * ti.prices.oracle; // prefer to liquidate tokens with asset weight that have >$1 liquidatable let is_preferred = - ti.init_asset_weight > 0 && quote_value > I80F48::from(1_000_000); + ti.maint_asset_weight > 0 && quote_value > I80F48::from(1_000_000); is_valid_asset.then_some((ti.token_index, is_preferred, quote_value)) }) .collect_vec(); diff --git a/mango_v4.json b/mango_v4.json index b3475028d..35363b714 100644 --- a/mango_v4.json +++ b/mango_v4.json @@ -631,6 +631,10 @@ { "name": "platformLiquidationFee", "type": "f32" + }, + { + "name": "disableAssetLiquidation", + "type": "bool" } ] }, @@ -1041,6 +1045,12 @@ "type": { "option": "f32" } + }, + { + "name": "disableAssetLiquidationOpt", + "type": { + "option": "bool" + } } ] }, @@ -7373,12 +7383,20 @@ "name": "forceClose", "type": "u8" }, + { + "name": "disableAssetLiquidation", + "docs": [ + "If set to 1, deposits cannot be liquidated when an account is liquidatable.", + "That means bankrupt accounts may still have assets of this type deposited." + ], + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 6 + 5 ] } }, @@ -14028,6 +14046,11 @@ "code": 6068, "name": "MissingFeedForCLMMOracle", "msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)" + }, + { + "code": 6069, + "name": "TokenAssetLiquidationDisabled", + "msg": "the asset does not allow liquidation" } ] } \ No newline at end of file diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index ed4702bfd..1859d26aa 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -143,6 +143,8 @@ pub enum MangoError { InvalidFeedForCLMMOracle, #[msg("Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)")] MissingFeedForCLMMOracle, + #[msg("the asset does not allow liquidation")] + TokenAssetLiquidationDisabled, } impl MangoError { diff --git a/programs/mango-v4/src/health/cache.rs b/programs/mango-v4/src/health/cache.rs index 8d29215e3..4c9fa7c3e 100644 --- a/programs/mango-v4/src/health/cache.rs +++ b/programs/mango-v4/src/health/cache.rs @@ -175,6 +175,8 @@ pub struct TokenInfo { /// Includes TokenPosition and free Serum3OpenOrders balances. /// Does not include perp upnl or Serum3 reserved amounts. pub balance_spot: I80F48, + + pub allow_asset_liquidation: bool, } /// Temporary value used during health computations @@ -907,6 +909,7 @@ impl HealthCache { } /// Liquidatable spot assets mean: actual token deposits and also a positive effective token balance + /// and is available for asset liquidation pub fn has_liq_spot_assets(&self) -> bool { let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd); self.token_infos @@ -914,11 +917,11 @@ impl HealthCache { .zip(health_token_balances.iter()) .any(|(ti, b)| { // need 1 native token to use token_liq_with_token - ti.balance_spot >= 1 && b.spot_and_perp >= 1 + ti.balance_spot >= 1 && b.spot_and_perp >= 1 && ti.allow_asset_liquidation }) } - /// Liquidatable spot borrows mean: actual toen borrows plus a negative effective token balance + /// Liquidatable spot borrows mean: actual token borrows plus a negative effective token balance pub fn has_liq_spot_borrows(&self) -> bool { let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd); self.token_infos @@ -932,7 +935,9 @@ impl HealthCache { let health_token_balances = self.effective_token_balances(HealthType::LiquidationEnd); let all_iter = || self.token_infos.iter().zip(health_token_balances.iter()); all_iter().any(|(ti, b)| ti.balance_spot < 0 && b.spot_and_perp < 0) - && all_iter().any(|(ti, b)| ti.balance_spot >= 1 && b.spot_and_perp >= 1) + && all_iter().any(|(ti, b)| { + ti.balance_spot >= 1 && b.spot_and_perp >= 1 && ti.allow_asset_liquidation + }) } pub fn has_serum3_open_orders_funds(&self) -> bool { @@ -1286,6 +1291,7 @@ fn new_health_cache_impl( init_scaled_liab_weight: bank.scaled_init_liab_weight(liab_price), prices, balance_spot: native, + allow_asset_liquidation: bank.allows_asset_liquidation(), }); } diff --git a/programs/mango-v4/src/health/client.rs b/programs/mango-v4/src/health/client.rs index 0f0bcad5f..c7bad1009 100644 --- a/programs/mango-v4/src/health/client.rs +++ b/programs/mango-v4/src/health/client.rs @@ -682,6 +682,7 @@ mod tests { init_scaled_liab_weight: I80F48::from_num(1.0 + x), prices: Prices::new_single_price(I80F48::from_num(price)), balance_spot: I80F48::ZERO, + allow_asset_liquidation: true, } } diff --git a/programs/mango-v4/src/instructions/token_edit.rs b/programs/mango-v4/src/instructions/token_edit.rs index e44e6c2ee..e98d65014 100644 --- a/programs/mango-v4/src/instructions/token_edit.rs +++ b/programs/mango-v4/src/instructions/token_edit.rs @@ -53,6 +53,7 @@ pub fn token_edit( deposit_limit_opt: Option, zero_util_rate: Option, platform_liquidation_fee: Option, + disable_asset_liquidation_opt: Option, ) -> Result<()> { let group = ctx.accounts.group.load()?; @@ -484,6 +485,16 @@ pub fn token_edit( bank.platform_liquidation_fee = I80F48::from_num(platform_liquidation_fee); require_group_admin = true; } + + if let Some(disable_asset_liquidation) = disable_asset_liquidation_opt { + msg!( + "Asset liquidation disabled old {:?}, new {:?}", + bank.disable_asset_liquidation, + disable_asset_liquidation + ); + bank.disable_asset_liquidation = u8::from(disable_asset_liquidation); + require_group_admin = true; + } } // account constraint #1 diff --git a/programs/mango-v4/src/instructions/token_liq_with_token.rs b/programs/mango-v4/src/instructions/token_liq_with_token.rs index c40a3a3c7..c064e2216 100644 --- a/programs/mango-v4/src/instructions/token_liq_with_token.rs +++ b/programs/mango-v4/src/instructions/token_liq_with_token.rs @@ -112,6 +112,10 @@ pub(crate) fn liquidation_action( liqee.token_position_and_raw_index(asset_token_index)?; let liqee_asset_native = liqee_asset_position.native(asset_bank); require_gt!(liqee_asset_native, 0); + require!( + asset_bank.allows_asset_liquidation(), + MangoError::TokenAssetLiquidationDisabled + ); let (liqee_liab_position, liqee_liab_raw_index) = liqee.token_position_and_raw_index(liab_token_index)?; diff --git a/programs/mango-v4/src/instructions/token_register.rs b/programs/mango-v4/src/instructions/token_register.rs index e9b6f5b2f..d1a82adb8 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -44,6 +44,7 @@ pub fn token_register( deposit_limit: u64, zero_util_rate: f32, platform_liquidation_fee: f32, + disable_asset_liquidation: bool, ) -> Result<()> { // Require token 0 to be in the insurance token if token_index == INSURANCE_TOKEN_INDEX { @@ -109,6 +110,7 @@ pub fn token_register( deposit_weight_scale_start_quote, reduce_only, force_close: 0, + disable_asset_liquidation: u8::from(disable_asset_liquidation), padding: Default::default(), fees_withdrawn: 0, token_conditional_swap_taker_fee_rate, diff --git a/programs/mango-v4/src/instructions/token_register_trustless.rs b/programs/mango-v4/src/instructions/token_register_trustless.rs index fa2f9a3f1..54c5fdc78 100644 --- a/programs/mango-v4/src/instructions/token_register_trustless.rs +++ b/programs/mango-v4/src/instructions/token_register_trustless.rs @@ -90,6 +90,7 @@ pub fn token_register_trustless( deposit_weight_scale_start_quote: 5_000_000_000.0, // $5k reduce_only: 2, // deposit-only force_close: 0, + disable_asset_liquidation: 1, padding: Default::default(), fees_withdrawn: 0, token_conditional_swap_taker_fee_rate: 0.0, diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index bc5deb976..e3ef47a95 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -157,6 +157,7 @@ pub mod mango_v4 { deposit_limit: u64, zero_util_rate: f32, platform_liquidation_fee: f32, + disable_asset_liquidation: bool, ) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::token_register( @@ -190,6 +191,7 @@ pub mod mango_v4 { deposit_limit, zero_util_rate, platform_liquidation_fee, + disable_asset_liquidation, )?; Ok(()) } @@ -245,6 +247,7 @@ pub mod mango_v4 { deposit_limit_opt: Option, zero_util_rate_opt: Option, platform_liquidation_fee_opt: Option, + disable_asset_liquidation_opt: Option, ) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::token_edit( @@ -287,6 +290,7 @@ pub mod mango_v4 { deposit_limit_opt, zero_util_rate_opt, platform_liquidation_fee_opt, + disable_asset_liquidation_opt, )?; Ok(()) } diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 7a9fbbde7..cafd731cb 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -158,8 +158,12 @@ pub struct Bank { pub reduce_only: u8, pub force_close: u8, + /// If set to 1, deposits cannot be liquidated when an account is liquidatable. + /// That means bankrupt accounts may still have assets of this type deposited. + pub disable_asset_liquidation: u8, + #[derivative(Debug = "ignore")] - pub padding: [u8; 6], + pub padding: [u8; 5], // Do separate bookkeping for how many tokens were withdrawn // This ensures that collected_fees_native is strictly increasing for stats gathering purposes @@ -346,7 +350,8 @@ impl Bank { deposit_weight_scale_start_quote: existing_bank.deposit_weight_scale_start_quote, reduce_only: existing_bank.reduce_only, force_close: existing_bank.force_close, - padding: [0; 6], + disable_asset_liquidation: existing_bank.disable_asset_liquidation, + padding: [0; 5], token_conditional_swap_taker_fee_rate: existing_bank .token_conditional_swap_taker_fee_rate, token_conditional_swap_maker_fee_rate: existing_bank @@ -396,6 +401,10 @@ impl Bank { require_gte!(self.maint_weight_shift_liab_target, 0.0); require_gte!(self.zero_util_rate, I80F48::ZERO); require_gte!(self.platform_liquidation_fee, 0.0); + if !self.allows_asset_liquidation() { + require!(self.are_borrows_reduce_only(), MangoError::SomeError); + require_eq!(self.maint_asset_weight, I80F48::ZERO); + } Ok(()) } @@ -417,6 +426,10 @@ impl Bank { self.force_close == 1 } + pub fn allows_asset_liquidation(&self) -> bool { + self.disable_asset_liquidation == 0 + } + #[inline(always)] pub fn native_borrows(&self) -> I80F48 { self.borrow_index * self.indexed_borrows diff --git a/programs/mango-v4/tests/cases/test_liq_tokens.rs b/programs/mango-v4/tests/cases/test_liq_tokens.rs index 56faf7cca..db049681d 100644 --- a/programs/mango-v4/tests/cases/test_liq_tokens.rs +++ b/programs/mango-v4/tests/cases/test_liq_tokens.rs @@ -324,6 +324,66 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> { // set_bank_stub_oracle_price(solana, group, borrow_token1, admin, 2.0).await; + // + // TEST: can't liquidate if token has no asset weight + // + + send_tx( + solana, + TokenEdit { + group, + admin, + mint: collateral_token2.mint.pubkey, + fallback_oracle: Pubkey::default(), + options: mango_v4::instruction::TokenEdit { + maint_asset_weight_opt: Some(0.0), + init_asset_weight_opt: Some(0.0), + disable_asset_liquidation_opt: Some(true), + reduce_only_opt: Some(1), + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + let res = send_tx( + solana, + TokenLiqWithTokenInstruction { + liqee: account, + liqor: vault_account, + liqor_owner: owner, + asset_token_index: collateral_token2.index, + liab_token_index: borrow_token2.index, + asset_bank_index: 0, + liab_bank_index: 0, + max_liab_transfer: I80F48::from_num(10000.0), + }, + ) + .await; + assert_mango_error( + &res, + MangoError::TokenAssetLiquidationDisabled.into(), + "liquidation disabled".to_string(), + ); + send_tx( + solana, + TokenEdit { + group, + admin, + mint: collateral_token2.mint.pubkey, + fallback_oracle: Pubkey::default(), + options: mango_v4::instruction::TokenEdit { + maint_asset_weight_opt: Some(0.8), + init_asset_weight_opt: Some(0.6), + disable_asset_liquidation_opt: Some(false), + reduce_only_opt: Some(0), + ..token_edit_instruction_default() + }, + }, + ) + .await + .unwrap(); + // // TEST: liquidate borrow2 against too little collateral2 // diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index e0b5bcd13..e3a3c23cd 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -1077,6 +1077,7 @@ impl ClientInstruction for TokenRegisterInstruction { deposit_limit: 0, zero_util_rate: 0.0, platform_liquidation_fee: self.platform_liquidation_fee, + disable_asset_liquidation: false, }; let bank = Pubkey::find_program_address( @@ -1324,6 +1325,7 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit { deposit_limit_opt: None, zero_util_rate_opt: None, platform_liquidation_fee_opt: None, + disable_asset_liquidation_opt: None, } } diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index 4aead0732..9f4408c09 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -130,6 +130,7 @@ export class Bank implements BankForHealth { depositWeightScaleStartQuote: number; reduceOnly: number; forceClose: number; + disableAssetLiquidation: number; feesWithdrawn: BN; tokenConditionalSwapTakerFeeRate: number; tokenConditionalSwapMakerFeeRate: number; @@ -211,6 +212,7 @@ export class Bank implements BankForHealth { obj.zeroUtilRate, obj.platformLiquidationFee, obj.collectedLiquidationFees, + obj.disableAssetLiquidation == 0, ); } @@ -276,6 +278,7 @@ export class Bank implements BankForHealth { zeroUtilRate: I80F48Dto, platformLiquidationFee: I80F48Dto, collectedLiquidationFees: I80F48Dto, + public allowAssetLiquidation: boolean, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; this.oracleConfig = { diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 212b48650..24067689a 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -461,6 +461,7 @@ export class MangoClient { params.depositLimit, params.zeroUtilRate, params.platformLiquidationFee, + params.disableAssetLiquidation, ) .accounts({ group: group.publicKey, @@ -548,6 +549,7 @@ export class MangoClient { params.depositLimit, params.zeroUtilRate, params.platformLiquidationFee, + params.disableAssetLiquidation, ) .accounts({ group: group.publicKey, diff --git a/ts/client/src/clientIxParamBuilder.ts b/ts/client/src/clientIxParamBuilder.ts index 7a9255ede..20b83fe33 100644 --- a/ts/client/src/clientIxParamBuilder.ts +++ b/ts/client/src/clientIxParamBuilder.ts @@ -30,6 +30,7 @@ export interface TokenRegisterParams { depositLimit: BN; zeroUtilRate: number; platformLiquidationFee: number; + disableAssetLiquidation: boolean; } export const DefaultTokenRegisterParams: TokenRegisterParams = { @@ -70,6 +71,7 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = { depositLimit: new BN(0), zeroUtilRate: 0.0, platformLiquidationFee: 0.0, + disableAssetLiquidation: false, }; export interface TokenEditParams { @@ -111,6 +113,7 @@ export interface TokenEditParams { depositLimit: BN | null; zeroUtilRate: number | null; platformLiquidationFee: number | null; + disableAssetLiquidation: boolean | null; } export const NullTokenEditParams: TokenEditParams = { @@ -152,6 +155,7 @@ export const NullTokenEditParams: TokenEditParams = { depositLimit: null, zeroUtilRate: null, platformLiquidationFee: null, + disableAssetLiquidation: null, }; export interface PerpEditParams { diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 463d43468..ee363dabb 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -631,6 +631,10 @@ export type MangoV4 = { { "name": "platformLiquidationFee", "type": "f32" + }, + { + "name": "disableAssetLiquidation", + "type": "bool" } ] }, @@ -1041,6 +1045,12 @@ export type MangoV4 = { "type": { "option": "f32" } + }, + { + "name": "disableAssetLiquidationOpt", + "type": { + "option": "bool" + } } ] }, @@ -7373,12 +7383,20 @@ export type MangoV4 = { "name": "forceClose", "type": "u8" }, + { + "name": "disableAssetLiquidation", + "docs": [ + "If set to 1, deposits cannot be liquidated when an account is liquidatable.", + "That means bankrupt accounts may still have assets of this type deposited." + ], + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 6 + 5 ] } }, @@ -14028,6 +14046,11 @@ export type MangoV4 = { "code": 6068, "name": "MissingFeedForCLMMOracle", "msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)" + }, + { + "code": 6069, + "name": "TokenAssetLiquidationDisabled", + "msg": "the asset does not allow liquidation" } ] }; @@ -14665,6 +14688,10 @@ export const IDL: MangoV4 = { { "name": "platformLiquidationFee", "type": "f32" + }, + { + "name": "disableAssetLiquidation", + "type": "bool" } ] }, @@ -15075,6 +15102,12 @@ export const IDL: MangoV4 = { "type": { "option": "f32" } + }, + { + "name": "disableAssetLiquidationOpt", + "type": { + "option": "bool" + } } ] }, @@ -21407,12 +21440,20 @@ export const IDL: MangoV4 = { "name": "forceClose", "type": "u8" }, + { + "name": "disableAssetLiquidation", + "docs": [ + "If set to 1, deposits cannot be liquidated when an account is liquidatable.", + "That means bankrupt accounts may still have assets of this type deposited." + ], + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 6 + 5 ] } }, @@ -28062,6 +28103,11 @@ export const IDL: MangoV4 = { "code": 6068, "name": "MissingFeedForCLMMOracle", "msg": "Pyth USDC/USD or SOL/USD feed not found (required by CLMM oracle)" + }, + { + "code": 6069, + "name": "TokenAssetLiquidationDisabled", + "msg": "the asset does not allow liquidation" } ] };