From f5cfba8d56c815ea1516987684772f6692da41de Mon Sep 17 00:00:00 2001 From: Jon Gurary <91919816+jgur-psyops@users.noreply.github.com> Date: Thu, 19 Dec 2024 11:02:24 -0500 Subject: [PATCH] Freeze settings v2 (#263) * To support arena, we added banks with frozen settings. After some consideration, we have determined that the pool administrator should be able to modify the deposit/borrow limits. Those settings are now configurable even on frozen pools. * Adds a floor to oracle_max_age --- programs/marginfi/fuzz/src/lib.rs | 1 + programs/marginfi/src/constants.rs | 3 ++ programs/marginfi/src/errors.rs | 2 - programs/marginfi/src/events.rs | 9 ++++ .../instructions/marginfi_group/add_pool.rs | 2 +- .../marginfi_group/configure_bank.rs | 52 +++++++++++------- programs/marginfi/src/state/marginfi_group.rs | 14 ++++- test-utils/src/test.rs | 1 + tests/04_configureBank.spec.ts | 54 +++++++++++-------- 9 files changed, 92 insertions(+), 46 deletions(-) diff --git a/programs/marginfi/fuzz/src/lib.rs b/programs/marginfi/fuzz/src/lib.rs index c550d8799..0c34b0b88 100644 --- a/programs/marginfi/fuzz/src/lib.rs +++ b/programs/marginfi/fuzz/src/lib.rs @@ -299,6 +299,7 @@ impl<'state> MarginfiFuzzContext<'state> { } else { marginfi::state::marginfi_group::RiskTier::Isolated }, + oracle_max_age: 100, ..Default::default() }, ) diff --git a/programs/marginfi/src/constants.rs b/programs/marginfi/src/constants.rs index 42a09442b..0310c3003 100644 --- a/programs/marginfi/src/constants.rs +++ b/programs/marginfi/src/constants.rs @@ -45,6 +45,9 @@ pub const INIT_BANK_ORIGINATION_FEE_DEFAULT: u32 = 10000; pub const SECONDS_PER_YEAR: I80F48 = I80F48!(31_536_000); +/// Due to real-world constraints, oracles using an age less than this value are typically too +/// unreliable, and we want to restrict pools from picking an oracle that is effectively unusable +pub const ORACLE_MIN_AGE: u16 = 30; pub const MAX_PYTH_ORACLE_AGE: u64 = 60; pub const MAX_SWB_ORACLE_AGE: u64 = 3 * 60; diff --git a/programs/marginfi/src/errors.rs b/programs/marginfi/src/errors.rs index 21ca13bb3..98ff22aba 100644 --- a/programs/marginfi/src/errors.rs +++ b/programs/marginfi/src/errors.rs @@ -98,8 +98,6 @@ pub enum MarginfiError { T22MintRequired, #[msg("Invalid ATA for global fee account")] // 6048 InvalidFeeAta, - #[msg("Bank settings are frozen and cannot be updated")] // 6049 - BankSettingsFrozen, } impl From for ProgramError { diff --git a/programs/marginfi/src/events.rs b/programs/marginfi/src/events.rs index 35518c6f6..24f6fe381 100644 --- a/programs/marginfi/src/events.rs +++ b/programs/marginfi/src/events.rs @@ -45,6 +45,15 @@ pub struct LendingPoolBankConfigureEvent { pub config: BankConfigOpt, } +#[event] +pub struct LendingPoolBankConfigureFrozenEvent { + pub header: GroupEventHeader, + pub bank: Pubkey, + pub mint: Pubkey, + pub deposit_limit: u64, + pub borrow_limit: u64, +} + #[event] pub struct LendingPoolBankAccrueInterestEvent { pub header: GroupEventHeader, diff --git a/programs/marginfi/src/instructions/marginfi_group/add_pool.rs b/programs/marginfi/src/instructions/marginfi_group/add_pool.rs index 7b1610323..ebd1f49bd 100644 --- a/programs/marginfi/src/instructions/marginfi_group/add_pool.rs +++ b/programs/marginfi/src/instructions/marginfi_group/add_pool.rs @@ -44,7 +44,7 @@ pub fn lending_pool_add_bank( let mut bank = bank_loader.load_init()?; let liquidity_vault_bump = ctx.bumps.liquidity_vault; - let liquidity_vault_authority_bump = ctx.bumps.liquidity_vault_authority; + let liquidity_vault_authority_bump: u8 = ctx.bumps.liquidity_vault_authority; let insurance_vault_bump = ctx.bumps.insurance_vault; let insurance_vault_authority_bump = ctx.bumps.insurance_vault_authority; let fee_vault_bump = ctx.bumps.fee_vault; diff --git a/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs b/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs index 1ac64b734..600241e5e 100644 --- a/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs +++ b/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs @@ -1,5 +1,7 @@ use crate::constants::{EMISSIONS_AUTH_SEED, EMISSIONS_TOKEN_ACCOUNT_SEED, FREEZE_SETTINGS}; -use crate::events::{GroupEventHeader, LendingPoolBankConfigureEvent}; +use crate::events::{ + GroupEventHeader, LendingPoolBankConfigureEvent, LendingPoolBankConfigureFrozenEvent, +}; use crate::prelude::MarginfiError; use crate::{check, math_error, utils}; use crate::{ @@ -17,27 +19,39 @@ pub fn lending_pool_configure_bank( ) -> MarginfiResult { let mut bank = ctx.accounts.bank.load_mut()?; - check!( - !bank.get_flag(FREEZE_SETTINGS), - MarginfiError::BankSettingsFrozen - ); - - bank.configure(&bank_config)?; + // If settings are frozen, you can only update the deposit and borrow limits, everything else is ignored. + if bank.get_flag(FREEZE_SETTINGS) { + bank.configure_unfrozen_fields_only(&bank_config)?; - if bank_config.oracle.is_some() { - bank.config.validate_oracle_setup(ctx.remaining_accounts)?; + emit!(LendingPoolBankConfigureFrozenEvent { + header: GroupEventHeader { + marginfi_group: ctx.accounts.marginfi_group.key(), + signer: Some(*ctx.accounts.admin.key) + }, + bank: ctx.accounts.bank.key(), + mint: bank.mint, + deposit_limit: bank.config.deposit_limit, + borrow_limit: bank.config.borrow_limit, + }); + } else { + // Settings are not frozen, everything updates + bank.configure(&bank_config)?; + + if bank_config.oracle.is_some() { + bank.config.validate_oracle_setup(ctx.remaining_accounts)?; + } + + emit!(LendingPoolBankConfigureEvent { + header: GroupEventHeader { + marginfi_group: ctx.accounts.marginfi_group.key(), + signer: Some(*ctx.accounts.admin.key) + }, + bank: ctx.accounts.bank.key(), + mint: bank.mint, + config: bank_config, + }); } - emit!(LendingPoolBankConfigureEvent { - header: GroupEventHeader { - marginfi_group: ctx.accounts.marginfi_group.key(), - signer: Some(*ctx.accounts.admin.key) - }, - bank: ctx.accounts.bank.key(), - mint: bank.mint, - config: bank_config, - }); - Ok(()) } diff --git a/programs/marginfi/src/state/marginfi_group.rs b/programs/marginfi/src/state/marginfi_group.rs index 955b6ef1c..188a6aed4 100644 --- a/programs/marginfi/src/state/marginfi_group.rs +++ b/programs/marginfi/src/state/marginfi_group.rs @@ -10,7 +10,7 @@ use crate::{ 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_PYTH_ORACLE_AGE, MAX_SWB_ORACLE_AGE, - PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG, PYTH_ID, SECONDS_PER_YEAR, + ORACLE_MIN_AGE, PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG, PYTH_ID, SECONDS_PER_YEAR, TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE, }, debug, math_error, @@ -722,6 +722,14 @@ impl Bank { Ok(()) } + /// Configures just the borrow and deposit limits, ignoring all other values + pub fn configure_unfrozen_fields_only(&mut self, config: &BankConfigOpt) -> MarginfiResult { + set_if_some!(self.config.deposit_limit, config.deposit_limit); + set_if_some!(self.config.borrow_limit, config.borrow_limit); + // weights didn't change so no validation is needed + Ok(()) + } + /// Calculate the interest rate accrual state changes for a given time period /// /// Collected protocol and insurance fees are stored in state. @@ -1414,6 +1422,10 @@ impl BankConfig { } pub fn validate_oracle_setup(&self, ais: &[AccountInfo]) -> MarginfiResult { + check!( + self.oracle_max_age >= ORACLE_MIN_AGE, + MarginfiError::InvalidOracleSetup + ); OraclePriceFeedAdapter::validate_bank_config(self, ais)?; Ok(()) } diff --git a/test-utils/src/test.rs b/test-utils/src/test.rs index 146091865..de3e0003f 100644 --- a/test-utils/src/test.rs +++ b/test-utils/src/test.rs @@ -305,6 +305,7 @@ lazy_static! { protocol_origination_fee: I80F48!(0).into(), ..Default::default() }, + oracle_max_age: 100, ..Default::default() }; pub static ref DEFAULT_USDC_TEST_BANK_CONFIG: BankConfig = BankConfig { diff --git a/tests/04_configureBank.spec.ts b/tests/04_configureBank.spec.ts index a6c01c1ba..da194892e 100644 --- a/tests/04_configureBank.spec.ts +++ b/tests/04_configureBank.spec.ts @@ -1,5 +1,5 @@ import { BN, Program, workspace } from "@coral-xyz/anchor"; -import { Transaction } from "@solana/web3.js"; +import { PublicKey, Transaction } from "@solana/web3.js"; import { configureBank } from "./utils/instructions"; import { Marginfi } from "../target/types/marginfi"; import { bankKeypairUsdc, groupAdmin, marginfiGroup } from "./rootHooks"; @@ -117,28 +117,36 @@ describe("Lending pool configure bank", () => { ); const bank = await program.account.bank.fetch(bankKeypairUsdc.publicKey); assertBNEqual(bank.flags, FREEZE_SETTINGS); + }); + + it("(admin) Update settings after a freeze - only deposit/borrow caps update", async () => { + let configNew = defaultBankConfigOptRaw(); + const newDepositLimit = new BN(2_000_000_000); + const newBorrowLimit = new BN(3_000_000_000); + configNew.depositLimit = newDepositLimit; + configNew.borrowLimit = newBorrowLimit; + + // These will be ignored... + configNew.oracleMaxAge = 42; + configNew.freezeSettings = false; + + await groupAdmin.mrgnProgram!.provider.sendAndConfirm!( + new Transaction().add( + await configureBank(program, { + marginfiGroup: marginfiGroup.publicKey, + admin: groupAdmin.wallet.publicKey, + bank: bankKeypairUsdc.publicKey, + bankConfigOpt: configNew, + }) + ) + ); + const bank = await program.account.bank.fetch(bankKeypairUsdc.publicKey); + const config = bank.config; + assertBNEqual(config.depositLimit, newDepositLimit); + assertBNEqual(config.borrowLimit, newBorrowLimit); - // Attempting to config again should fail... - let failed = false; - try { - await groupAdmin.mrgnProgram!.provider.sendAndConfirm!( - new Transaction().add( - await configureBank(program, { - marginfiGroup: marginfiGroup.publicKey, - admin: groupAdmin.wallet.publicKey, - bank: bankKeypairUsdc.publicKey, - bankConfigOpt: defaultBankConfigOptRaw(), - }) - ) - ); - } catch (err) { - assert.ok( - err.logs.some((log: string) => - log.includes("Error Code: BankSettingsFrozen") - ) - ); - failed = true; - } - assert.ok(failed, "Transaction succeeded when it should have failed"); + // Ignored fields didn't change.. + assert.equal(config.oracleMaxAge, 100); + assertBNEqual(bank.flags, FREEZE_SETTINGS); // still frozen }); });