From ce714ea1d2e42fdbd387e92b6f917466203b7c81 Mon Sep 17 00:00:00 2001 From: J Date: Wed, 8 May 2024 14:01:19 +0200 Subject: [PATCH] Permissionless Bad Debt handling (#207) * feat: add permissionless bad debt flag * fix: changed account names * fix: fuzz + tests --------- Co-authored-by: Jakob --- clients/rust/marginfi-cli/src/entrypoint.rs | 7 + .../rust/marginfi-cli/src/processor/mod.rs | 6 +- programs/marginfi/fuzz/src/lib.rs | 2 +- programs/marginfi/src/constants.rs | 4 + programs/marginfi/src/errors.rs | 2 + .../marginfi_group/configure_bank.rs | 6 +- .../marginfi_group/handle_bankruptcy.rs | 19 +- .../marginfi/src/state/marginfi_account.rs | 4 +- programs/marginfi/src/state/marginfi_group.rs | 47 +++- programs/marginfi/tests/bankruptcy_auth.rs | 204 ++++++++++++++++++ programs/marginfi/tests/marginfi_account.rs | 7 +- test-utils/src/marginfi_group.rs | 2 +- tools/llama-snapshot-tool/src/bin/main.rs | 4 +- 13 files changed, 286 insertions(+), 28 deletions(-) create mode 100644 programs/marginfi/tests/bankruptcy_auth.rs diff --git a/clients/rust/marginfi-cli/src/entrypoint.rs b/clients/rust/marginfi-cli/src/entrypoint.rs index bd68653b..f81c1e0f 100644 --- a/clients/rust/marginfi-cli/src/entrypoint.rs +++ b/clients/rust/marginfi-cli/src/entrypoint.rs @@ -251,6 +251,11 @@ pub enum BankCommand { usd_init_limit: Option, #[clap(long, help = "Oracle max age in seconds, 0 to use default value (60s)")] oracle_max_age: Option, + #[clap( + long, + help = "Permissionless bad debt settlement, if true the group admin is not required to settle bad debt" + )] + permissionless_bad_debt_settlement: Option, }, #[cfg(feature = "dev")] InspectPriceOracle { @@ -614,6 +619,7 @@ fn bank(subcmd: BankCommand, global_options: &GlobalOptions) -> Result<()> { oracle_key, usd_init_limit, oracle_max_age, + permissionless_bad_debt_settlement, } => { let bank = config .mfi_program @@ -661,6 +667,7 @@ fn bank(subcmd: BankCommand, global_options: &GlobalOptions) -> Result<()> { risk_tier: risk_tier.map(|x| x.into()), total_asset_value_init_limit: usd_init_limit, oracle_max_age, + permissionless_bad_debt_settlement, }, ) } diff --git a/clients/rust/marginfi-cli/src/processor/mod.rs b/clients/rust/marginfi-cli/src/processor/mod.rs index f47c4c32..78a36fd0 100644 --- a/clients/rust/marginfi-cli/src/processor/mod.rs +++ b/clients/rust/marginfi-cli/src/processor/mod.rs @@ -197,7 +197,7 @@ Last Update: {:?}h ago ({}) bank.config.oracle_setup, bank.config.oracle_keys, bank.config.get_oracle_max_age(), - bank.emissions_flags, + bank.flags, I80F48::from(bank.emissions_rate), bank.emissions_mint, I80F48::from(bank.emissions_remaining), @@ -736,7 +736,7 @@ fn handle_bankruptcy_for_an_account( program_id: config.program_id, accounts: marginfi::accounts::LendingPoolHandleBankruptcy { marginfi_group: profile.marginfi_group.unwrap(), - admin: config.authority(), + signer: config.authority(), bank: bank_pk, marginfi_account: marginfi_account_pk, liquidity_vault: find_bank_vault_pda( @@ -881,7 +881,7 @@ fn make_bankruptcy_ix( program_id: config.program_id, accounts: marginfi::accounts::LendingPoolHandleBankruptcy { marginfi_group: profile.marginfi_group.unwrap(), - admin: config.fee_payer.pubkey(), + signer: config.fee_payer.pubkey(), bank: bank_pk, marginfi_account: marginfi_account_pk, liquidity_vault: find_bank_vault_pda( diff --git a/programs/marginfi/fuzz/src/lib.rs b/programs/marginfi/fuzz/src/lib.rs index 106f70d8..f418460d 100644 --- a/programs/marginfi/fuzz/src/lib.rs +++ b/programs/marginfi/fuzz/src/lib.rs @@ -665,7 +665,7 @@ impl<'bump> MarginfiFuzzContext<'bump> { &marginfi::ID, &mut marginfi::instructions::LendingPoolHandleBankruptcy { marginfi_group: AccountLoader::try_from(&self.marginfi_group.clone())?, - admin: Signer::try_from(&self.owner)?, + signer: Signer::try_from(&self.owner)?, bank: AccountLoader::try_from(&bank.bank.clone())?, marginfi_account: AccountLoader::try_from( &marginfi_account.margin_account.clone(), diff --git a/programs/marginfi/src/constants.rs b/programs/marginfi/src/constants.rs index a3cdebb3..d563ba23 100644 --- a/programs/marginfi/src/constants.rs +++ b/programs/marginfi/src/constants.rs @@ -60,6 +60,10 @@ pub const ZERO_AMOUNT_THRESHOLD: I80F48 = I80F48!(0.0001); pub const EMISSIONS_FLAG_BORROW_ACTIVE: u64 = 1 << 0; pub const EMISSIONS_FLAG_LENDING_ACTIVE: u64 = 1 << 1; +pub const PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG: u64 = 1 << 2; + +pub(crate) const EMISSION_FLAGS: u64 = EMISSIONS_FLAG_BORROW_ACTIVE | EMISSIONS_FLAG_LENDING_ACTIVE; +pub(crate) const GROUP_FLAGS: u64 = PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG; /// Cutoff timestamp for balance last_update used in accounting collected emissions. /// Any balance updates before this timestamp are ignored, and current_timestamp is used instead. diff --git a/programs/marginfi/src/errors.rs b/programs/marginfi/src/errors.rs index 7cb62116..2b219f42 100644 --- a/programs/marginfi/src/errors.rs +++ b/programs/marginfi/src/errors.rs @@ -91,6 +91,8 @@ pub enum MarginfiError { IllegalBalanceState, #[msg("Illegal account authority transfer")] // 6044 IllegalAccountAuthorityTransfer, + #[msg("Unauthorized")] // 6045 + Unauthorized, } impl From for ProgramError { diff --git a/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs b/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs index 7b0d7839..097e5899 100644 --- a/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs +++ b/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs @@ -65,7 +65,9 @@ pub fn lending_pool_setup_emissions( ); bank.emissions_mint = ctx.accounts.emissions_mint.key(); - bank.emissions_flags = emissions_flags; + + bank.override_emissions_flag(emissions_flags); + bank.emissions_rate = emissions_rate; bank.emissions_remaining = I80F48::from_num(total_emissions).into(); @@ -155,7 +157,7 @@ pub fn lending_pool_update_emissions_parameters( if let Some(flags) = emissions_flags { msg!("Updating emissions flags to {:#010b}", flags); - bank.emissions_flags = flags; + bank.flags = flags; } if let Some(rate) = emissions_rate { diff --git a/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs b/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs index 315a3fd1..eaba6476 100644 --- a/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs +++ b/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs @@ -1,4 +1,4 @@ -use crate::constants::ZERO_AMOUNT_THRESHOLD; +use crate::constants::{PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG, ZERO_AMOUNT_THRESHOLD}; use crate::events::{AccountEventHeader, LendingPoolBankHandleBankruptcyEvent}; use crate::state::marginfi_account::DISABLED_FLAG; use crate::{ @@ -29,8 +29,19 @@ pub fn lending_pool_handle_bankruptcy(ctx: Context) insurance_vault, token_program, bank: bank_loader, + marginfi_group: marginfi_group_loader, .. } = ctx.accounts; + let bank = bank_loader.load()?; + + if !bank.get_flag(PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG) { + check!( + ctx.accounts.signer.key() == marginfi_group_loader.load()?.admin, + MarginfiError::Unauthorized + ); + } + + drop(bank); let mut marginfi_account = marginfi_account_loader.load_mut()?; @@ -107,7 +118,7 @@ pub fn lending_pool_handle_bankruptcy(ctx: Context) emit!(LendingPoolBankHandleBankruptcyEvent { header: AccountEventHeader { - signer: Some(ctx.accounts.admin.key()), + signer: Some(ctx.accounts.signer.key()), marginfi_account: marginfi_account_loader.key(), marginfi_account_authority: marginfi_account.authority, marginfi_group: marginfi_account.group, @@ -126,8 +137,8 @@ pub fn lending_pool_handle_bankruptcy(ctx: Context) pub struct LendingPoolHandleBankruptcy<'info> { pub marginfi_group: AccountLoader<'info, MarginfiGroup>, - #[account(address = marginfi_group.load()?.admin)] - pub admin: Signer<'info>, + // #[account(address = marginfi_group.load()?.admin)] + pub signer: Signer<'info>, #[account( mut, diff --git a/programs/marginfi/src/state/marginfi_account.rs b/programs/marginfi/src/state/marginfi_account.rs index df094340..65892e0f 100644 --- a/programs/marginfi/src/state/marginfi_account.rs +++ b/programs/marginfi/src/state/marginfi_account.rs @@ -1150,8 +1150,8 @@ impl<'a> BankAccountWrapper<'a> { pub fn claim_emissions(&mut self, current_timestamp: u64) -> MarginfiResult { if let Some(balance_amount) = match ( self.balance.get_side(), - self.bank.get_emissions_flag(EMISSIONS_FLAG_LENDING_ACTIVE), - self.bank.get_emissions_flag(EMISSIONS_FLAG_BORROW_ACTIVE), + self.bank.get_flag(EMISSIONS_FLAG_LENDING_ACTIVE), + self.bank.get_flag(EMISSIONS_FLAG_BORROW_ACTIVE), ) { (Some(BalanceSide::Assets), true, _) => Some( self.bank diff --git a/programs/marginfi/src/state/marginfi_group.rs b/programs/marginfi/src/state/marginfi_group.rs index 8855666e..cbd4ef9a 100644 --- a/programs/marginfi/src/state/marginfi_group.rs +++ b/programs/marginfi/src/state/marginfi_group.rs @@ -7,9 +7,10 @@ use crate::events::{GroupEventHeader, LendingPoolBankAccrueInterestEvent}; use crate::{ assert_struct_align, assert_struct_size, check, constants::{ - FEE_VAULT_AUTHORITY_SEED, FEE_VAULT_SEED, INSURANCE_VAULT_AUTHORITY_SEED, - INSURANCE_VAULT_SEED, LIQUIDITY_VAULT_AUTHORITY_SEED, LIQUIDITY_VAULT_SEED, - MAX_ORACLE_KEYS, MAX_PRICE_AGE_SEC, PYTH_ID, SECONDS_PER_YEAR, + EMISSION_FLAGS, FEE_VAULT_AUTHORITY_SEED, FEE_VAULT_SEED, GROUP_FLAGS, + INSURANCE_VAULT_AUTHORITY_SEED, INSURANCE_VAULT_SEED, LIQUIDITY_VAULT_AUTHORITY_SEED, + LIQUIDITY_VAULT_SEED, MAX_ORACLE_KEYS, MAX_PRICE_AGE_SEC, + PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG, PYTH_ID, SECONDS_PER_YEAR, TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE, }, debug, math_error, @@ -316,12 +317,13 @@ pub struct Bank { pub config: BankConfig, - /// Emissions Config Flags + /// Bank Config Flags /// /// - EMISSIONS_FLAG_BORROW_ACTIVE: 1 /// - EMISSIONS_FLAG_LENDING_ACTIVE: 2 + /// - PERMISSIONLESS_BAD_DEBT_SETTLEMENT: 4 /// - pub emissions_flags: u64, + pub flags: u64, /// Emissions APR. /// Number of emitted tokens (emissions_mint) per 1e(bank.mint_decimal) tokens (bank mint) (native amount) per 1 YEAR. pub emissions_rate: u64, @@ -371,7 +373,7 @@ impl Bank { total_asset_shares: I80F48::ZERO.into(), last_update: current_timestamp, config, - emissions_flags: 0, + flags: 0, emissions_rate: 0, emissions_remaining: I80F48::ZERO.into(), emissions_mint: Pubkey::default(), @@ -541,6 +543,10 @@ impl Bank { set_if_some!(self.config.oracle_max_age, config.oracle_max_age); + if let Some(flag) = config.permissionless_bad_debt_settlement { + self.update_flag(flag, PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG); + } + self.config.validate()?; Ok(()) @@ -717,8 +723,31 @@ impl Bank { } } - pub fn get_emissions_flag(&self, flag: u64) -> bool { - (self.emissions_flags & flag) == flag + pub fn get_flag(&self, flag: u64) -> bool { + (self.flags & flag) == flag + } + + pub(crate) fn override_emissions_flag(&mut self, flag: u64) { + assert!(Self::verify_emissions_flags(flag)); + self.flags = flag; + } + + pub(crate) fn update_flag(&mut self, value: bool, flag: u64) { + assert!(Self::verify_group_flags(flag)); + + if value { + self.flags |= flag; + } else { + self.flags &= !flag; + } + } + + const fn verify_emissions_flags(flags: u64) -> bool { + flags & EMISSION_FLAGS == flags + } + + const fn verify_group_flags(flags: u64) -> bool { + flags & GROUP_FLAGS == flags } } @@ -1151,6 +1180,8 @@ pub struct BankConfigOpt { pub total_asset_value_init_limit: Option, pub oracle_max_age: Option, + + pub permissionless_bad_debt_settlement: Option, } #[cfg_attr( diff --git a/programs/marginfi/tests/bankruptcy_auth.rs b/programs/marginfi/tests/bankruptcy_auth.rs new file mode 100644 index 00000000..9bbfae4a --- /dev/null +++ b/programs/marginfi/tests/bankruptcy_auth.rs @@ -0,0 +1,204 @@ +use fixed_macro::types::I80F48; +use fixtures::{ + assert_custom_error, + test::{BankMint, TestBankSetting, TestFixture, TestSettings, DEFAULT_SOL_TEST_BANK_CONFIG}, +}; +use marginfi::{ + errors::MarginfiError, + state::marginfi_group::{BankConfig, BankConfigOpt, BankVaultType, GroupConfig}, +}; +use solana_program_test::tokio; +use solana_sdk::pubkey::Pubkey; + +#[tokio::test] +async fn marginfi_group_handle_bankruptcy_unauthorized() -> anyhow::Result<()> { + let mut test_f = TestFixture::new(Some(TestSettings { + group_config: Some(GroupConfig { admin: None }), + banks: vec![ + TestBankSetting { + mint: BankMint::USDC, + config: None, + }, + TestBankSetting { + mint: BankMint::SOL, + config: Some(BankConfig { + asset_weight_init: I80F48!(1).into(), + ..*DEFAULT_SOL_TEST_BANK_CONFIG + }), + }, + ], + })) + .await; + + let lender_mfi_account_f = test_f.create_marginfi_account().await; + let lender_token_account_usdc = test_f + .usdc_mint + .create_token_account_and_mint_to(100_000) + .await; + lender_mfi_account_f + .try_bank_deposit( + lender_token_account_usdc.key, + test_f.get_bank(&BankMint::USDC), + 100_000, + ) + .await?; + + let borrower_account = test_f.create_marginfi_account().await; + let borrower_deposit_account = test_f + .sol_mint + .create_token_account_and_mint_to(1_001) + .await; + + borrower_account + .try_bank_deposit( + borrower_deposit_account.key, + test_f.get_bank(&BankMint::SOL), + 1_001, + ) + .await?; + + let borrower_borrow_account = test_f.usdc_mint.create_token_account_and_mint_to(0).await; + + borrower_account + .try_bank_borrow( + borrower_borrow_account.key, + test_f.get_bank(&BankMint::USDC), + 10_000, + ) + .await?; + + let mut borrower_mfi_account = borrower_account.load().await; + borrower_mfi_account.lending_account.balances[0] + .asset_shares + .value = 0_i128.to_le_bytes(); + borrower_account.set_account(&borrower_mfi_account).await?; + + { + let (insurance_vault, _) = test_f + .get_bank(&BankMint::USDC) + .get_vault(BankVaultType::Insurance); + test_f + .get_bank_mut(&BankMint::USDC) + .mint + .mint_to(&insurance_vault, 10_000) + .await; + } + + test_f + .marginfi_group + .try_update(GroupConfig { + admin: Some(Pubkey::new_unique()), + }) + .await?; + + let bank = test_f.get_bank(&BankMint::USDC); + + let res = test_f + .marginfi_group + .try_handle_bankruptcy(bank, &borrower_account) + .await; + + assert!(res.is_err()); + assert_custom_error!(res.unwrap_err(), MarginfiError::Unauthorized); + + Ok(()) +} + +#[tokio::test] +async fn marginfi_group_handle_bankruptcy_perimssionless() -> anyhow::Result<()> { + let mut test_f = TestFixture::new(Some(TestSettings { + group_config: Some(GroupConfig { admin: None }), + banks: vec![ + TestBankSetting { + mint: BankMint::USDC, + config: None, + }, + TestBankSetting { + mint: BankMint::SOL, + config: Some(BankConfig { + asset_weight_init: I80F48!(1).into(), + ..*DEFAULT_SOL_TEST_BANK_CONFIG + }), + }, + ], + })) + .await; + + let lender_mfi_account_f = test_f.create_marginfi_account().await; + let lender_token_account_usdc = test_f + .usdc_mint + .create_token_account_and_mint_to(100_000) + .await; + lender_mfi_account_f + .try_bank_deposit( + lender_token_account_usdc.key, + test_f.get_bank(&BankMint::USDC), + 100_000, + ) + .await?; + + let borrower_account = test_f.create_marginfi_account().await; + let borrower_deposit_account = test_f + .sol_mint + .create_token_account_and_mint_to(1_001) + .await; + + borrower_account + .try_bank_deposit( + borrower_deposit_account.key, + test_f.get_bank(&BankMint::SOL), + 1_001, + ) + .await?; + + let borrower_borrow_account = test_f.usdc_mint.create_token_account_and_mint_to(0).await; + + borrower_account + .try_bank_borrow( + borrower_borrow_account.key, + test_f.get_bank(&BankMint::USDC), + 10_000, + ) + .await?; + + let mut borrower_mfi_account = borrower_account.load().await; + borrower_mfi_account.lending_account.balances[0] + .asset_shares + .value = 0_i128.to_le_bytes(); + borrower_account.set_account(&borrower_mfi_account).await?; + + { + let (insurance_vault, _) = test_f + .get_bank(&BankMint::USDC) + .get_vault(BankVaultType::Insurance); + test_f + .get_bank_mut(&BankMint::USDC) + .mint + .mint_to(&insurance_vault, 10_000) + .await; + } + + let bank = test_f.get_bank(&BankMint::USDC); + + bank.update_config(BankConfigOpt { + permissionless_bad_debt_settlement: Some(true), + ..Default::default() + }) + .await?; + + test_f + .marginfi_group + .try_update(GroupConfig { + admin: Some(Pubkey::new_unique()), + }) + .await?; + + let res = test_f + .marginfi_group + .try_handle_bankruptcy(bank, &borrower_account) + .await; + + assert!(res.is_ok()); + + Ok(()) +} diff --git a/programs/marginfi/tests/marginfi_account.rs b/programs/marginfi/tests/marginfi_account.rs index 5da4cc7b..86ea820a 100644 --- a/programs/marginfi/tests/marginfi_account.rs +++ b/programs/marginfi/tests/marginfi_account.rs @@ -1716,10 +1716,7 @@ async fn emissions_test_2() -> anyhow::Result<()> { let usdc_bank_data = usdc_bank.load().await; - assert_eq!( - usdc_bank_data.emissions_flags, - EMISSIONS_FLAG_LENDING_ACTIVE - ); + assert_eq!(usdc_bank_data.flags, EMISSIONS_FLAG_LENDING_ACTIVE); assert_eq!(usdc_bank_data.emissions_rate, 1_000_000); @@ -1738,7 +1735,7 @@ async fn emissions_test_2() -> anyhow::Result<()> { let usdc_bank_data = usdc_bank.load().await; - assert_eq!(usdc_bank_data.emissions_flags, EMISSIONS_FLAG_BORROW_ACTIVE); + assert_eq!(usdc_bank_data.flags, EMISSIONS_FLAG_BORROW_ACTIVE); assert_eq!(usdc_bank_data.emissions_rate, 500_000); diff --git a/test-utils/src/marginfi_group.rs b/test-utils/src/marginfi_group.rs index aa47dfc8..0cf5ae10 100644 --- a/test-utils/src/marginfi_group.rs +++ b/test-utils/src/marginfi_group.rs @@ -330,7 +330,7 @@ impl MarginfiGroupFixture { ) -> Result<(), BanksClientError> { let mut accounts = marginfi::accounts::LendingPoolHandleBankruptcy { marginfi_group: self.key, - admin: self.ctx.borrow().payer.pubkey(), + signer: self.ctx.borrow().payer.pubkey(), bank: bank.key, marginfi_account: marginfi_account.key, liquidity_vault: bank.get_vault(BankVaultType::Liquidity).0, diff --git a/tools/llama-snapshot-tool/src/bin/main.rs b/tools/llama-snapshot-tool/src/bin/main.rs index a9a802db..0cf46988 100644 --- a/tools/llama-snapshot-tool/src/bin/main.rs +++ b/tools/llama-snapshot-tool/src/bin/main.rs @@ -172,12 +172,12 @@ impl DefiLammaPoolInfo { / I80F48::from_num(token_price); ( - if bank.get_emissions_flag(EMISSIONS_FLAG_LENDING_ACTIVE) { + if bank.get_flag(EMISSIONS_FLAG_LENDING_ACTIVE) { Some(relative_emissions_value) } else { None }, - if bank.get_emissions_flag(EMISSIONS_FLAG_BORROW_ACTIVE) { + if bank.get_flag(EMISSIONS_FLAG_BORROW_ACTIVE) { Some(relative_emissions_value) } else { None