diff --git a/clients/rust/marginfi-cli/src/entrypoint.rs b/clients/rust/marginfi-cli/src/entrypoint.rs index 3b27abd7d..ada7e7ae4 100644 --- a/clients/rust/marginfi-cli/src/entrypoint.rs +++ b/clients/rust/marginfi-cli/src/entrypoint.rs @@ -12,11 +12,10 @@ use marginfi::state::marginfi_account::TRANSFER_AUTHORITY_ALLOWED_FLAG; use marginfi::{ prelude::*, state::{ + bank::{Bank, BankConfig, BankConfigOpt}, + interest_rate::{InterestRateConfig, InterestRateConfigOpt}, marginfi_account::{Balance, LendingAccount, MarginfiAccount, FLASHLOAN_ENABLED_FLAG}, - marginfi_group::{ - Bank, BankConfig, BankConfigOpt, BankOperationalState, InterestRateConfig, - InterestRateConfigOpt, OracleConfig, RiskTier, WrappedI80F48, - }, + marginfi_group::{BankOperationalState, OracleConfig, RiskTier, WrappedI80F48}, price::OracleSetup, }, }; @@ -753,7 +752,7 @@ fn bank(subcmd: BankCommand, global_options: &GlobalOptions) -> Result<()> { } => { let bank = config .mfi_program - .account::(bank_pk) + .account::(bank_pk) .unwrap(); processor::bank_configure( config, @@ -859,10 +858,7 @@ fn inspect_padding() -> Result<()> { println!("MarginfiGroup: {}", MarginfiGroup::type_layout()); println!("GroupConfig: {}", GroupConfig::type_layout()); println!("InterestRateConfig: {}", InterestRateConfig::type_layout()); - println!( - "Bank: {}", - marginfi::state::marginfi_group::Bank::type_layout() - ); + println!("Bank: {}", marginfi::state::bank::Bank::type_layout()); println!("BankConfig: {}", BankConfig::type_layout()); println!("OracleConfig: {}", OracleConfig::type_layout()); println!("BankConfigOpt: {}", BankConfigOpt::type_layout()); @@ -881,10 +877,7 @@ fn inspect_size() -> Result<()> { println!("MarginfiGroup: {}", size_of::()); println!("GroupConfig: {}", size_of::()); println!("InterestRateConfig: {}", size_of::()); - println!( - "Bank: {}", - size_of::() - ); + println!("Bank: {}", size_of::()); println!("BankConfig: {}", size_of::()); println!("OracleConfig: {}", size_of::()); println!("BankConfigOpt: {}", size_of::()); diff --git a/clients/rust/marginfi-cli/src/processor/admin.rs b/clients/rust/marginfi-cli/src/processor/admin.rs index 2552761ac..cad5ff6aa 100644 --- a/clients/rust/marginfi-cli/src/processor/admin.rs +++ b/clients/rust/marginfi-cli/src/processor/admin.rs @@ -7,7 +7,7 @@ use anchor_spl::associated_token; use anyhow::Result; use marginfi::{ bank_authority_seed, - state::marginfi_group::{Bank, BankVaultType}, + state::{bank::Bank, marginfi_group::BankVaultType}, }; use solana_sdk::{ instruction::Instruction, message::Message, pubkey::Pubkey, transaction::Transaction, diff --git a/clients/rust/marginfi-cli/src/processor/group.rs b/clients/rust/marginfi-cli/src/processor/group.rs index 1c6fd1e5a..3a383c07e 100644 --- a/clients/rust/marginfi-cli/src/processor/group.rs +++ b/clients/rust/marginfi-cli/src/processor/group.rs @@ -1,7 +1,7 @@ use crate::{config::Config, profile::Profile, utils}; use anyhow::Result; use log::{debug, info, warn}; -use marginfi::state::marginfi_group::Bank; +use marginfi::state::bank::Bank; use solana_address_lookup_table_program::{ instruction::{create_lookup_table, extend_lookup_table}, state::AddressLookupTable, diff --git a/clients/rust/marginfi-cli/src/processor/mod.rs b/clients/rust/marginfi-cli/src/processor/mod.rs index 47ec2a3ef..a615bd52a 100644 --- a/clients/rust/marginfi-cli/src/processor/mod.rs +++ b/clients/rust/marginfi-cli/src/processor/mod.rs @@ -29,11 +29,10 @@ use { }, prelude::*, state::{ + bank::{Bank, BankConfig, BankConfigOpt}, + interest_rate::InterestRateConfig, marginfi_account::{BankAccountWrapper, MarginfiAccount}, - marginfi_group::{ - Bank, BankConfig, BankConfigOpt, BankOperationalState, BankVaultType, - InterestRateConfig, WrappedI80F48, - }, + marginfi_group::{BankOperationalState, BankVaultType, WrappedI80F48}, price::{OraclePriceFeedAdapter, OracleSetup, PriceAdapter, PythPushOraclePriceFeed}, }, utils::NumTraitsWithTolerance, diff --git a/clients/rust/marginfi-cli/src/utils.rs b/clients/rust/marginfi-cli/src/utils.rs index f936081c1..72f21436b 100644 --- a/clients/rust/marginfi-cli/src/utils.rs +++ b/clients/rust/marginfi-cli/src/utils.rs @@ -11,8 +11,9 @@ use { PYTH_PUSH_PYTH_SPONSORED_SHARD_ID, }, state::{ + bank::{Bank, BankConfig}, marginfi_account::MarginfiAccount, - marginfi_group::{Bank, BankConfig, BankVaultType}, + marginfi_group::BankVaultType, price::PythPushOraclePriceFeed, }, }, diff --git a/programs/liquidity-incentive-program/src/instructions/create_campaign.rs b/programs/liquidity-incentive-program/src/instructions/create_campaign.rs index e08ffb7f7..0c13e5196 100644 --- a/programs/liquidity-incentive-program/src/instructions/create_campaign.rs +++ b/programs/liquidity-incentive-program/src/instructions/create_campaign.rs @@ -4,7 +4,7 @@ use crate::{ }; use anchor_lang::prelude::*; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; -use marginfi::state::marginfi_group::Bank; +use marginfi::state::bank::Bank; use std::mem::size_of; pub fn process<'info>( diff --git a/programs/liquidity-incentive-program/src/instructions/create_deposit.rs b/programs/liquidity-incentive-program/src/instructions/create_deposit.rs index c7ffa93e5..0b6f7a589 100644 --- a/programs/liquidity-incentive-program/src/instructions/create_deposit.rs +++ b/programs/liquidity-incentive-program/src/instructions/create_deposit.rs @@ -8,7 +8,7 @@ use anchor_spl::{ token_2022::{close_account, CloseAccount}, token_interface::{Mint, TokenAccount, TokenInterface}, }; -use marginfi::{program::Marginfi, state::marginfi_group::Bank}; +use marginfi::{program::Marginfi, state::bank::Bank}; use std::mem::size_of; /// Creates a new deposit in an active liquidity incentive campaign (LIP). diff --git a/programs/liquidity-incentive-program/src/instructions/end_deposit.rs b/programs/liquidity-incentive-program/src/instructions/end_deposit.rs index 2860e82a1..df27d0d78 100644 --- a/programs/liquidity-incentive-program/src/instructions/end_deposit.rs +++ b/programs/liquidity-incentive-program/src/instructions/end_deposit.rs @@ -4,7 +4,7 @@ use anchor_spl::{ token_interface::{Mint, TokenAccount, TokenInterface}, }; use fixed::types::I80F48; -use marginfi::{program::Marginfi, state::marginfi_group::Bank}; +use marginfi::{program::Marginfi, state::bank::Bank}; use crate::{ constants::{ diff --git a/programs/marginfi/fuzz/fuzz_targets/lend.rs b/programs/marginfi/fuzz/fuzz_targets/lend.rs index a00182ac1..5c5f77638 100644 --- a/programs/marginfi/fuzz/fuzz_targets/lend.rs +++ b/programs/marginfi/fuzz/fuzz_targets/lend.rs @@ -6,7 +6,7 @@ use arbitrary::Arbitrary; use fixed::types::I80F48; use lazy_static::lazy_static; use libfuzzer_sys::fuzz_target; -use marginfi::{assert_eq_with_tolerance, prelude::MarginfiGroup, state::marginfi_group::Bank}; +use marginfi::{assert_eq_with_tolerance, prelude::MarginfiGroup, state::bank::Bank}; use marginfi_fuzz::{ account_state::AccountsState, arbitrary_helpers::*, metrics::Metrics, MarginfiFuzzContext, }; diff --git a/programs/marginfi/fuzz/src/lib.rs b/programs/marginfi/fuzz/src/lib.rs index 0c34b0b88..ef64dea25 100644 --- a/programs/marginfi/fuzz/src/lib.rs +++ b/programs/marginfi/fuzz/src/lib.rs @@ -24,8 +24,10 @@ use marginfi::{ instructions::LendingPoolAddBankBumps, prelude::MarginfiGroup, state::{ + bank::{Bank, BankConfig}, + interest_rate::InterestRateConfig, marginfi_account::MarginfiAccount, - marginfi_group::{Bank, BankConfig, BankVaultType, InterestRateConfig}, + marginfi_group::BankVaultType, }, }; use metrics::{MetricAction, Metrics}; diff --git a/programs/marginfi/src/events.rs b/programs/marginfi/src/events.rs index 24f6fe381..3ef76e21b 100644 --- a/programs/marginfi/src/events.rs +++ b/programs/marginfi/src/events.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, state::marginfi_group::BankConfigOpt}; +use crate::{prelude::*, state::bank::BankConfigOpt}; use anchor_lang::prelude::*; // Event headers diff --git a/programs/marginfi/src/instructions/marginfi_account/borrow.rs b/programs/marginfi/src/instructions/marginfi_account/borrow.rs index 82a44615a..e701ecb12 100644 --- a/programs/marginfi/src/instructions/marginfi_account/borrow.rs +++ b/programs/marginfi/src/instructions/marginfi_account/borrow.rs @@ -5,8 +5,10 @@ use crate::{ math_error, prelude::{MarginfiError, MarginfiGroup, MarginfiResult}, state::{ - marginfi_account::{BankAccountWrapper, MarginfiAccount, RiskEngine, DISABLED_FLAG}, - marginfi_group::{Bank, BankVaultType}, + bank::Bank, + marginfi_account::{BankAccountWrapper, MarginfiAccount, DISABLED_FLAG}, + marginfi_group::BankVaultType, + risk_engine::RiskEngine, }, utils::{self, validate_asset_tags}, }; diff --git a/programs/marginfi/src/instructions/marginfi_account/close_balance.rs b/programs/marginfi/src/instructions/marginfi_account/close_balance.rs index 992718d9b..d4bb54bd2 100644 --- a/programs/marginfi/src/instructions/marginfi_account/close_balance.rs +++ b/programs/marginfi/src/instructions/marginfi_account/close_balance.rs @@ -4,8 +4,8 @@ use crate::{ check, prelude::*, state::{ + bank::Bank, marginfi_account::{BankAccountWrapper, MarginfiAccount, DISABLED_FLAG}, - marginfi_group::Bank, }, }; diff --git a/programs/marginfi/src/instructions/marginfi_account/deposit.rs b/programs/marginfi/src/instructions/marginfi_account/deposit.rs index 6285eb623..adc05b6a1 100644 --- a/programs/marginfi/src/instructions/marginfi_account/deposit.rs +++ b/programs/marginfi/src/instructions/marginfi_account/deposit.rs @@ -4,8 +4,8 @@ use crate::{ events::{AccountEventHeader, LendingAccountDepositEvent}, prelude::*, state::{ + bank::Bank, marginfi_account::{BankAccountWrapper, MarginfiAccount, DISABLED_FLAG}, - marginfi_group::Bank, }, utils::{self, validate_asset_tags}, }; diff --git a/programs/marginfi/src/instructions/marginfi_account/emissions.rs b/programs/marginfi/src/instructions/marginfi_account/emissions.rs index a83c071be..e547640bb 100644 --- a/programs/marginfi/src/instructions/marginfi_account/emissions.rs +++ b/programs/marginfi/src/instructions/marginfi_account/emissions.rs @@ -9,8 +9,9 @@ use crate::{ debug, prelude::{MarginfiError, MarginfiResult}, state::{ + bank::Bank, marginfi_account::{BankAccountWrapper, MarginfiAccount, DISABLED_FLAG}, - marginfi_group::{Bank, MarginfiGroup}, + marginfi_group::MarginfiGroup, }, }; diff --git a/programs/marginfi/src/instructions/marginfi_account/flashloan.rs b/programs/marginfi/src/instructions/marginfi_account/flashloan.rs index b5deb83ce..bab475387 100644 --- a/programs/marginfi/src/instructions/marginfi_account/flashloan.rs +++ b/programs/marginfi/src/instructions/marginfi_account/flashloan.rs @@ -7,7 +7,10 @@ use solana_program::{ use crate::{ check, prelude::*, - state::marginfi_account::{MarginfiAccount, RiskEngine, DISABLED_FLAG, IN_FLASHLOAN_FLAG}, + state::{ + marginfi_account::{MarginfiAccount, DISABLED_FLAG}, + risk_engine::{RiskEngine, IN_FLASHLOAN_FLAG}, + }, }; pub fn lending_account_start_flashloan( diff --git a/programs/marginfi/src/instructions/marginfi_account/liquidate.rs b/programs/marginfi/src/instructions/marginfi_account/liquidate.rs index 8355a529c..9e993f4cb 100644 --- a/programs/marginfi/src/instructions/marginfi_account/liquidate.rs +++ b/programs/marginfi/src/instructions/marginfi_account/liquidate.rs @@ -2,9 +2,13 @@ use crate::constants::{ INSURANCE_VAULT_SEED, LIQUIDATION_INSURANCE_FEE, LIQUIDATION_LIQUIDATOR_FEE, }; use crate::events::{AccountEventHeader, LendingAccountLiquidateEvent, LiquidationBalances}; -use crate::state::marginfi_account::{calc_amount, calc_value, RiskEngine}; -use crate::state::marginfi_group::{Bank, BankVaultType}; use crate::state::price::{OraclePriceFeedAdapter, OraclePriceType, PriceAdapter, PriceBias}; +use crate::state::{ + bank::Bank, + marginfi_account::{calc_amount, calc_value}, + marginfi_group::BankVaultType, + risk_engine::RiskEngine, +}; use crate::utils::{validate_asset_tags, validate_bank_asset_tags}; use crate::{ bank_signer, diff --git a/programs/marginfi/src/instructions/marginfi_account/repay.rs b/programs/marginfi/src/instructions/marginfi_account/repay.rs index 3abe9a158..d1f269f30 100644 --- a/programs/marginfi/src/instructions/marginfi_account/repay.rs +++ b/programs/marginfi/src/instructions/marginfi_account/repay.rs @@ -4,8 +4,8 @@ use crate::{ events::{AccountEventHeader, LendingAccountRepayEvent}, prelude::{MarginfiError, MarginfiGroup, MarginfiResult}, state::{ + bank::Bank, marginfi_account::{BankAccountWrapper, MarginfiAccount, DISABLED_FLAG}, - marginfi_group::Bank, }, utils, }; diff --git a/programs/marginfi/src/instructions/marginfi_account/withdraw.rs b/programs/marginfi/src/instructions/marginfi_account/withdraw.rs index db42c550c..23d42a935 100644 --- a/programs/marginfi/src/instructions/marginfi_account/withdraw.rs +++ b/programs/marginfi/src/instructions/marginfi_account/withdraw.rs @@ -4,8 +4,10 @@ use crate::{ events::{AccountEventHeader, LendingAccountWithdrawEvent}, prelude::*, state::{ - marginfi_account::{BankAccountWrapper, MarginfiAccount, RiskEngine, DISABLED_FLAG}, - marginfi_group::{Bank, BankVaultType}, + bank::Bank, + marginfi_account::{BankAccountWrapper, MarginfiAccount, DISABLED_FLAG}, + marginfi_group::BankVaultType, + risk_engine::RiskEngine, }, utils, }; diff --git a/programs/marginfi/src/instructions/marginfi_group/accrue_bank_interest.rs b/programs/marginfi/src/instructions/marginfi_group/accrue_bank_interest.rs index 838681c4e..1bf76feed 100644 --- a/programs/marginfi/src/instructions/marginfi_group/accrue_bank_interest.rs +++ b/programs/marginfi/src/instructions/marginfi_group/accrue_bank_interest.rs @@ -1,5 +1,5 @@ use crate::{ - state::marginfi_group::{Bank, MarginfiGroup}, + state::{bank::Bank, marginfi_group::MarginfiGroup}, MarginfiResult, }; use anchor_lang::prelude::*; diff --git a/programs/marginfi/src/instructions/marginfi_group/add_pool.rs b/programs/marginfi/src/instructions/marginfi_group/add_pool.rs index 31e50c71a..f2a77fa0a 100644 --- a/programs/marginfi/src/instructions/marginfi_group/add_pool.rs +++ b/programs/marginfi/src/instructions/marginfi_group/add_pool.rs @@ -7,8 +7,9 @@ use crate::{ }, events::{GroupEventHeader, LendingPoolBankCreateEvent}, state::{ + bank::{Bank, BankConfig, BankConfigCompact}, fee_state::FeeState, - marginfi_group::{Bank, BankConfig, BankConfigCompact, MarginfiGroup}, + marginfi_group::MarginfiGroup, }, MarginfiError, MarginfiResult, }; diff --git a/programs/marginfi/src/instructions/marginfi_group/add_pool_permissionless.rs b/programs/marginfi/src/instructions/marginfi_group/add_pool_permissionless.rs index 51c1b9cce..344b02380 100644 --- a/programs/marginfi/src/instructions/marginfi_group/add_pool_permissionless.rs +++ b/programs/marginfi/src/instructions/marginfi_group/add_pool_permissionless.rs @@ -9,9 +9,9 @@ use crate::{ }, events::{GroupEventHeader, LendingPoolBankCreateEvent}, state::{ - marginfi_group::{ - Bank, BankConfigCompact, BankOperationalState, InterestRateConfig, MarginfiGroup, - }, + bank::{Bank, BankConfigCompact}, + interest_rate::InterestRateConfig, + marginfi_group::{BankOperationalState, MarginfiGroup}, price::OracleSetup, staked_settings::StakedSettings, }, diff --git a/programs/marginfi/src/instructions/marginfi_group/add_pool_with_seed.rs b/programs/marginfi/src/instructions/marginfi_group/add_pool_with_seed.rs index 3cb3d8f6a..626bc9685 100644 --- a/programs/marginfi/src/instructions/marginfi_group/add_pool_with_seed.rs +++ b/programs/marginfi/src/instructions/marginfi_group/add_pool_with_seed.rs @@ -7,8 +7,9 @@ use crate::{ }, events::{GroupEventHeader, LendingPoolBankCreateEvent}, state::{ + bank::{Bank, BankConfig, BankConfigCompact}, fee_state::FeeState, - marginfi_group::{Bank, BankConfig, BankConfigCompact, MarginfiGroup}, + marginfi_group::MarginfiGroup, }, MarginfiError, MarginfiResult, }; diff --git a/programs/marginfi/src/instructions/marginfi_group/collect_bank_fees.rs b/programs/marginfi/src/instructions/marginfi_group/collect_bank_fees.rs index dbd366fbb..21a2507d7 100644 --- a/programs/marginfi/src/instructions/marginfi_group/collect_bank_fees.rs +++ b/programs/marginfi/src/instructions/marginfi_group/collect_bank_fees.rs @@ -7,7 +7,10 @@ use crate::{ FEE_VAULT_SEED, INSURANCE_VAULT_SEED, LIQUIDITY_VAULT_AUTHORITY_SEED, LIQUIDITY_VAULT_SEED, }, math_error, - state::marginfi_group::{Bank, BankVaultType, MarginfiGroup}, + state::{ + bank::Bank, + marginfi_group::{BankVaultType, MarginfiGroup}, + }, MarginfiResult, }; use crate::{check, utils, MarginfiError}; diff --git a/programs/marginfi/src/instructions/marginfi_group/configure.rs b/programs/marginfi/src/instructions/marginfi_group/configure.rs index 97099f1df..4106079c0 100644 --- a/programs/marginfi/src/instructions/marginfi_group/configure.rs +++ b/programs/marginfi/src/instructions/marginfi_group/configure.rs @@ -107,8 +107,11 @@ pub struct UnsetAccountFlag<'info> { #[cfg(test)] mod tests { - use crate::state::marginfi_account::{ - DISABLED_FLAG, FLASHLOAN_ENABLED_FLAG, IN_FLASHLOAN_FLAG, TRANSFER_AUTHORITY_ALLOWED_FLAG, + use crate::state::{ + marginfi_account::{ + DISABLED_FLAG, FLASHLOAN_ENABLED_FLAG, TRANSFER_AUTHORITY_ALLOWED_FLAG, + }, + risk_engine::IN_FLASHLOAN_FLAG, }; #[test] diff --git a/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs b/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs index dd6ad5492..39850fd78 100644 --- a/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs +++ b/programs/marginfi/src/instructions/marginfi_group/configure_bank.rs @@ -5,7 +5,10 @@ use crate::events::{ use crate::prelude::MarginfiError; use crate::{check, math_error, utils}; use crate::{ - state::marginfi_group::{Bank, BankConfigOpt, MarginfiGroup}, + state::{ + bank::{Bank, BankConfigOpt}, + marginfi_group::MarginfiGroup, + }, MarginfiResult, }; use anchor_lang::prelude::*; diff --git a/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs b/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs index 5a94e42e5..6bdb6dda2 100644 --- a/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs +++ b/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs @@ -9,8 +9,10 @@ use crate::{ math_error, prelude::MarginfiError, state::{ - marginfi_account::{BankAccountWrapper, MarginfiAccount, RiskEngine, DISABLED_FLAG}, - marginfi_group::{Bank, BankVaultType, MarginfiGroup}, + bank::Bank, + marginfi_account::{BankAccountWrapper, MarginfiAccount, DISABLED_FLAG}, + marginfi_group::{BankVaultType, MarginfiGroup}, + risk_engine::RiskEngine, }, utils, MarginfiResult, }; diff --git a/programs/marginfi/src/instructions/marginfi_group/propagate_staked_settings.rs b/programs/marginfi/src/instructions/marginfi_group/propagate_staked_settings.rs index d9515bef1..1c6376cdf 100644 --- a/programs/marginfi/src/instructions/marginfi_group/propagate_staked_settings.rs +++ b/programs/marginfi/src/instructions/marginfi_group/propagate_staked_settings.rs @@ -1,7 +1,6 @@ use crate::constants::ASSET_TAG_STAKED; // Permissionless ix to propagate a group's staked collateral settings to any bank in that group -use crate::state::marginfi_group::Bank; -use crate::state::staked_settings::StakedSettings; +use crate::state::{bank::Bank, staked_settings::StakedSettings}; use crate::MarginfiGroup; use anchor_lang::prelude::*; diff --git a/programs/marginfi/src/lib.rs b/programs/marginfi/src/lib.rs index 7ba7c8023..953da38e1 100644 --- a/programs/marginfi/src/lib.rs +++ b/programs/marginfi/src/lib.rs @@ -10,8 +10,10 @@ pub mod utils; use anchor_lang::prelude::*; use instructions::*; use prelude::*; -use state::marginfi_group::WrappedI80F48; -use state::marginfi_group::{BankConfigCompact, BankConfigOpt}; +use state::{ + bank::{BankConfigCompact, BankConfigOpt}, + marginfi_group::WrappedI80F48, +}; cfg_if::cfg_if! { if #[cfg(feature = "mainnet-beta")] { diff --git a/programs/marginfi/src/state/bank.rs b/programs/marginfi/src/state/bank.rs new file mode 100644 index 000000000..8df115644 --- /dev/null +++ b/programs/marginfi/src/state/bank.rs @@ -0,0 +1,962 @@ +#[cfg(not(feature = "client"))] +use crate::events::{GroupEventHeader, LendingPoolBankAccrueInterestEvent}; +use crate::{ + assert_struct_align, assert_struct_size, check, + constants::{ + ASSET_TAG_DEFAULT, EMISSION_FLAGS, FREEZE_SETTINGS, GROUP_FLAGS, MAX_ORACLE_KEYS, + MAX_PYTH_ORACLE_AGE, MAX_SWB_ORACLE_AGE, ORACLE_MIN_AGE, + PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG, TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE, + }, + debug, math_error, + prelude::MarginfiError, + set_if_some, + state::{ + interest_rate::{calc_interest_rate_accrual_state_changes, InterestRateStateChanges}, + interest_rate::{InterestRateConfig, InterestRateConfigCompact, InterestRateConfigOpt}, + marginfi_account::{calc_value, BalanceSide, RequirementType}, + marginfi_group::{ + BankOperationalState, MarginfiGroup, OracleConfig, RiskTier, WrappedI80F48, + }, + price::{OraclePriceFeedAdapter, OracleSetup}, + }, + MarginfiResult, +}; + +use anchor_lang::prelude::*; +use anchor_spl::token_interface::*; +use fixed::types::I80F48; +use pyth_solana_receiver_sdk::price_update::FeedId; +use std::{fmt::Debug, ops::Not}; + +#[cfg(any(feature = "test", feature = "client"))] +use type_layout::TypeLayout; + +assert_struct_size!(Bank, 1856); +assert_struct_align!(Bank, 8); +#[account(zero_copy(unsafe))] +#[repr(C)] +#[cfg_attr( + any(feature = "test", feature = "client"), + derive(Debug, PartialEq, Eq, TypeLayout) +)] +#[derive(Default)] +pub struct Bank { + pub mint: Pubkey, + pub mint_decimals: u8, + + pub group: Pubkey, + + // Note: The padding is here, not after mint_decimals. Pubkey has alignment 1, so those 32 + // bytes can cross the alignment 8 threshold, but WrappedI80F48 has alignment 8 and cannot + pub _pad0: [u8; 7], // 1x u8 + 7 = 8 + + pub asset_share_value: WrappedI80F48, + pub liability_share_value: WrappedI80F48, + + pub liquidity_vault: Pubkey, + pub liquidity_vault_bump: u8, + pub liquidity_vault_authority_bump: u8, + + pub insurance_vault: Pubkey, + pub insurance_vault_bump: u8, + pub insurance_vault_authority_bump: u8, + + pub _pad1: [u8; 4], // 4x u8 + 4 = 8 + + /// Fees collected and pending withdraw for the `insurance_vault` + pub collected_insurance_fees_outstanding: WrappedI80F48, + + pub fee_vault: Pubkey, + pub fee_vault_bump: u8, + pub fee_vault_authority_bump: u8, + + pub _pad2: [u8; 6], // 2x u8 + 6 = 8 + + /// Fees collected and pending withdraw for the `fee_vault` + pub collected_group_fees_outstanding: WrappedI80F48, + + pub total_liability_shares: WrappedI80F48, + pub total_asset_shares: WrappedI80F48, + + pub last_update: i64, + + pub config: BankConfig, + + /// Bank Config Flags + /// + /// - EMISSIONS_FLAG_BORROW_ACTIVE: 1 + /// - EMISSIONS_FLAG_LENDING_ACTIVE: 2 + /// - PERMISSIONLESS_BAD_DEBT_SETTLEMENT: 4 + /// + 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, + pub emissions_remaining: WrappedI80F48, + pub emissions_mint: Pubkey, + + /// Fees collected and pending withdraw for the `FeeState.global_fee_wallet`'s cannonical ATA for `mint` + pub collected_program_fees_outstanding: WrappedI80F48, + + pub _padding_0: [[u64; 2]; 27], + pub _padding_1: [[u64; 2]; 32], // 16 * 2 * 32 = 1024B +} + +impl Bank { + #[allow(clippy::too_many_arguments)] + pub fn new( + marginfi_group_pk: Pubkey, + config: BankConfig, + mint: Pubkey, + mint_decimals: u8, + liquidity_vault: Pubkey, + insurance_vault: Pubkey, + fee_vault: Pubkey, + current_timestamp: i64, + liquidity_vault_bump: u8, + liquidity_vault_authority_bump: u8, + insurance_vault_bump: u8, + insurance_vault_authority_bump: u8, + fee_vault_bump: u8, + fee_vault_authority_bump: u8, + ) -> Bank { + Bank { + mint, + mint_decimals, + group: marginfi_group_pk, + asset_share_value: I80F48::ONE.into(), + liability_share_value: I80F48::ONE.into(), + liquidity_vault, + liquidity_vault_bump, + liquidity_vault_authority_bump, + insurance_vault, + insurance_vault_bump, + insurance_vault_authority_bump, + collected_insurance_fees_outstanding: I80F48::ZERO.into(), + fee_vault, + fee_vault_bump, + fee_vault_authority_bump, + collected_group_fees_outstanding: I80F48::ZERO.into(), + total_liability_shares: I80F48::ZERO.into(), + total_asset_shares: I80F48::ZERO.into(), + last_update: current_timestamp, + config, + flags: 0, + emissions_rate: 0, + emissions_remaining: I80F48::ZERO.into(), + emissions_mint: Pubkey::default(), + collected_program_fees_outstanding: I80F48::ZERO.into(), + ..Default::default() + } + } + + pub fn get_liability_amount(&self, shares: I80F48) -> MarginfiResult { + Ok(shares + .checked_mul(self.liability_share_value.into()) + .ok_or_else(math_error!())?) + } + + pub fn get_asset_amount(&self, shares: I80F48) -> MarginfiResult { + Ok(shares + .checked_mul(self.asset_share_value.into()) + .ok_or_else(math_error!())?) + } + + pub fn get_liability_shares(&self, value: I80F48) -> MarginfiResult { + Ok(value + .checked_div(self.liability_share_value.into()) + .ok_or_else(math_error!())?) + } + + pub fn get_asset_shares(&self, value: I80F48) -> MarginfiResult { + Ok(value + .checked_div(self.asset_share_value.into()) + .ok_or_else(math_error!())?) + } + + pub fn change_asset_shares( + &mut self, + shares: I80F48, + bypass_deposit_limit: bool, + ) -> MarginfiResult { + let total_asset_shares: I80F48 = self.total_asset_shares.into(); + self.total_asset_shares = total_asset_shares + .checked_add(shares) + .ok_or_else(math_error!())? + .into(); + + if shares.is_positive() && self.config.is_deposit_limit_active() && !bypass_deposit_limit { + let total_deposits_amount = self.get_asset_amount(self.total_asset_shares.into())?; + let deposit_limit = I80F48::from_num(self.config.deposit_limit); + + check!( + total_deposits_amount < deposit_limit, + crate::prelude::MarginfiError::BankAssetCapacityExceeded + ) + } + + Ok(()) + } + + pub fn maybe_get_asset_weight_init_discount( + &self, + price: I80F48, + ) -> MarginfiResult> { + if self.config.usd_init_limit_active() { + let bank_total_assets_value = calc_value( + self.get_asset_amount(self.total_asset_shares.into())?, + price, + self.mint_decimals, + None, + )?; + + let total_asset_value_init_limit = + I80F48::from_num(self.config.total_asset_value_init_limit); + + #[cfg(target_os = "solana")] + debug!( + "Init limit active, limit: {}, total_assets: {}", + total_asset_value_init_limit, bank_total_assets_value + ); + + if bank_total_assets_value > total_asset_value_init_limit { + let discount = total_asset_value_init_limit + .checked_div(bank_total_assets_value) + .ok_or_else(math_error!())?; + + #[cfg(target_os = "solana")] + debug!( + "Discounting assets by {:.2} because of total deposits {} over {} usd cap", + discount, bank_total_assets_value, total_asset_value_init_limit + ); + + Ok(Some(discount)) + } else { + Ok(None) + } + } else { + Ok(None) + } + } + + pub fn change_liability_shares( + &mut self, + shares: I80F48, + bypass_borrow_limit: bool, + ) -> MarginfiResult { + let total_liability_shares: I80F48 = self.total_liability_shares.into(); + self.total_liability_shares = total_liability_shares + .checked_add(shares) + .ok_or_else(math_error!())? + .into(); + + if bypass_borrow_limit.not() && shares.is_positive() && self.config.is_borrow_limit_active() + { + let total_liability_amount = + self.get_liability_amount(self.total_liability_shares.into())?; + let borrow_limit = I80F48::from_num(self.config.borrow_limit); + + check!( + total_liability_amount < borrow_limit, + crate::prelude::MarginfiError::BankLiabilityCapacityExceeded + ) + } + + Ok(()) + } + + pub fn check_utilization_ratio(&self) -> MarginfiResult { + let total_assets = self.get_asset_amount(self.total_asset_shares.into())?; + let total_liabilities = self.get_liability_amount(self.total_liability_shares.into())?; + + check!( + total_assets >= total_liabilities, + crate::prelude::MarginfiError::IllegalUtilizationRatio + ); + + Ok(()) + } + + pub fn configure(&mut self, config: &BankConfigOpt) -> MarginfiResult { + set_if_some!(self.config.asset_weight_init, config.asset_weight_init); + set_if_some!(self.config.asset_weight_maint, config.asset_weight_maint); + set_if_some!( + self.config.liability_weight_init, + config.liability_weight_init + ); + set_if_some!( + self.config.liability_weight_maint, + config.liability_weight_maint + ); + set_if_some!(self.config.deposit_limit, config.deposit_limit); + + set_if_some!(self.config.borrow_limit, config.borrow_limit); + + set_if_some!(self.config.operational_state, config.operational_state); + + set_if_some!(self.config.oracle_setup, config.oracle.map(|o| o.setup)); + + set_if_some!(self.config.oracle_keys, config.oracle.map(|o| o.keys)); + + if let Some(ir_config) = &config.interest_rate_config { + self.config.interest_rate_config.update(ir_config); + } + + set_if_some!(self.config.risk_tier, config.risk_tier); + + set_if_some!(self.config.asset_tag, config.asset_tag); + + set_if_some!( + self.config.total_asset_value_init_limit, + config.total_asset_value_init_limit + ); + + 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); + } + + if let Some(flag) = config.freeze_settings { + self.update_flag(flag, FREEZE_SETTINGS); + } + + self.config.validate()?; + + 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. + /// A separate instruction is required to withdraw these fees. + pub fn accrue_interest( + &mut self, + current_timestamp: i64, + group: &MarginfiGroup, + #[cfg(not(feature = "client"))] bank: Pubkey, + ) -> MarginfiResult<()> { + #[cfg(all(not(feature = "client"), feature = "debug"))] + solana_program::log::sol_log_compute_units(); + + let time_delta: u64 = (current_timestamp - self.last_update).try_into().unwrap(); + if time_delta == 0 { + return Ok(()); + } + + let total_assets = self.get_asset_amount(self.total_asset_shares.into())?; + let total_liabilities = self.get_liability_amount(self.total_liability_shares.into())?; + + self.last_update = current_timestamp; + + if (total_assets == I80F48::ZERO) || (total_liabilities == I80F48::ZERO) { + #[cfg(not(feature = "client"))] + emit!(LendingPoolBankAccrueInterestEvent { + header: GroupEventHeader { + marginfi_group: self.group, + signer: None + }, + bank, + mint: self.mint, + delta: time_delta, + fees_collected: 0., + insurance_collected: 0., + }); + + return Ok(()); + } + let ir_calc = self + .config + .interest_rate_config + .create_interest_rate_calculator(group); + + let InterestRateStateChanges { + new_asset_share_value: asset_share_value, + new_liability_share_value: liability_share_value, + insurance_fees_collected, + group_fees_collected, + protocol_fees_collected, + } = calc_interest_rate_accrual_state_changes( + time_delta, + total_assets, + total_liabilities, + &ir_calc, + self.asset_share_value.into(), + self.liability_share_value.into(), + ) + .ok_or_else(math_error!())?; + + debug!("deposit share value: {}\nliability share value: {}\nfees collected: {}\ninsurance collected: {}", + asset_share_value, liability_share_value, group_fees_collected, insurance_fees_collected); + + self.asset_share_value = asset_share_value.into(); + self.liability_share_value = liability_share_value.into(); + + if group_fees_collected > I80F48::ZERO { + self.collected_group_fees_outstanding = { + group_fees_collected + .checked_add(self.collected_group_fees_outstanding.into()) + .ok_or_else(math_error!())? + .into() + }; + } + + if insurance_fees_collected > I80F48::ZERO { + self.collected_insurance_fees_outstanding = { + insurance_fees_collected + .checked_add(self.collected_insurance_fees_outstanding.into()) + .ok_or_else(math_error!())? + .into() + }; + } + if protocol_fees_collected > I80F48::ZERO { + self.collected_program_fees_outstanding = { + protocol_fees_collected + .checked_add(self.collected_program_fees_outstanding.into()) + .ok_or_else(math_error!())? + .into() + }; + } + + #[cfg(not(feature = "client"))] + { + #[cfg(feature = "debug")] + solana_program::log::sol_log_compute_units(); + + emit!(LendingPoolBankAccrueInterestEvent { + header: GroupEventHeader { + marginfi_group: self.group, + signer: None + }, + bank, + mint: self.mint, + delta: time_delta, + fees_collected: group_fees_collected.to_num::(), + insurance_collected: insurance_fees_collected.to_num::(), + }); + } + + Ok(()) + } + + pub fn deposit_spl_transfer<'info>( + &self, + amount: u64, + from: AccountInfo<'info>, + to: AccountInfo<'info>, + authority: AccountInfo<'info>, + maybe_mint: Option<&InterfaceAccount<'info, Mint>>, + program: AccountInfo<'info>, + remaining_accounts: &[AccountInfo<'info>], + ) -> MarginfiResult { + check!( + to.key.eq(&self.liquidity_vault), + MarginfiError::InvalidTransfer + ); + + debug!( + "deposit_spl_transfer: amount: {} from {} to {}, auth {}", + amount, from.key, to.key, authority.key + ); + + if let Some(mint) = maybe_mint { + spl_token_2022::onchain::invoke_transfer_checked( + program.key, + from, + mint.to_account_info(), + to, + authority, + remaining_accounts, + amount, + mint.decimals, + &[], + )?; + } else { + #[allow(deprecated)] + transfer( + CpiContext::new_with_signer( + program, + Transfer { + from, + to, + authority, + }, + &[], + ), + amount, + )?; + } + + Ok(()) + } + + pub fn withdraw_spl_transfer<'info>( + &self, + amount: u64, + from: AccountInfo<'info>, + to: AccountInfo<'info>, + authority: AccountInfo<'info>, + maybe_mint: Option<&InterfaceAccount<'info, Mint>>, + program: AccountInfo<'info>, + signer_seeds: &[&[&[u8]]], + remaining_accounts: &[AccountInfo<'info>], + ) -> MarginfiResult { + debug!( + "withdraw_spl_transfer: amount: {} from {} to {}, auth {}", + amount, from.key, to.key, authority.key + ); + + if let Some(mint) = maybe_mint { + spl_token_2022::onchain::invoke_transfer_checked( + program.key, + from, + mint.to_account_info(), + to, + authority, + remaining_accounts, + amount, + mint.decimals, + signer_seeds, + )?; + } else { + // `transfer_checked` and `transfer` does the same thing, the additional `_checked` logic + // is only to assert the expected attributes by the user (mint, decimal scaling), + // + // Security of `transfer` is equal to `transfer_checked`. + #[allow(deprecated)] + transfer( + CpiContext::new_with_signer( + program, + Transfer { + from, + to, + authority, + }, + signer_seeds, + ), + amount, + )?; + } + + Ok(()) + } + + /// Socialize a loss `loss_amount` among depositors, + /// the `total_deposit_shares` stays the same, but total value of deposits is + /// reduced by `loss_amount`; + pub fn socialize_loss(&mut self, loss_amount: I80F48) -> MarginfiResult { + let total_asset_shares: I80F48 = self.total_asset_shares.into(); + let old_asset_share_value: I80F48 = self.asset_share_value.into(); + + let new_share_value = total_asset_shares + .checked_mul(old_asset_share_value) + .ok_or_else(math_error!())? + .checked_sub(loss_amount) + .ok_or_else(math_error!())? + .checked_div(total_asset_shares) + .ok_or_else(math_error!())?; + + self.asset_share_value = new_share_value.into(); + + Ok(()) + } + + pub fn assert_operational_mode( + &self, + is_asset_or_liability_amount_increasing: Option, + ) -> Result<()> { + match self.config.operational_state { + BankOperationalState::Paused => Err(MarginfiError::BankPaused.into()), + BankOperationalState::Operational => Ok(()), + BankOperationalState::ReduceOnly => { + if let Some(is_asset_or_liability_amount_increasing) = + is_asset_or_liability_amount_increasing + { + check!( + !is_asset_or_liability_amount_increasing, + MarginfiError::BankReduceOnly + ); + } + + Ok(()) + } + } + } + + 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 + } +} + +assert_struct_size!(BankConfig, 544); +assert_struct_align!(BankConfig, 8); +#[zero_copy(unsafe)] +#[repr(C)] +#[cfg_attr( + any(feature = "test", feature = "client"), + derive(PartialEq, Eq, TypeLayout) +)] +#[derive(Debug)] +/// TODO: Convert weights to (u64, u64) to avoid precision loss (maybe?) +pub struct BankConfig { + pub asset_weight_init: WrappedI80F48, + pub asset_weight_maint: WrappedI80F48, + + pub liability_weight_init: WrappedI80F48, + pub liability_weight_maint: WrappedI80F48, + + pub deposit_limit: u64, + + pub interest_rate_config: InterestRateConfig, + pub operational_state: BankOperationalState, + + pub oracle_setup: OracleSetup, + pub oracle_keys: [Pubkey; MAX_ORACLE_KEYS], + + // Note: Pubkey is aligned 1, so borrow_limit is the first aligned-8 value after deposit_limit + pub _pad0: [u8; 6], // Bank state (1) + Oracle Setup (1) + 6 = 8 + + pub borrow_limit: u64, + + pub risk_tier: RiskTier, + + /// Determines what kinds of assets users of this bank can interact with. + /// Options: + /// * ASSET_TAG_DEFAULT (0) - A regular asset that can be comingled with any other regular asset + /// or with `ASSET_TAG_SOL` + /// * ASSET_TAG_SOL (1) - Accounts with a SOL position can comingle with **either** + /// `ASSET_TAG_DEFAULT` or `ASSET_TAG_STAKED` positions, but not both + /// * ASSET_TAG_STAKED (2) - Staked SOL assets. Accounts with a STAKED position can only deposit + /// other STAKED assets or SOL (`ASSET_TAG_SOL`) and can only borrow SOL + pub asset_tag: u8, + + pub _pad1: [u8; 6], + + /// USD denominated limit for calculating asset value for initialization margin requirements. + /// Example, if total SOL deposits are equal to $1M and the limit it set to $500K, + /// then SOL assets will be discounted by 50%. + /// + /// In other words the max value of liabilities that can be backed by the asset is $500K. + /// This is useful for limiting the damage of orcale attacks. + /// + /// Value is UI USD value, for example value 100 -> $100 + pub total_asset_value_init_limit: u64, + + /// Time window in seconds for the oracle price feed to be considered live. + pub oracle_max_age: u16, + + // Note: 6 bytes of padding to next 8 byte alignment, then end padding + pub _padding: [u8; 38], +} + +impl Default for BankConfig { + fn default() -> Self { + Self { + asset_weight_init: I80F48::ZERO.into(), + asset_weight_maint: I80F48::ZERO.into(), + liability_weight_init: I80F48::ONE.into(), + liability_weight_maint: I80F48::ONE.into(), + deposit_limit: 0, + borrow_limit: 0, + interest_rate_config: Default::default(), + operational_state: BankOperationalState::Paused, + oracle_setup: OracleSetup::None, + oracle_keys: [Pubkey::default(); MAX_ORACLE_KEYS], + _pad0: [0; 6], + risk_tier: RiskTier::Isolated, + asset_tag: ASSET_TAG_DEFAULT, + _pad1: [0; 6], + total_asset_value_init_limit: TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE, + oracle_max_age: 0, + _padding: [0; 38], + } + } +} + +impl BankConfig { + #[inline] + pub fn get_weights(&self, req_type: RequirementType) -> (I80F48, I80F48) { + match req_type { + RequirementType::Initial => ( + self.asset_weight_init.into(), + self.liability_weight_init.into(), + ), + RequirementType::Maintenance => ( + self.asset_weight_maint.into(), + self.liability_weight_maint.into(), + ), + RequirementType::Equity => (I80F48::ONE, I80F48::ONE), + } + } + + #[inline] + pub fn get_weight( + &self, + requirement_type: RequirementType, + balance_side: BalanceSide, + ) -> I80F48 { + match (requirement_type, balance_side) { + (RequirementType::Initial, BalanceSide::Assets) => self.asset_weight_init.into(), + (RequirementType::Initial, BalanceSide::Liabilities) => { + self.liability_weight_init.into() + } + (RequirementType::Maintenance, BalanceSide::Assets) => self.asset_weight_maint.into(), + (RequirementType::Maintenance, BalanceSide::Liabilities) => { + self.liability_weight_maint.into() + } + (RequirementType::Equity, _) => I80F48::ONE, + } + } + + pub fn validate(&self) -> MarginfiResult { + let asset_init_w = I80F48::from(self.asset_weight_init); + let asset_maint_w = I80F48::from(self.asset_weight_maint); + + check!( + asset_init_w >= I80F48::ZERO && asset_init_w <= I80F48::ONE, + MarginfiError::InvalidConfig + ); + check!(asset_maint_w >= asset_init_w, MarginfiError::InvalidConfig); + + let liab_init_w = I80F48::from(self.liability_weight_init); + let liab_maint_w = I80F48::from(self.liability_weight_maint); + + check!(liab_init_w >= I80F48::ONE, MarginfiError::InvalidConfig); + check!( + liab_maint_w <= liab_init_w && liab_maint_w >= I80F48::ONE, + MarginfiError::InvalidConfig + ); + + self.interest_rate_config.validate()?; + + if self.risk_tier == RiskTier::Isolated { + check!(asset_init_w == I80F48::ZERO, MarginfiError::InvalidConfig); + check!(asset_maint_w == I80F48::ZERO, MarginfiError::InvalidConfig); + } + + Ok(()) + } + + #[inline] + pub fn is_deposit_limit_active(&self) -> bool { + self.deposit_limit != u64::MAX + } + + #[inline] + pub fn is_borrow_limit_active(&self) -> bool { + self.borrow_limit != u64::MAX + } + + /// * lst_mint, stake_pool, sol_pool - required only if configuring + /// `OracleSetup::StakedWithPythPush` on initial setup. If configuring a staked bank after + /// initial setup, can be omitted + pub fn validate_oracle_setup( + &self, + ais: &[AccountInfo], + lst_mint: Option, + stake_pool: Option, + sol_pool: Option, + ) -> MarginfiResult { + check!( + self.oracle_max_age >= ORACLE_MIN_AGE, + MarginfiError::InvalidOracleSetup + ); + OraclePriceFeedAdapter::validate_bank_config(self, ais, lst_mint, stake_pool, sol_pool)?; + Ok(()) + } + + pub fn usd_init_limit_active(&self) -> bool { + self.total_asset_value_init_limit != TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE + } + + #[inline] + pub fn get_oracle_max_age(&self) -> u64 { + match (self.oracle_max_age, self.oracle_setup) { + (0, OracleSetup::SwitchboardV2) => MAX_SWB_ORACLE_AGE, + (0, OracleSetup::PythLegacy | OracleSetup::PythPushOracle) => MAX_PYTH_ORACLE_AGE, + (n, _) => n as u64, + } + } + + pub fn get_pyth_push_oracle_feed_id(&self) -> Option<&FeedId> { + if matches!( + self.oracle_setup, + OracleSetup::PythPushOracle | OracleSetup::StakedWithPythPush + ) { + let bytes: &[u8; 32] = self.oracle_keys[0].as_ref().try_into().unwrap(); + Some(bytes) + } else { + None + } + } +} + +#[repr(C)] +#[cfg_attr( + any(feature = "test", feature = "client"), + derive(PartialEq, Eq, TypeLayout) +)] +#[derive(AnchorDeserialize, AnchorSerialize, Debug)] +/// TODO: Convert weights to (u64, u64) to avoid precision loss (maybe?) +pub struct BankConfigCompact { + pub asset_weight_init: WrappedI80F48, + pub asset_weight_maint: WrappedI80F48, + + pub liability_weight_init: WrappedI80F48, + pub liability_weight_maint: WrappedI80F48, + + pub deposit_limit: u64, + + pub interest_rate_config: InterestRateConfigCompact, + pub operational_state: BankOperationalState, + + pub oracle_setup: OracleSetup, + pub oracle_key: Pubkey, + + pub borrow_limit: u64, + + pub risk_tier: RiskTier, + + /// Determines what kinds of assets users of this bank can interact with. + /// Options: + /// * ASSET_TAG_DEFAULT (0) - A regular asset that can be comingled with any other regular asset + /// or with `ASSET_TAG_SOL` + /// * ASSET_TAG_SOL (1) - Accounts with a SOL position can comingle with **either** + /// `ASSET_TAG_DEFAULT` or `ASSET_TAG_STAKED` positions, but not both + /// * ASSET_TAG_STAKED (2) - Staked SOL assets. Accounts with a STAKED position can only deposit + /// other STAKED assets or SOL (`ASSET_TAG_SOL`) and can only borrow SOL + pub asset_tag: u8, + + pub _pad0: [u8; 6], + + /// USD denominated limit for calculating asset value for initialization margin requirements. + /// Example, if total SOL deposits are equal to $1M and the limit it set to $500K, + /// then SOL assets will be discounted by 50%. + /// + /// In other words the max value of liabilities that can be backed by the asset is $500K. + /// This is useful for limiting the damage of orcale attacks. + /// + /// Value is UI USD value, for example value 100 -> $100 + pub total_asset_value_init_limit: u64, + + /// Time window in seconds for the oracle price feed to be considered live. + pub oracle_max_age: u16, +} + +impl From for BankConfig { + fn from(config: BankConfigCompact) -> Self { + let keys = [ + config.oracle_key, + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + ]; + Self { + asset_weight_init: config.asset_weight_init, + asset_weight_maint: config.asset_weight_maint, + liability_weight_init: config.liability_weight_init, + liability_weight_maint: config.liability_weight_maint, + deposit_limit: config.deposit_limit, + interest_rate_config: config.interest_rate_config.into(), + operational_state: config.operational_state, + oracle_setup: config.oracle_setup, + oracle_keys: keys, + _pad0: [0; 6], + borrow_limit: config.borrow_limit, + risk_tier: config.risk_tier, + asset_tag: config.asset_tag, + _pad1: [0; 6], + total_asset_value_init_limit: config.total_asset_value_init_limit, + oracle_max_age: config.oracle_max_age, + _padding: [0; 38], + } + } +} + +impl From for BankConfigCompact { + fn from(config: BankConfig) -> Self { + Self { + asset_weight_init: config.asset_weight_init, + asset_weight_maint: config.asset_weight_maint, + liability_weight_init: config.liability_weight_init, + liability_weight_maint: config.liability_weight_maint, + deposit_limit: config.deposit_limit, + interest_rate_config: config.interest_rate_config.into(), + operational_state: config.operational_state, + oracle_setup: config.oracle_setup, + oracle_key: config.oracle_keys[0], + borrow_limit: config.borrow_limit, + risk_tier: config.risk_tier, + asset_tag: config.asset_tag, + _pad0: [0; 6], + total_asset_value_init_limit: config.total_asset_value_init_limit, + oracle_max_age: config.oracle_max_age, + } + } +} + +#[cfg_attr( + any(feature = "test", feature = "client"), + derive(Clone, PartialEq, Eq, TypeLayout) +)] +#[derive(AnchorDeserialize, AnchorSerialize, Default)] +pub struct BankConfigOpt { + pub asset_weight_init: Option, + pub asset_weight_maint: Option, + + pub liability_weight_init: Option, + pub liability_weight_maint: Option, + + pub deposit_limit: Option, + pub borrow_limit: Option, + + pub operational_state: Option, + + pub oracle: Option, + + pub interest_rate_config: Option, + + pub risk_tier: Option, + + pub asset_tag: Option, + + pub total_asset_value_init_limit: Option, + + pub oracle_max_age: Option, + + pub permissionless_bad_debt_settlement: Option, + + pub freeze_settings: Option, +} diff --git a/programs/marginfi/src/state/interest_rate.rs b/programs/marginfi/src/state/interest_rate.rs new file mode 100644 index 000000000..eddbaa218 --- /dev/null +++ b/programs/marginfi/src/state/interest_rate.rs @@ -0,0 +1,875 @@ +use crate::{ + assert_struct_size, check, + constants::SECONDS_PER_YEAR, + debug, + prelude::MarginfiError, + set_if_some, + state::marginfi_group::{MarginfiGroup, WrappedI80F48}, + MarginfiResult, +}; +use anchor_lang::prelude::borsh; +use anchor_lang::prelude::*; +use fixed::types::I80F48; +use std::fmt::Debug; + +#[cfg(any(feature = "test", feature = "client"))] +use type_layout::TypeLayout; + +#[repr(C)] +#[cfg_attr( + any(feature = "test", feature = "client"), + derive(PartialEq, Eq, TypeLayout) +)] +#[derive(Default, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct InterestRateConfigCompact { + // Curve Params + pub optimal_utilization_rate: WrappedI80F48, + pub plateau_interest_rate: WrappedI80F48, + pub max_interest_rate: WrappedI80F48, + + // Fees + pub insurance_fee_fixed_apr: WrappedI80F48, + pub insurance_ir_fee: WrappedI80F48, + pub protocol_fixed_fee_apr: WrappedI80F48, + pub protocol_ir_fee: WrappedI80F48, + pub protocol_origination_fee: WrappedI80F48, +} + +impl From for InterestRateConfig { + fn from(ir_config: InterestRateConfigCompact) -> Self { + InterestRateConfig { + optimal_utilization_rate: ir_config.optimal_utilization_rate, + plateau_interest_rate: ir_config.plateau_interest_rate, + max_interest_rate: ir_config.max_interest_rate, + insurance_fee_fixed_apr: ir_config.insurance_fee_fixed_apr, + insurance_ir_fee: ir_config.insurance_ir_fee, + protocol_fixed_fee_apr: ir_config.protocol_fixed_fee_apr, + protocol_ir_fee: ir_config.protocol_ir_fee, + protocol_origination_fee: ir_config.protocol_origination_fee, + _padding0: [0; 16], + _padding1: [[0; 32]; 3], + } + } +} + +impl From for InterestRateConfigCompact { + fn from(ir_config: InterestRateConfig) -> Self { + InterestRateConfigCompact { + optimal_utilization_rate: ir_config.optimal_utilization_rate, + plateau_interest_rate: ir_config.plateau_interest_rate, + max_interest_rate: ir_config.max_interest_rate, + insurance_fee_fixed_apr: ir_config.insurance_fee_fixed_apr, + insurance_ir_fee: ir_config.insurance_ir_fee, + protocol_fixed_fee_apr: ir_config.protocol_fixed_fee_apr, + protocol_ir_fee: ir_config.protocol_ir_fee, + protocol_origination_fee: ir_config.protocol_origination_fee, + } + } +} + +assert_struct_size!(InterestRateConfig, 240); +#[zero_copy] +#[repr(C)] +#[cfg_attr( + any(feature = "test", feature = "client"), + derive(PartialEq, Eq, TypeLayout) +)] +#[derive(Default, Debug)] +pub struct InterestRateConfig { + // Curve Params + pub optimal_utilization_rate: WrappedI80F48, + pub plateau_interest_rate: WrappedI80F48, + pub max_interest_rate: WrappedI80F48, + + // Fees + /// Goes to insurance, funds `collected_insurance_fees_outstanding` + pub insurance_fee_fixed_apr: WrappedI80F48, + /// Goes to insurance, funds `collected_insurance_fees_outstanding` + pub insurance_ir_fee: WrappedI80F48, + /// Earned by the group, goes to `collected_group_fees_outstanding` + pub protocol_fixed_fee_apr: WrappedI80F48, + /// Earned by the group, goes to `collected_group_fees_outstanding` + pub protocol_ir_fee: WrappedI80F48, + pub protocol_origination_fee: WrappedI80F48, + + pub _padding0: [u8; 16], + pub _padding1: [[u8; 32]; 3], +} + +impl InterestRateConfig { + pub fn create_interest_rate_calculator(&self, group: &MarginfiGroup) -> InterestRateCalc { + let group_bank_config = &group.get_group_bank_config(); + debug!( + "Creating interest rate calculator with protocol fees: {}", + group_bank_config.program_fees + ); + InterestRateCalc { + optimal_utilization_rate: self.optimal_utilization_rate.into(), + plateau_interest_rate: self.plateau_interest_rate.into(), + max_interest_rate: self.max_interest_rate.into(), + insurance_fixed_fee: self.insurance_fee_fixed_apr.into(), + insurance_rate_fee: self.insurance_ir_fee.into(), + protocol_fixed_fee: self.protocol_fixed_fee_apr.into(), + protocol_rate_fee: self.protocol_ir_fee.into(), + add_program_fees: group_bank_config.program_fees, + program_fee_fixed: group.fee_state_cache.program_fee_fixed.into(), + program_fee_rate: group.fee_state_cache.program_fee_rate.into(), + } + } + + pub fn validate(&self) -> MarginfiResult { + let optimal_ur: I80F48 = self.optimal_utilization_rate.into(); + let plateau_ir: I80F48 = self.plateau_interest_rate.into(); + let max_ir: I80F48 = self.max_interest_rate.into(); + + check!( + optimal_ur > I80F48::ZERO && optimal_ur < I80F48::ONE, + MarginfiError::InvalidConfig + ); + check!(plateau_ir > I80F48::ZERO, MarginfiError::InvalidConfig); + check!(max_ir > I80F48::ZERO, MarginfiError::InvalidConfig); + check!(plateau_ir < max_ir, MarginfiError::InvalidConfig); + + Ok(()) + } + + pub fn update(&mut self, ir_config: &InterestRateConfigOpt) { + set_if_some!( + self.optimal_utilization_rate, + ir_config.optimal_utilization_rate + ); + set_if_some!(self.plateau_interest_rate, ir_config.plateau_interest_rate); + set_if_some!(self.max_interest_rate, ir_config.max_interest_rate); + set_if_some!( + self.insurance_fee_fixed_apr, + ir_config.insurance_fee_fixed_apr + ); + set_if_some!(self.insurance_ir_fee, ir_config.insurance_ir_fee); + set_if_some!( + self.protocol_fixed_fee_apr, + ir_config.protocol_fixed_fee_apr + ); + set_if_some!(self.protocol_ir_fee, ir_config.protocol_ir_fee); + set_if_some!( + self.protocol_origination_fee, + ir_config.protocol_origination_fee + ); + } +} + +#[cfg_attr( + any(feature = "test", feature = "client"), + derive(Debug, PartialEq, Eq, TypeLayout) +)] +#[derive(AnchorDeserialize, AnchorSerialize, Default, Clone)] +pub struct InterestRateConfigOpt { + pub optimal_utilization_rate: Option, + pub plateau_interest_rate: Option, + pub max_interest_rate: Option, + + pub insurance_fee_fixed_apr: Option, + pub insurance_ir_fee: Option, + pub protocol_fixed_fee_apr: Option, + pub protocol_ir_fee: Option, + pub protocol_origination_fee: Option, +} + +#[derive(Debug, Clone)] +/// Short for calculator +pub struct InterestRateCalc { + optimal_utilization_rate: I80F48, + plateau_interest_rate: I80F48, + max_interest_rate: I80F48, + + // Fees + insurance_fixed_fee: I80F48, + insurance_rate_fee: I80F48, + /// AKA group fixed fee + protocol_fixed_fee: I80F48, + /// AKA group rate fee + protocol_rate_fee: I80F48, + + program_fee_fixed: I80F48, + program_fee_rate: I80F48, + + add_program_fees: bool, +} + +impl InterestRateCalc { + /// Return interest rate charged to borrowers and to depositors. + /// Rate is denominated in APR (0-). + /// + /// Return ComputedInterestRates + pub fn calc_interest_rate(&self, utilization_ratio: I80F48) -> Option { + let Fees { + insurance_fee_rate, + insurance_fee_fixed, + group_fee_rate, + group_fee_fixed, + protocol_fee_rate, + protocol_fee_fixed, + } = self.get_fees(); + + let fee_ir = insurance_fee_rate + group_fee_rate + protocol_fee_rate; + let fee_fixed = insurance_fee_fixed + group_fee_fixed + protocol_fee_fixed; + + let base_rate = self.interest_rate_curve(utilization_ratio)?; + + // Lending rate is adjusted for utilization ratio to symmetrize payments between borrowers and depositors. + let lending_rate_apr = base_rate.checked_mul(utilization_ratio)?; + + // Borrowing rate is adjusted for fees. + // borrowing_rate = base_rate + base_rate * rate_fee + total_fixed_fee_apr + let borrowing_rate_apr = base_rate + .checked_mul(I80F48::ONE.checked_add(fee_ir)?)? + .checked_add(fee_fixed)?; + + let group_fee_apr = calc_fee_rate(base_rate, group_fee_rate, group_fee_fixed)?; + let insurance_fee_apr = calc_fee_rate(base_rate, insurance_fee_rate, insurance_fee_fixed)?; + let protocol_fee_apr = calc_fee_rate(base_rate, protocol_fee_rate, protocol_fee_fixed)?; + + assert!(lending_rate_apr >= I80F48::ZERO); + assert!(borrowing_rate_apr >= I80F48::ZERO); + assert!(group_fee_apr >= I80F48::ZERO); + assert!(insurance_fee_apr >= I80F48::ZERO); + assert!(protocol_fee_apr >= I80F48::ZERO); + + // TODO: Add liquidation discount check + Some(ComputedInterestRates { + lending_rate_apr, + borrowing_rate_apr, + group_fee_apr, + insurance_fee_apr, + protocol_fee_apr, + }) + } + + /// Piecewise linear interest rate function. + /// The curves approaches the `plateau_interest_rate` as the utilization ratio approaches the `optimal_utilization_rate`, + /// once the utilization ratio exceeds the `optimal_utilization_rate`, the curve approaches the `max_interest_rate`. + /// + /// To be clear we don't particularly appreciate the piecewise linear nature of this "curve", but it is what it is. + #[inline] + fn interest_rate_curve(&self, ur: I80F48) -> Option { + let optimal_ur: I80F48 = self.optimal_utilization_rate; + let plateau_ir: I80F48 = self.plateau_interest_rate; + let max_ir: I80F48 = self.max_interest_rate; + + if ur <= optimal_ur { + ur.checked_div(optimal_ur)?.checked_mul(plateau_ir) + } else { + (ur - optimal_ur) + .checked_div(I80F48::ONE - optimal_ur)? + .checked_mul(max_ir - plateau_ir)? + .checked_add(plateau_ir) + } + } + + pub fn get_fees(&self) -> Fees { + let (protocol_fee_rate, protocol_fee_fixed) = if self.add_program_fees { + (self.program_fee_rate, self.program_fee_fixed) + } else { + (I80F48::ZERO, I80F48::ZERO) + }; + + Fees { + insurance_fee_rate: self.insurance_rate_fee, + insurance_fee_fixed: self.insurance_fixed_fee, + group_fee_rate: self.protocol_rate_fee, + group_fee_fixed: self.protocol_fixed_fee, + protocol_fee_rate, + protocol_fee_fixed, + } + } +} + +#[derive(Debug, Clone)] +pub struct Fees { + pub insurance_fee_rate: I80F48, + pub insurance_fee_fixed: I80F48, + pub group_fee_rate: I80F48, + pub group_fee_fixed: I80F48, + pub protocol_fee_rate: I80F48, + pub protocol_fee_fixed: I80F48, +} + +#[derive(Debug, Clone)] +pub struct ComputedInterestRates { + pub lending_rate_apr: I80F48, + pub borrowing_rate_apr: I80F48, + pub group_fee_apr: I80F48, + pub insurance_fee_apr: I80F48, + pub protocol_fee_apr: I80F48, +} + +/// We use a simple interest rate model that auto settles the accrued interest into the lending account balances. +/// The plan is to move to a compound interest model in the future. +/// +/// Simple interest rate model: +/// - `P` - principal +/// - `i` - interest rate (per second) +/// - `t` - time (in seconds) +/// +/// `P_t = P_0 * (1 + i) * t` +/// +/// We use two interest rates, one for lending and one for borrowing. +/// +/// Lending interest rate: +/// - `i_l` - lending interest rate +/// - `i` - base interest rate +/// - `ur` - utilization rate +/// +/// `i_l` = `i` * `ur` +/// +/// Borrowing interest rate: +/// - `i_b` - borrowing interest rate +/// - `i` - base interest rate +/// - `f_i` - interest rate fee +/// - `f_f` - fixed fee +/// +/// `i_b = i * (1 + f_i) + f_f` +/// +pub fn calc_interest_rate_accrual_state_changes( + time_delta: u64, + total_assets_amount: I80F48, + total_liabilities_amount: I80F48, + interest_rate_calc: &InterestRateCalc, + asset_share_value: I80F48, + liability_share_value: I80F48, +) -> Option { + let utilization_rate = total_liabilities_amount.checked_div(total_assets_amount)?; + let computed_rates = interest_rate_calc.calc_interest_rate(utilization_rate)?; + + debug!( + "Utilization rate: {}, time delta {}s", + utilization_rate, time_delta + ); + debug!("{:#?}", computed_rates); + + let ComputedInterestRates { + lending_rate_apr, + borrowing_rate_apr, + group_fee_apr, + insurance_fee_apr, + protocol_fee_apr, + } = computed_rates; + + Some(InterestRateStateChanges { + new_asset_share_value: calc_accrued_interest_payment_per_period( + lending_rate_apr, + time_delta, + asset_share_value, + )?, + new_liability_share_value: calc_accrued_interest_payment_per_period( + borrowing_rate_apr, + time_delta, + liability_share_value, + )?, + insurance_fees_collected: calc_interest_payment_for_period( + insurance_fee_apr, + time_delta, + total_liabilities_amount, + )?, + group_fees_collected: calc_interest_payment_for_period( + group_fee_apr, + time_delta, + total_liabilities_amount, + )?, + protocol_fees_collected: calc_interest_payment_for_period( + protocol_fee_apr, + time_delta, + total_liabilities_amount, + )?, + }) +} + +pub struct InterestRateStateChanges { + pub new_asset_share_value: I80F48, + pub new_liability_share_value: I80F48, + pub insurance_fees_collected: I80F48, + pub group_fees_collected: I80F48, + pub protocol_fees_collected: I80F48, +} + +/// Calculates the fee rate for a given base rate and fees specified. +/// The returned rate is only the fee rate without the base rate. +/// +/// Used for calculating the fees charged to the borrowers. +fn calc_fee_rate(base_rate: I80F48, rate_fees: I80F48, fixed_fees: I80F48) -> Option { + if rate_fees.is_zero() { + return Some(fixed_fees); + } + + base_rate.checked_mul(rate_fees)?.checked_add(fixed_fees) +} + +/// Calculates the accrued interest payment per period `time_delta` in a principal value `value` for interest rate (in APR) `arp`. +/// Result is the new principal value. +pub fn calc_accrued_interest_payment_per_period( + apr: I80F48, + time_delta: u64, + value: I80F48, +) -> Option { + let ir_per_period = apr + .checked_mul(time_delta.into())? + .checked_div(SECONDS_PER_YEAR)?; + + let new_value = value.checked_mul(I80F48::ONE.checked_add(ir_per_period)?)?; + + Some(new_value) +} + +/// Calculates the interest payment for a given period `time_delta` in a principal value `value` for interest rate (in APR) `arp`. +/// Result is the interest payment. +pub fn calc_interest_payment_for_period( + apr: I80F48, + time_delta: u64, + value: I80F48, +) -> Option { + if apr.is_zero() { + return Some(I80F48::ZERO); + } + + let interest_payment = value + .checked_mul(apr)? + .checked_mul(time_delta.into())? + .checked_div(SECONDS_PER_YEAR)?; + + Some(interest_payment) +} + +#[macro_export] +macro_rules! assert_eq_with_tolerance { + ($test_val:expr, $val:expr, $tolerance:expr) => { + assert!( + ($test_val - $val).abs() <= $tolerance, + "assertion failed: `({} - {}) <= {}`", + $test_val, + $val, + $tolerance + ); + }; +} + +#[cfg(test)] +mod tests { + use std::time::{SystemTime, UNIX_EPOCH}; + + use crate::{ + constants::{PROTOCOL_FEE_FIXED_DEFAULT, PROTOCOL_FEE_RATE_DEFAULT}, + state::bank::{Bank, BankConfig}, + }; + + use super::*; + use fixed_macro::types::I80F48; + + #[test] + /// Tests that the interest payment for a 1 year period with 100% APR is 1. + fn interest_payment_100apr_1year() { + let apr = I80F48::ONE; + let time_delta = 31_536_000; // 1 year + let value = I80F48::ONE; + + assert_eq_with_tolerance!( + calc_interest_payment_for_period(apr, time_delta, value).unwrap(), + I80F48::ONE, + I80F48!(0.001) + ); + } + + /// Tests that the interest payment for a 1 year period with 50% APR is 0.5. + #[test] + fn interest_payment_50apr_1year() { + let apr = I80F48::from_num(0.5); + let time_delta = 31_536_000; // 1 year + let value = I80F48::ONE; + + assert_eq_with_tolerance!( + calc_interest_payment_for_period(apr, time_delta, value).unwrap(), + I80F48::from_num(0.5), + I80F48!(0.001) + ); + } + /// P: 1_000_000 + /// Apr: 12% + /// Time: 1 second + #[test] + fn interest_payment_12apr_1second() { + let apr = I80F48!(0.12); + let time_delta = 1; + let value = I80F48!(1_000_000); + + assert_eq_with_tolerance!( + calc_interest_payment_for_period(apr, time_delta, value).unwrap(), + I80F48!(0.0038), + I80F48!(0.001) + ); + } + + #[test] + /// apr: 100% + /// time: 1 year + /// principal: 2 + /// expected: 4 + fn accrued_interest_apr100_year1() { + assert_eq_with_tolerance!( + calc_accrued_interest_payment_per_period(I80F48!(1), 31_536_000, I80F48!(2)).unwrap(), + I80F48!(4), + I80F48!(0.001) + ); + } + + #[test] + /// apr: 50% + /// time: 1 year + /// principal: 2 + /// expected: 3 + fn accrued_interest_apr50_year1() { + assert_eq_with_tolerance!( + calc_accrued_interest_payment_per_period(I80F48!(0.5), 31_536_000, I80F48!(2)).unwrap(), + I80F48!(3), + I80F48!(0.001) + ); + } + + #[test] + /// apr: 12% + /// time: 1 second + /// principal: 1_000_000 + /// expected: 1_038 + fn accrued_interest_apr12_year1() { + assert_eq_with_tolerance!( + calc_accrued_interest_payment_per_period(I80F48!(0.12), 1, I80F48!(1_000_000)).unwrap(), + I80F48!(1_000_000.0038), + I80F48!(0.001) + ); + } + + #[test] + /// ur: 0 + /// protocol_fixed_fee: 0.01 + fn ir_config_calc_interest_rate_pff_01() { + let config = InterestRateConfig { + optimal_utilization_rate: I80F48!(0.6).into(), + plateau_interest_rate: I80F48!(0.40).into(), + protocol_fixed_fee_apr: I80F48!(0.01).into(), + ..Default::default() + }; + + let ComputedInterestRates { + lending_rate_apr: lending_apr, + borrowing_rate_apr: borrow_apr, + group_fee_apr: group_fees_apr, + insurance_fee_apr: insurance_apr, + protocol_fee_apr, + } = config + .create_interest_rate_calculator(&MarginfiGroup::default()) + .calc_interest_rate(I80F48!(0.6)) + .unwrap(); + + assert_eq_with_tolerance!(lending_apr, I80F48!(0.24), I80F48!(0.001)); + assert_eq_with_tolerance!(borrow_apr, I80F48!(0.41), I80F48!(0.001)); + assert_eq_with_tolerance!(group_fees_apr, I80F48!(0.01), I80F48!(0.001)); + assert_eq_with_tolerance!(insurance_apr, I80F48!(0), I80F48!(0.001)); + assert_eq_with_tolerance!(protocol_fee_apr, I80F48!(0), I80F48!(0.001)); + } + + #[test] + /// ur: 0.5 + /// protocol_fixed_fee: 0.01 + /// optimal_utilization_rate: 0.5 + /// plateau_interest_rate: 0.4 + fn ir_config_calc_interest_rate_pff_01_ur_05() { + let config = InterestRateConfig { + optimal_utilization_rate: I80F48!(0.5).into(), + plateau_interest_rate: I80F48!(0.4).into(), + protocol_fixed_fee_apr: I80F48!(0.01).into(), + insurance_ir_fee: I80F48!(0.1).into(), + ..Default::default() + }; + + let ComputedInterestRates { + lending_rate_apr: lending_apr, + borrowing_rate_apr: borrow_apr, + group_fee_apr: group_fees_apr, + insurance_fee_apr: insurance_apr, + protocol_fee_apr: _, + } = config + .create_interest_rate_calculator(&MarginfiGroup::default()) + .calc_interest_rate(I80F48!(0.5)) + .unwrap(); + + assert_eq_with_tolerance!(lending_apr, I80F48!(0.2), I80F48!(0.001)); + assert_eq_with_tolerance!(borrow_apr, I80F48!(0.45), I80F48!(0.001)); + assert_eq_with_tolerance!(group_fees_apr, I80F48!(0.01), I80F48!(0.001)); + assert_eq_with_tolerance!(insurance_apr, I80F48!(0.04), I80F48!(0.001)); + } + + #[test] + fn calc_fee_rate_1() { + let rate = I80F48!(0.4); + let fee_ir = I80F48!(0.05); + let fee_fixed = I80F48!(0.01); + + assert_eq!( + calc_fee_rate(rate, fee_ir, fee_fixed).unwrap(), + I80F48!(0.03) + ); + } + + /// ur: 0.8 + /// protocol_fixed_fee: 0.01 + /// optimal_utilization_rate: 0.5 + /// plateau_interest_rate: 0.4 + /// max_interest_rate: 3 + /// insurance_ir_fee: 0.1 + #[test] + fn ir_config_calc_interest_rate_pff_01_ur_08() { + let config = InterestRateConfig { + optimal_utilization_rate: I80F48!(0.4).into(), + plateau_interest_rate: I80F48!(0.4).into(), + protocol_fixed_fee_apr: I80F48!(0.01).into(), + max_interest_rate: I80F48!(3).into(), + insurance_ir_fee: I80F48!(0.1).into(), + ..Default::default() + }; + + let ComputedInterestRates { + lending_rate_apr: lending_apr, + borrowing_rate_apr: borrow_apr, + group_fee_apr: group_fees_apr, + insurance_fee_apr: insurance_apr, + protocol_fee_apr: _, + } = config + .create_interest_rate_calculator(&MarginfiGroup::default()) + .calc_interest_rate(I80F48!(0.7)) + .unwrap(); + + assert_eq_with_tolerance!(lending_apr, I80F48!(1.19), I80F48!(0.001)); + assert_eq_with_tolerance!(borrow_apr, I80F48!(1.88), I80F48!(0.001)); + assert_eq_with_tolerance!(group_fees_apr, I80F48!(0.01), I80F48!(0.001)); + assert_eq_with_tolerance!(insurance_apr, I80F48!(0.17), I80F48!(0.001)); + } + + #[test] + fn ir_accrual_failing_fuzz_test_example() -> anyhow::Result<()> { + let ir_config = InterestRateConfig { + optimal_utilization_rate: I80F48!(0.4).into(), + plateau_interest_rate: I80F48!(0.4).into(), + protocol_fixed_fee_apr: I80F48!(0.01).into(), + max_interest_rate: I80F48!(3).into(), + insurance_ir_fee: I80F48!(0.1).into(), + ..Default::default() + }; + + let current_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + + let mut bank = Bank { + asset_share_value: I80F48::ONE.into(), + liability_share_value: I80F48::ONE.into(), + total_liability_shares: I80F48!(207_112_621_602).into(), + total_asset_shares: I80F48!(10_000_000_000_000).into(), + last_update: current_timestamp, + config: BankConfig { + asset_weight_init: I80F48!(0.5).into(), + asset_weight_maint: I80F48!(0.75).into(), + liability_weight_init: I80F48!(1.5).into(), + liability_weight_maint: I80F48!(1.25).into(), + borrow_limit: u64::MAX, + deposit_limit: u64::MAX, + interest_rate_config: ir_config, + ..Default::default() + }, + ..Default::default() + }; + + let pre_net_assets = bank.get_asset_amount(bank.total_asset_shares.into())? + - bank.get_liability_amount(bank.total_liability_shares.into())?; + + let mut clock = Clock::default(); + + clock.unix_timestamp = current_timestamp + 3600; + + bank.accrue_interest( + current_timestamp, + &MarginfiGroup::default(), + #[cfg(not(feature = "client"))] + Pubkey::default(), + ) + .unwrap(); + + let post_collected_fees = I80F48::from(bank.collected_group_fees_outstanding) + + I80F48::from(bank.collected_insurance_fees_outstanding); + + let post_net_assets = bank.get_asset_amount(bank.total_asset_shares.into())? + + post_collected_fees + - bank.get_liability_amount(bank.total_liability_shares.into())?; + + assert_eq_with_tolerance!(pre_net_assets, post_net_assets, I80F48!(1)); + + Ok(()) + } + + #[test] + fn interest_rate_accrual_test_0() -> anyhow::Result<()> { + let ir_config = InterestRateConfig { + optimal_utilization_rate: I80F48!(0.4).into(), + plateau_interest_rate: I80F48!(0.4).into(), + protocol_fixed_fee_apr: I80F48!(0.01).into(), + max_interest_rate: I80F48!(3).into(), + insurance_ir_fee: I80F48!(0.1).into(), + ..Default::default() + }; + + let ur = I80F48!(207_112_621_602) / I80F48!(10_000_000_000_000); + let mut group = MarginfiGroup::default(); + group.group_flags = 1; + group.fee_state_cache.program_fee_fixed = PROTOCOL_FEE_FIXED_DEFAULT.into(); + group.fee_state_cache.program_fee_rate = PROTOCOL_FEE_RATE_DEFAULT.into(); + + let ComputedInterestRates { + lending_rate_apr: lending_apr, + borrowing_rate_apr: borrow_apr, + group_fee_apr, + insurance_fee_apr, + protocol_fee_apr, + } = ir_config + .create_interest_rate_calculator(&group) + .calc_interest_rate(ur) + .expect("interest rate calculation failed"); + + println!("ur: {}", ur); + println!("lending_apr: {}", lending_apr); + println!("borrow_apr: {}", borrow_apr); + println!("group_fee_apr: {}", group_fee_apr); + println!("insurance_fee_apr: {}", insurance_fee_apr); + + assert_eq_with_tolerance!( + borrow_apr, + (lending_apr / ur) + group_fee_apr + insurance_fee_apr + protocol_fee_apr, + I80F48!(0.001) + ); + + Ok(()) + } + + #[test] + fn interest_rate_accrual_test_0_no_protocol_fees() -> anyhow::Result<()> { + let ir_config = InterestRateConfig { + optimal_utilization_rate: I80F48!(0.4).into(), + plateau_interest_rate: I80F48!(0.4).into(), + protocol_fixed_fee_apr: I80F48!(0.01).into(), + max_interest_rate: I80F48!(3).into(), + insurance_ir_fee: I80F48!(0.1).into(), + ..Default::default() + }; + + let ur = I80F48!(207_112_621_602) / I80F48!(10_000_000_000_000); + + let ComputedInterestRates { + lending_rate_apr: lending_apr, + borrowing_rate_apr: borrow_apr, + group_fee_apr, + insurance_fee_apr, + protocol_fee_apr, + } = ir_config + .create_interest_rate_calculator(&MarginfiGroup::default()) + .calc_interest_rate(ur) + .expect("interest rate calculation failed"); + + println!("ur: {}", ur); + println!("lending_apr: {}", lending_apr); + println!("borrow_apr: {}", borrow_apr); + println!("group_fee_apr: {}", group_fee_apr); + println!("insurance_fee_apr: {}", insurance_fee_apr); + + assert!(protocol_fee_apr.is_zero()); + + assert_eq_with_tolerance!( + borrow_apr, + (lending_apr / ur) + group_fee_apr + insurance_fee_apr, + I80F48!(0.001) + ); + + Ok(()) + } + + #[test] + fn test_accruing_interest() -> anyhow::Result<()> { + let ir_config = InterestRateConfig { + optimal_utilization_rate: I80F48!(0.4).into(), + plateau_interest_rate: I80F48!(0.4).into(), + protocol_fixed_fee_apr: I80F48!(0.01).into(), + max_interest_rate: I80F48!(3).into(), + insurance_ir_fee: I80F48!(0.1).into(), + ..Default::default() + }; + + let mut group = MarginfiGroup::default(); + group.group_flags = 1; + group.fee_state_cache.program_fee_fixed = PROTOCOL_FEE_FIXED_DEFAULT.into(); + group.fee_state_cache.program_fee_rate = PROTOCOL_FEE_RATE_DEFAULT.into(); + + let liab_share_value = I80F48!(1.0); + let asset_share_value = I80F48!(1.0); + + let total_liability_shares = I80F48!(207_112_621_602); + let total_asset_shares = I80F48!(10_000_000_000_000); + + let old_total_liability_amount = liab_share_value * total_liability_shares; + let old_total_asset_amount = asset_share_value * total_asset_shares; + + let InterestRateStateChanges { + new_asset_share_value, + new_liability_share_value: new_liab_share_value, + insurance_fees_collected: insurance_collected, + group_fees_collected, + protocol_fees_collected, + } = calc_interest_rate_accrual_state_changes( + 3600, + total_asset_shares, + total_liability_shares, + &ir_config.create_interest_rate_calculator(&group), + asset_share_value, + liab_share_value, + ) + .unwrap(); + + let new_total_liability_amount = total_liability_shares * new_liab_share_value; + let new_total_asset_amount = total_asset_shares * new_asset_share_value; + + println!("new_asset_share_value: {}", new_asset_share_value); + println!("new_liab_share_value: {}", new_liab_share_value); + println!("group_fees_collected: {}", group_fees_collected); + println!("insurance_collected: {}", insurance_collected); + println!("protocol_fees_collected: {}", protocol_fees_collected); + + println!("new_total_liability_amount: {}", new_total_liability_amount); + println!("new_total_asset_amount: {}", new_total_asset_amount); + + println!("old_total_liability_amount: {}", old_total_liability_amount); + println!("old_total_asset_amount: {}", old_total_asset_amount); + + let total_fees_collected = + group_fees_collected + insurance_collected + protocol_fees_collected; + + println!("total_fee_collected: {}", total_fees_collected); + + println!( + "diff: {}", + ((new_total_asset_amount - new_total_liability_amount) + total_fees_collected) + - (old_total_asset_amount - old_total_liability_amount) + ); + + assert_eq_with_tolerance!( + (new_total_asset_amount - new_total_liability_amount) + total_fees_collected, + old_total_asset_amount - old_total_liability_amount, + I80F48::ONE + ); + + Ok(()) + } +} diff --git a/programs/marginfi/src/state/marginfi_account.rs b/programs/marginfi/src/state/marginfi_account.rs index 460a69c74..dcdbe0395 100644 --- a/programs/marginfi/src/state/marginfi_account.rs +++ b/programs/marginfi/src/state/marginfi_account.rs @@ -1,11 +1,12 @@ use super::{ - marginfi_group::{Bank, RiskTier, WrappedI80F48}, + bank::Bank, + marginfi_group::{RiskTier, WrappedI80F48}, price::{OraclePriceFeedAdapter, OraclePriceType, PriceAdapter, PriceBias}, }; use crate::{ assert_struct_align, assert_struct_size, check, constants::{ - ASSET_TAG_DEFAULT, ASSET_TAG_STAKED, BANKRUPT_THRESHOLD, EMISSIONS_FLAG_BORROW_ACTIVE, + ASSET_TAG_DEFAULT, ASSET_TAG_STAKED, EMISSIONS_FLAG_BORROW_ACTIVE, EMISSIONS_FLAG_LENDING_ACTIVE, EMPTY_BALANCE_THRESHOLD, EXP_10_I80F48, MIN_EMISSIONS_START_TIME, SECONDS_PER_YEAR, ZERO_AMOUNT_THRESHOLD, }, @@ -16,10 +17,7 @@ use crate::{ use anchor_lang::prelude::*; use anchor_spl::token_interface::Mint; use fixed::types::I80F48; -use std::{ - cmp::{max, min}, - ops::Not, -}; +use std::cmp::{max, min}; #[cfg(any(feature = "test", feature = "client"))] use type_layout::TypeLayout; @@ -49,7 +47,6 @@ pub struct MarginfiAccount { } pub const DISABLED_FLAG: u64 = 1 << 0; -pub const IN_FLASHLOAN_FLAG: u64 = 1 << 1; pub const FLASHLOAN_ENABLED_FLAG: u64 = 1 << 2; pub const TRANSFER_AUTHORITY_ALLOWED_FLAG: u64 = 1 << 3; @@ -154,9 +151,9 @@ impl RequirementType { } pub struct BankAccountWithPriceFeed<'a, 'info> { - bank: AccountInfo<'info>, + pub bank: AccountInfo<'info>, price_feed: Box>, - balance: &'a Balance, + pub balance: &'a Balance, } pub enum BalanceSide { @@ -246,7 +243,7 @@ impl<'info> BankAccountWithPriceFeed<'_, 'info> { /// 3. Initial requirement is discounted by the initial discount, if enabled and the usd limit is exceeded. /// 4. Assets are only calculated for collateral risk tier. /// 5. Oracle errors are ignored for deposits in isolated risk tier. - fn calc_weighted_assets_and_liabilities_values<'a>( + pub fn calc_weighted_assets_and_liabilities_values<'a>( &'a self, requirement_type: RequirementType, ) -> MarginfiResult<(I80F48, I80F48)> @@ -427,306 +424,6 @@ pub fn calc_amount(value: I80F48, price: I80F48, mint_decimals: u8) -> MarginfiR Ok(qt) } -pub enum RiskRequirementType { - Initial, - Maintenance, - Equity, -} - -impl RiskRequirementType { - pub fn to_weight_type(&self) -> RequirementType { - match self { - RiskRequirementType::Initial => RequirementType::Initial, - RiskRequirementType::Maintenance => RequirementType::Maintenance, - RiskRequirementType::Equity => RequirementType::Equity, - } - } -} - -pub struct RiskEngine<'a, 'info> { - marginfi_account: &'a MarginfiAccount, - bank_accounts_with_price: Vec>, -} - -impl<'info> RiskEngine<'_, 'info> { - pub fn new<'a>( - marginfi_account: &'a MarginfiAccount, - remaining_ais: &'info [AccountInfo<'info>], - ) -> MarginfiResult> { - check!( - !marginfi_account.get_flag(IN_FLASHLOAN_FLAG), - MarginfiError::AccountInFlashloan - ); - - Self::new_no_flashloan_check(marginfi_account, remaining_ais) - } - - /// Internal constructor used either after manually checking account is not in a flashloan, - /// or explicity checking health for flashloan enabled actions. - fn new_no_flashloan_check<'a>( - marginfi_account: &'a MarginfiAccount, - remaining_ais: &'info [AccountInfo<'info>], - ) -> MarginfiResult> { - let bank_accounts_with_price = - BankAccountWithPriceFeed::load(&marginfi_account.lending_account, remaining_ais)?; - - Ok(RiskEngine { - marginfi_account, - bank_accounts_with_price, - }) - } - - /// Checks account is healthy after performing actions that increase risk (removing liquidity). - /// - /// `IN_FLASHLOAN_FLAG` behavior. - /// - Health check is skipped. - /// - `remaining_ais` can be an empty vec. - pub fn check_account_init_health<'a>( - marginfi_account: &'a MarginfiAccount, - remaining_ais: &'info [AccountInfo<'info>], - ) -> MarginfiResult<()> { - if marginfi_account.get_flag(IN_FLASHLOAN_FLAG) { - return Ok(()); - } - - Self::new_no_flashloan_check(marginfi_account, remaining_ais)? - .check_account_health(RiskRequirementType::Initial)?; - - Ok(()) - } - - /// Returns the total assets and liabilities of the account in the form of (assets, liabilities) - pub fn get_account_health_components( - &self, - requirement_type: RiskRequirementType, - ) -> MarginfiResult<(I80F48, I80F48)> { - let mut total_assets = I80F48::ZERO; - let mut total_liabilities = I80F48::ZERO; - - for a in &self.bank_accounts_with_price { - let (assets, liabilities) = - a.calc_weighted_assets_and_liabilities_values(requirement_type.to_weight_type())?; - - debug!( - "Balance {}, assets: {}, liabilities: {}", - a.balance.bank_pk, assets, liabilities - ); - - total_assets = total_assets.checked_add(assets).ok_or_else(math_error!())?; - total_liabilities = total_liabilities - .checked_add(liabilities) - .ok_or_else(math_error!())?; - } - - Ok((total_assets, total_liabilities)) - } - - pub fn get_account_health( - &'info self, - requirement_type: RiskRequirementType, - ) -> MarginfiResult { - let (total_weighted_assets, total_weighted_liabilities) = - self.get_account_health_components(requirement_type)?; - - Ok(total_weighted_assets - .checked_sub(total_weighted_liabilities) - .ok_or_else(math_error!())?) - } - - fn check_account_health(&self, requirement_type: RiskRequirementType) -> MarginfiResult { - let (total_weighted_assets, total_weighted_liabilities) = - self.get_account_health_components(requirement_type)?; - - debug!( - "check_health: assets {} - liabs: {}", - total_weighted_assets, total_weighted_liabilities - ); - - check!( - total_weighted_assets >= total_weighted_liabilities, - MarginfiError::RiskEngineInitRejected - ); - - self.check_account_risk_tiers()?; - - Ok(()) - } - - /// Checks - /// 1. Account is liquidatable - /// 2. Account has an outstanding liability for the provided liability bank - pub fn check_pre_liquidation_condition_and_get_account_health( - &self, - bank_pk: &Pubkey, - ) -> MarginfiResult { - check!( - !self.marginfi_account.get_flag(IN_FLASHLOAN_FLAG), - MarginfiError::AccountInFlashloan - ); - - let liability_bank_balance = self - .bank_accounts_with_price - .iter() - .find(|a| a.balance.bank_pk == *bank_pk) - .ok_or(MarginfiError::LendingAccountBalanceNotFound)?; - - check!( - liability_bank_balance - .is_empty(BalanceSide::Liabilities) - .not(), - MarginfiError::IllegalLiquidation - ); - - check!( - liability_bank_balance.is_empty(BalanceSide::Assets), - MarginfiError::IllegalLiquidation - ); - - let (assets, liabs) = - self.get_account_health_components(RiskRequirementType::Maintenance)?; - - let account_health = assets.checked_sub(liabs).ok_or_else(math_error!())?; - - debug!( - "pre_liquidation_health: {} ({} - {})", - account_health, assets, liabs - ); - - check!( - account_health <= I80F48::ZERO, - MarginfiError::IllegalLiquidation, - "Account not unhealthy" - ); - - Ok(account_health) - } - - /// Check that the account is at most at the maintenance requirement level post liquidation. - /// This check is used to ensure two things in the liquidation process: - /// 1. We check that the liquidatee's remaining liability is not empty - /// 2. Liquidatee account was below the maintenance requirement level before liquidation (as health can only increase, because liquidations always pay down liabilities) - /// 3. Liquidator didn't liquidate too many assets that would result in unnecessary loss for the liquidatee. - /// - /// This check works on the assumption that the liquidation always results in a reduction of risk. - /// - /// 1. We check that the paid off liability is not zero. Assuming the liquidation always pays off some liability, this ensures that the liquidation was not too large. - /// 2. We check that the account is still at most at the maintenance requirement level. This ensures that the liquidation was not too large overall. - pub fn check_post_liquidation_condition_and_get_account_health( - &self, - bank_pk: &Pubkey, - pre_liquidation_health: I80F48, - ) -> MarginfiResult { - check!( - !self.marginfi_account.get_flag(IN_FLASHLOAN_FLAG), - MarginfiError::AccountInFlashloan - ); - - let liability_bank_balance = self - .bank_accounts_with_price - .iter() - .find(|a| a.balance.bank_pk == *bank_pk) - .unwrap(); - - check!( - liability_bank_balance - .is_empty(BalanceSide::Liabilities) - .not(), - MarginfiError::IllegalLiquidation, - "Liability payoff too severe, exhausted liability" - ); - - check!( - liability_bank_balance.is_empty(BalanceSide::Assets), - MarginfiError::IllegalLiquidation, - "Liability payoff too severe, liability balance has assets" - ); - - let (assets, liabs) = - self.get_account_health_components(RiskRequirementType::Maintenance)?; - - let account_health = assets.checked_sub(liabs).ok_or_else(math_error!())?; - - check!( - account_health <= I80F48::ZERO, - MarginfiError::IllegalLiquidation, - "Liquidation too severe, account above maintenance requirement" - ); - - debug!( - "account_health: {} ({} - {}), pre_liquidation_health: {}", - account_health, assets, liabs, pre_liquidation_health, - ); - - check!( - account_health > pre_liquidation_health, - MarginfiError::IllegalLiquidation, - "Post liquidation health worse" - ); - - Ok(account_health) - } - - /// Check that the account is in a bankrupt state. - /// Account needs to be insolvent and total value of assets need to be below the bankruptcy threshold. - pub fn check_account_bankrupt(&self) -> MarginfiResult { - let (total_assets, total_liabilities) = - self.get_account_health_components(RiskRequirementType::Equity)?; - - check!( - !self.marginfi_account.get_flag(IN_FLASHLOAN_FLAG), - MarginfiError::AccountInFlashloan - ); - - msg!( - "check_bankrupt: assets {} - liabs: {}", - total_assets, - total_liabilities - ); - - check!( - total_assets < total_liabilities, - MarginfiError::AccountNotBankrupt - ); - check!( - total_assets < BANKRUPT_THRESHOLD && total_liabilities > ZERO_AMOUNT_THRESHOLD, - MarginfiError::AccountNotBankrupt - ); - - Ok(()) - } - - fn check_account_risk_tiers<'a>(&'a self) -> MarginfiResult - where - 'info: 'a, - { - let balances_with_liablities = self - .bank_accounts_with_price - .iter() - .filter(|a| a.balance.is_empty(BalanceSide::Liabilities).not()); - - let n_balances_with_liablities = balances_with_liablities.clone().count(); - - let is_in_isolated_risk_tier = balances_with_liablities.clone().any(|a| { - // SAFETY: We are shortening 'info -> 'a - let shorter_bank: &'a AccountInfo<'a> = unsafe { core::mem::transmute(&a.bank) }; - AccountLoader::::try_from(shorter_bank) - .unwrap() - .load() - .unwrap() - .config - .risk_tier - == RiskTier::Isolated - }); - - check!( - !is_in_isolated_risk_tier || n_balances_with_liablities == 1, - MarginfiError::IsolatedAccountIllegalState - ); - - Ok(()) - } -} - const MAX_LENDING_ACCOUNT_BALANCES: usize = 16; assert_struct_size!(LendingAccount, 1728); diff --git a/programs/marginfi/src/state/marginfi_group.rs b/programs/marginfi/src/state/marginfi_group.rs index 579766e06..726ed12cd 100644 --- a/programs/marginfi/src/state/marginfi_group.rs +++ b/programs/marginfi/src/state/marginfi_group.rs @@ -1,42 +1,23 @@ -use super::{ - marginfi_account::{BalanceSide, RequirementType}, - price::{OraclePriceFeedAdapter, OracleSetup}, -}; -#[cfg(not(feature = "client"))] -use crate::events::{GroupEventHeader, LendingPoolBankAccrueInterestEvent}; +use super::price::OracleSetup; +use crate::borsh::{BorshDeserialize, BorshSerialize}; use crate::{ - assert_struct_align, assert_struct_size, check, + assert_struct_size, check, constants::{ - 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, - ORACLE_MIN_AGE, PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG, PYTH_ID, SECONDS_PER_YEAR, - TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE, + FEE_VAULT_AUTHORITY_SEED, FEE_VAULT_SEED, INSURANCE_VAULT_AUTHORITY_SEED, + INSURANCE_VAULT_SEED, LIQUIDITY_VAULT_AUTHORITY_SEED, LIQUIDITY_VAULT_SEED, + MAX_ORACLE_KEYS, PYTH_ID, }, - debug, math_error, prelude::MarginfiError, - set_if_some, - state::marginfi_account::calc_value, - MarginfiResult, -}; -use crate::{ - borsh::{BorshDeserialize, BorshSerialize}, - constants::ASSET_TAG_DEFAULT, - constants::FREEZE_SETTINGS, + set_if_some, MarginfiResult, }; use anchor_lang::prelude::borsh; use anchor_lang::prelude::*; -use anchor_spl::token_interface::*; use bytemuck::{Pod, Zeroable}; use fixed::types::I80F48; use pyth_sdk_solana::{state::SolanaPriceAccount, PriceFeed}; -use pyth_solana_receiver_sdk::price_update::FeedId; #[cfg(feature = "client")] use std::fmt::Display; -use std::{ - fmt::{Debug, Formatter}, - ops::Not, -}; +use std::fmt::{Debug, Formatter}; #[cfg(any(feature = "test", feature = "client"))] use type_layout::TypeLayout; @@ -138,1019 +119,12 @@ pub fn load_pyth_price_feed(ai: &AccountInfo) -> MarginfiResult { Ok(price_feed) } -#[repr(C)] -#[cfg_attr( - any(feature = "test", feature = "client"), - derive(PartialEq, Eq, TypeLayout) -)] -#[derive(Default, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct InterestRateConfigCompact { - // Curve Params - pub optimal_utilization_rate: WrappedI80F48, - pub plateau_interest_rate: WrappedI80F48, - pub max_interest_rate: WrappedI80F48, - - // Fees - pub insurance_fee_fixed_apr: WrappedI80F48, - pub insurance_ir_fee: WrappedI80F48, - pub protocol_fixed_fee_apr: WrappedI80F48, - pub protocol_ir_fee: WrappedI80F48, - pub protocol_origination_fee: WrappedI80F48, -} - -impl From for InterestRateConfig { - fn from(ir_config: InterestRateConfigCompact) -> Self { - InterestRateConfig { - optimal_utilization_rate: ir_config.optimal_utilization_rate, - plateau_interest_rate: ir_config.plateau_interest_rate, - max_interest_rate: ir_config.max_interest_rate, - insurance_fee_fixed_apr: ir_config.insurance_fee_fixed_apr, - insurance_ir_fee: ir_config.insurance_ir_fee, - protocol_fixed_fee_apr: ir_config.protocol_fixed_fee_apr, - protocol_ir_fee: ir_config.protocol_ir_fee, - protocol_origination_fee: ir_config.protocol_origination_fee, - _padding0: [0; 16], - _padding1: [[0; 32]; 3], - } - } -} - -impl From for InterestRateConfigCompact { - fn from(ir_config: InterestRateConfig) -> Self { - InterestRateConfigCompact { - optimal_utilization_rate: ir_config.optimal_utilization_rate, - plateau_interest_rate: ir_config.plateau_interest_rate, - max_interest_rate: ir_config.max_interest_rate, - insurance_fee_fixed_apr: ir_config.insurance_fee_fixed_apr, - insurance_ir_fee: ir_config.insurance_ir_fee, - protocol_fixed_fee_apr: ir_config.protocol_fixed_fee_apr, - protocol_ir_fee: ir_config.protocol_ir_fee, - protocol_origination_fee: ir_config.protocol_origination_fee, - } - } -} - -assert_struct_size!(InterestRateConfig, 240); -#[zero_copy] -#[repr(C)] -#[cfg_attr( - any(feature = "test", feature = "client"), - derive(PartialEq, Eq, TypeLayout) -)] -#[derive(Default, Debug)] -pub struct InterestRateConfig { - // Curve Params - pub optimal_utilization_rate: WrappedI80F48, - pub plateau_interest_rate: WrappedI80F48, - pub max_interest_rate: WrappedI80F48, - - // Fees - /// Goes to insurance, funds `collected_insurance_fees_outstanding` - pub insurance_fee_fixed_apr: WrappedI80F48, - /// Goes to insurance, funds `collected_insurance_fees_outstanding` - pub insurance_ir_fee: WrappedI80F48, - /// Earned by the group, goes to `collected_group_fees_outstanding` - pub protocol_fixed_fee_apr: WrappedI80F48, - /// Earned by the group, goes to `collected_group_fees_outstanding` - pub protocol_ir_fee: WrappedI80F48, - pub protocol_origination_fee: WrappedI80F48, - - pub _padding0: [u8; 16], - pub _padding1: [[u8; 32]; 3], -} - -impl InterestRateConfig { - pub fn create_interest_rate_calculator(&self, group: &MarginfiGroup) -> InterestRateCalc { - let group_bank_config = &group.get_group_bank_config(); - debug!( - "Creating interest rate calculator with protocol fees: {}", - group_bank_config.program_fees - ); - InterestRateCalc { - optimal_utilization_rate: self.optimal_utilization_rate.into(), - plateau_interest_rate: self.plateau_interest_rate.into(), - max_interest_rate: self.max_interest_rate.into(), - insurance_fixed_fee: self.insurance_fee_fixed_apr.into(), - insurance_rate_fee: self.insurance_ir_fee.into(), - protocol_fixed_fee: self.protocol_fixed_fee_apr.into(), - protocol_rate_fee: self.protocol_ir_fee.into(), - add_program_fees: group_bank_config.program_fees, - program_fee_fixed: group.fee_state_cache.program_fee_fixed.into(), - program_fee_rate: group.fee_state_cache.program_fee_rate.into(), - } - } - - pub fn validate(&self) -> MarginfiResult { - let optimal_ur: I80F48 = self.optimal_utilization_rate.into(); - let plateau_ir: I80F48 = self.plateau_interest_rate.into(); - let max_ir: I80F48 = self.max_interest_rate.into(); - - check!( - optimal_ur > I80F48::ZERO && optimal_ur < I80F48::ONE, - MarginfiError::InvalidConfig - ); - check!(plateau_ir > I80F48::ZERO, MarginfiError::InvalidConfig); - check!(max_ir > I80F48::ZERO, MarginfiError::InvalidConfig); - check!(plateau_ir < max_ir, MarginfiError::InvalidConfig); - - Ok(()) - } - - pub fn update(&mut self, ir_config: &InterestRateConfigOpt) { - set_if_some!( - self.optimal_utilization_rate, - ir_config.optimal_utilization_rate - ); - set_if_some!(self.plateau_interest_rate, ir_config.plateau_interest_rate); - set_if_some!(self.max_interest_rate, ir_config.max_interest_rate); - set_if_some!( - self.insurance_fee_fixed_apr, - ir_config.insurance_fee_fixed_apr - ); - set_if_some!(self.insurance_ir_fee, ir_config.insurance_ir_fee); - set_if_some!( - self.protocol_fixed_fee_apr, - ir_config.protocol_fixed_fee_apr - ); - set_if_some!(self.protocol_ir_fee, ir_config.protocol_ir_fee); - set_if_some!( - self.protocol_origination_fee, - ir_config.protocol_origination_fee - ); - } -} - -#[derive(Debug, Clone)] -/// Short for calculator -pub struct InterestRateCalc { - optimal_utilization_rate: I80F48, - plateau_interest_rate: I80F48, - max_interest_rate: I80F48, - - // Fees - insurance_fixed_fee: I80F48, - insurance_rate_fee: I80F48, - /// AKA group fixed fee - protocol_fixed_fee: I80F48, - /// AKA group rate fee - protocol_rate_fee: I80F48, - - program_fee_fixed: I80F48, - program_fee_rate: I80F48, - - add_program_fees: bool, -} - -impl InterestRateCalc { - /// Return interest rate charged to borrowers and to depositors. - /// Rate is denominated in APR (0-). - /// - /// Return ComputedInterestRates - pub fn calc_interest_rate(&self, utilization_ratio: I80F48) -> Option { - let Fees { - insurance_fee_rate, - insurance_fee_fixed, - group_fee_rate, - group_fee_fixed, - protocol_fee_rate, - protocol_fee_fixed, - } = self.get_fees(); - - let fee_ir = insurance_fee_rate + group_fee_rate + protocol_fee_rate; - let fee_fixed = insurance_fee_fixed + group_fee_fixed + protocol_fee_fixed; - - let base_rate = self.interest_rate_curve(utilization_ratio)?; - - // Lending rate is adjusted for utilization ratio to symmetrize payments between borrowers and depositors. - let lending_rate_apr = base_rate.checked_mul(utilization_ratio)?; - - // Borrowing rate is adjusted for fees. - // borrowing_rate = base_rate + base_rate * rate_fee + total_fixed_fee_apr - let borrowing_rate_apr = base_rate - .checked_mul(I80F48::ONE.checked_add(fee_ir)?)? - .checked_add(fee_fixed)?; - - let group_fee_apr = calc_fee_rate(base_rate, group_fee_rate, group_fee_fixed)?; - let insurance_fee_apr = calc_fee_rate(base_rate, insurance_fee_rate, insurance_fee_fixed)?; - let protocol_fee_apr = calc_fee_rate(base_rate, protocol_fee_rate, protocol_fee_fixed)?; - - assert!(lending_rate_apr >= I80F48::ZERO); - assert!(borrowing_rate_apr >= I80F48::ZERO); - assert!(group_fee_apr >= I80F48::ZERO); - assert!(insurance_fee_apr >= I80F48::ZERO); - assert!(protocol_fee_apr >= I80F48::ZERO); - - // TODO: Add liquidation discount check - Some(ComputedInterestRates { - lending_rate_apr, - borrowing_rate_apr, - group_fee_apr, - insurance_fee_apr, - protocol_fee_apr, - }) - } - - /// Piecewise linear interest rate function. - /// The curves approaches the `plateau_interest_rate` as the utilization ratio approaches the `optimal_utilization_rate`, - /// once the utilization ratio exceeds the `optimal_utilization_rate`, the curve approaches the `max_interest_rate`. - /// - /// To be clear we don't particularly appreciate the piecewise linear nature of this "curve", but it is what it is. - #[inline] - fn interest_rate_curve(&self, ur: I80F48) -> Option { - let optimal_ur: I80F48 = self.optimal_utilization_rate; - let plateau_ir: I80F48 = self.plateau_interest_rate; - let max_ir: I80F48 = self.max_interest_rate; - - if ur <= optimal_ur { - ur.checked_div(optimal_ur)?.checked_mul(plateau_ir) - } else { - (ur - optimal_ur) - .checked_div(I80F48::ONE - optimal_ur)? - .checked_mul(max_ir - plateau_ir)? - .checked_add(plateau_ir) - } - } - - pub fn get_fees(&self) -> Fees { - let (protocol_fee_rate, protocol_fee_fixed) = if self.add_program_fees { - (self.program_fee_rate, self.program_fee_fixed) - } else { - (I80F48::ZERO, I80F48::ZERO) - }; - - Fees { - insurance_fee_rate: self.insurance_rate_fee, - insurance_fee_fixed: self.insurance_fixed_fee, - group_fee_rate: self.protocol_rate_fee, - group_fee_fixed: self.protocol_fixed_fee, - protocol_fee_rate, - protocol_fee_fixed, - } - } -} - -#[derive(Debug, Clone)] -pub struct Fees { - pub insurance_fee_rate: I80F48, - pub insurance_fee_fixed: I80F48, - pub group_fee_rate: I80F48, - pub group_fee_fixed: I80F48, - pub protocol_fee_rate: I80F48, - pub protocol_fee_fixed: I80F48, -} - -#[derive(Debug, Clone)] -pub struct ComputedInterestRates { - pub lending_rate_apr: I80F48, - pub borrowing_rate_apr: I80F48, - pub group_fee_apr: I80F48, - pub insurance_fee_apr: I80F48, - pub protocol_fee_apr: I80F48, -} - -#[cfg_attr( - any(feature = "test", feature = "client"), - derive(Debug, PartialEq, Eq, TypeLayout) -)] -#[derive(AnchorDeserialize, AnchorSerialize, Default, Clone)] -pub struct InterestRateConfigOpt { - pub optimal_utilization_rate: Option, - pub plateau_interest_rate: Option, - pub max_interest_rate: Option, - - pub insurance_fee_fixed_apr: Option, - pub insurance_ir_fee: Option, - pub protocol_fixed_fee_apr: Option, - pub protocol_ir_fee: Option, - pub protocol_origination_fee: Option, -} - /// Group level configuration to be used in bank accounts. #[derive(Clone, Debug)] pub struct GroupBankConfig { pub program_fees: bool, } -assert_struct_size!(Bank, 1856); -assert_struct_align!(Bank, 8); -#[account(zero_copy(unsafe))] -#[repr(C)] -#[cfg_attr( - any(feature = "test", feature = "client"), - derive(Debug, PartialEq, Eq, TypeLayout) -)] -#[derive(Default)] -pub struct Bank { - pub mint: Pubkey, - pub mint_decimals: u8, - - pub group: Pubkey, - - // Note: The padding is here, not after mint_decimals. Pubkey has alignment 1, so those 32 - // bytes can cross the alignment 8 threshold, but WrappedI80F48 has alignment 8 and cannot - pub _pad0: [u8; 7], // 1x u8 + 7 = 8 - - pub asset_share_value: WrappedI80F48, - pub liability_share_value: WrappedI80F48, - - pub liquidity_vault: Pubkey, - pub liquidity_vault_bump: u8, - pub liquidity_vault_authority_bump: u8, - - pub insurance_vault: Pubkey, - pub insurance_vault_bump: u8, - pub insurance_vault_authority_bump: u8, - - pub _pad1: [u8; 4], // 4x u8 + 4 = 8 - - /// Fees collected and pending withdraw for the `insurance_vault` - pub collected_insurance_fees_outstanding: WrappedI80F48, - - pub fee_vault: Pubkey, - pub fee_vault_bump: u8, - pub fee_vault_authority_bump: u8, - - pub _pad2: [u8; 6], // 2x u8 + 6 = 8 - - /// Fees collected and pending withdraw for the `fee_vault` - pub collected_group_fees_outstanding: WrappedI80F48, - - pub total_liability_shares: WrappedI80F48, - pub total_asset_shares: WrappedI80F48, - - pub last_update: i64, - - pub config: BankConfig, - - /// Bank Config Flags - /// - /// - EMISSIONS_FLAG_BORROW_ACTIVE: 1 - /// - EMISSIONS_FLAG_LENDING_ACTIVE: 2 - /// - PERMISSIONLESS_BAD_DEBT_SETTLEMENT: 4 - /// - 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, - pub emissions_remaining: WrappedI80F48, - pub emissions_mint: Pubkey, - - /// Fees collected and pending withdraw for the `FeeState.global_fee_wallet`'s cannonical ATA for `mint` - pub collected_program_fees_outstanding: WrappedI80F48, - - pub _padding_0: [[u64; 2]; 27], - pub _padding_1: [[u64; 2]; 32], // 16 * 2 * 32 = 1024B -} - -impl Bank { - #[allow(clippy::too_many_arguments)] - pub fn new( - marginfi_group_pk: Pubkey, - config: BankConfig, - mint: Pubkey, - mint_decimals: u8, - liquidity_vault: Pubkey, - insurance_vault: Pubkey, - fee_vault: Pubkey, - current_timestamp: i64, - liquidity_vault_bump: u8, - liquidity_vault_authority_bump: u8, - insurance_vault_bump: u8, - insurance_vault_authority_bump: u8, - fee_vault_bump: u8, - fee_vault_authority_bump: u8, - ) -> Bank { - Bank { - mint, - mint_decimals, - group: marginfi_group_pk, - asset_share_value: I80F48::ONE.into(), - liability_share_value: I80F48::ONE.into(), - liquidity_vault, - liquidity_vault_bump, - liquidity_vault_authority_bump, - insurance_vault, - insurance_vault_bump, - insurance_vault_authority_bump, - collected_insurance_fees_outstanding: I80F48::ZERO.into(), - fee_vault, - fee_vault_bump, - fee_vault_authority_bump, - collected_group_fees_outstanding: I80F48::ZERO.into(), - total_liability_shares: I80F48::ZERO.into(), - total_asset_shares: I80F48::ZERO.into(), - last_update: current_timestamp, - config, - flags: 0, - emissions_rate: 0, - emissions_remaining: I80F48::ZERO.into(), - emissions_mint: Pubkey::default(), - collected_program_fees_outstanding: I80F48::ZERO.into(), - ..Default::default() - } - } - - pub fn get_liability_amount(&self, shares: I80F48) -> MarginfiResult { - Ok(shares - .checked_mul(self.liability_share_value.into()) - .ok_or_else(math_error!())?) - } - - pub fn get_asset_amount(&self, shares: I80F48) -> MarginfiResult { - Ok(shares - .checked_mul(self.asset_share_value.into()) - .ok_or_else(math_error!())?) - } - - pub fn get_liability_shares(&self, value: I80F48) -> MarginfiResult { - Ok(value - .checked_div(self.liability_share_value.into()) - .ok_or_else(math_error!())?) - } - - pub fn get_asset_shares(&self, value: I80F48) -> MarginfiResult { - Ok(value - .checked_div(self.asset_share_value.into()) - .ok_or_else(math_error!())?) - } - - pub fn change_asset_shares( - &mut self, - shares: I80F48, - bypass_deposit_limit: bool, - ) -> MarginfiResult { - let total_asset_shares: I80F48 = self.total_asset_shares.into(); - self.total_asset_shares = total_asset_shares - .checked_add(shares) - .ok_or_else(math_error!())? - .into(); - - if shares.is_positive() && self.config.is_deposit_limit_active() && !bypass_deposit_limit { - let total_deposits_amount = self.get_asset_amount(self.total_asset_shares.into())?; - let deposit_limit = I80F48::from_num(self.config.deposit_limit); - - check!( - total_deposits_amount < deposit_limit, - crate::prelude::MarginfiError::BankAssetCapacityExceeded - ) - } - - Ok(()) - } - - pub fn maybe_get_asset_weight_init_discount( - &self, - price: I80F48, - ) -> MarginfiResult> { - if self.config.usd_init_limit_active() { - let bank_total_assets_value = calc_value( - self.get_asset_amount(self.total_asset_shares.into())?, - price, - self.mint_decimals, - None, - )?; - - let total_asset_value_init_limit = - I80F48::from_num(self.config.total_asset_value_init_limit); - - #[cfg(target_os = "solana")] - debug!( - "Init limit active, limit: {}, total_assets: {}", - total_asset_value_init_limit, bank_total_assets_value - ); - - if bank_total_assets_value > total_asset_value_init_limit { - let discount = total_asset_value_init_limit - .checked_div(bank_total_assets_value) - .ok_or_else(math_error!())?; - - #[cfg(target_os = "solana")] - debug!( - "Discounting assets by {:.2} because of total deposits {} over {} usd cap", - discount, bank_total_assets_value, total_asset_value_init_limit - ); - - Ok(Some(discount)) - } else { - Ok(None) - } - } else { - Ok(None) - } - } - - pub fn change_liability_shares( - &mut self, - shares: I80F48, - bypass_borrow_limit: bool, - ) -> MarginfiResult { - let total_liability_shares: I80F48 = self.total_liability_shares.into(); - self.total_liability_shares = total_liability_shares - .checked_add(shares) - .ok_or_else(math_error!())? - .into(); - - if bypass_borrow_limit.not() && shares.is_positive() && self.config.is_borrow_limit_active() - { - let total_liability_amount = - self.get_liability_amount(self.total_liability_shares.into())?; - let borrow_limit = I80F48::from_num(self.config.borrow_limit); - - check!( - total_liability_amount < borrow_limit, - crate::prelude::MarginfiError::BankLiabilityCapacityExceeded - ) - } - - Ok(()) - } - - pub fn check_utilization_ratio(&self) -> MarginfiResult { - let total_assets = self.get_asset_amount(self.total_asset_shares.into())?; - let total_liabilities = self.get_liability_amount(self.total_liability_shares.into())?; - - check!( - total_assets >= total_liabilities, - crate::prelude::MarginfiError::IllegalUtilizationRatio - ); - - Ok(()) - } - - pub fn configure(&mut self, config: &BankConfigOpt) -> MarginfiResult { - set_if_some!(self.config.asset_weight_init, config.asset_weight_init); - set_if_some!(self.config.asset_weight_maint, config.asset_weight_maint); - set_if_some!( - self.config.liability_weight_init, - config.liability_weight_init - ); - set_if_some!( - self.config.liability_weight_maint, - config.liability_weight_maint - ); - set_if_some!(self.config.deposit_limit, config.deposit_limit); - - set_if_some!(self.config.borrow_limit, config.borrow_limit); - - set_if_some!(self.config.operational_state, config.operational_state); - - set_if_some!(self.config.oracle_setup, config.oracle.map(|o| o.setup)); - - set_if_some!(self.config.oracle_keys, config.oracle.map(|o| o.keys)); - - if let Some(ir_config) = &config.interest_rate_config { - self.config.interest_rate_config.update(ir_config); - } - - set_if_some!(self.config.risk_tier, config.risk_tier); - - set_if_some!(self.config.asset_tag, config.asset_tag); - - set_if_some!( - self.config.total_asset_value_init_limit, - config.total_asset_value_init_limit - ); - - 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); - } - - if let Some(flag) = config.freeze_settings { - self.update_flag(flag, FREEZE_SETTINGS); - } - - self.config.validate()?; - - 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. - /// A separate instruction is required to withdraw these fees. - pub fn accrue_interest( - &mut self, - current_timestamp: i64, - group: &MarginfiGroup, - #[cfg(not(feature = "client"))] bank: Pubkey, - ) -> MarginfiResult<()> { - #[cfg(all(not(feature = "client"), feature = "debug"))] - solana_program::log::sol_log_compute_units(); - - let time_delta: u64 = (current_timestamp - self.last_update).try_into().unwrap(); - if time_delta == 0 { - return Ok(()); - } - - let total_assets = self.get_asset_amount(self.total_asset_shares.into())?; - let total_liabilities = self.get_liability_amount(self.total_liability_shares.into())?; - - self.last_update = current_timestamp; - - if (total_assets == I80F48::ZERO) || (total_liabilities == I80F48::ZERO) { - #[cfg(not(feature = "client"))] - emit!(LendingPoolBankAccrueInterestEvent { - header: GroupEventHeader { - marginfi_group: self.group, - signer: None - }, - bank, - mint: self.mint, - delta: time_delta, - fees_collected: 0., - insurance_collected: 0., - }); - - return Ok(()); - } - let ir_calc = self - .config - .interest_rate_config - .create_interest_rate_calculator(group); - - let InterestRateStateChanges { - new_asset_share_value: asset_share_value, - new_liability_share_value: liability_share_value, - insurance_fees_collected, - group_fees_collected, - protocol_fees_collected, - } = calc_interest_rate_accrual_state_changes( - time_delta, - total_assets, - total_liabilities, - &ir_calc, - self.asset_share_value.into(), - self.liability_share_value.into(), - ) - .ok_or_else(math_error!())?; - - debug!("deposit share value: {}\nliability share value: {}\nfees collected: {}\ninsurance collected: {}", - asset_share_value, liability_share_value, group_fees_collected, insurance_fees_collected); - - self.asset_share_value = asset_share_value.into(); - self.liability_share_value = liability_share_value.into(); - - if group_fees_collected > I80F48::ZERO { - self.collected_group_fees_outstanding = { - group_fees_collected - .checked_add(self.collected_group_fees_outstanding.into()) - .ok_or_else(math_error!())? - .into() - }; - } - - if insurance_fees_collected > I80F48::ZERO { - self.collected_insurance_fees_outstanding = { - insurance_fees_collected - .checked_add(self.collected_insurance_fees_outstanding.into()) - .ok_or_else(math_error!())? - .into() - }; - } - if protocol_fees_collected > I80F48::ZERO { - self.collected_program_fees_outstanding = { - protocol_fees_collected - .checked_add(self.collected_program_fees_outstanding.into()) - .ok_or_else(math_error!())? - .into() - }; - } - - #[cfg(not(feature = "client"))] - { - #[cfg(feature = "debug")] - solana_program::log::sol_log_compute_units(); - - emit!(LendingPoolBankAccrueInterestEvent { - header: GroupEventHeader { - marginfi_group: self.group, - signer: None - }, - bank, - mint: self.mint, - delta: time_delta, - fees_collected: group_fees_collected.to_num::(), - insurance_collected: insurance_fees_collected.to_num::(), - }); - } - - Ok(()) - } - - pub fn deposit_spl_transfer<'info>( - &self, - amount: u64, - from: AccountInfo<'info>, - to: AccountInfo<'info>, - authority: AccountInfo<'info>, - maybe_mint: Option<&InterfaceAccount<'info, Mint>>, - program: AccountInfo<'info>, - remaining_accounts: &[AccountInfo<'info>], - ) -> MarginfiResult { - check!( - to.key.eq(&self.liquidity_vault), - MarginfiError::InvalidTransfer - ); - - debug!( - "deposit_spl_transfer: amount: {} from {} to {}, auth {}", - amount, from.key, to.key, authority.key - ); - - if let Some(mint) = maybe_mint { - spl_token_2022::onchain::invoke_transfer_checked( - program.key, - from, - mint.to_account_info(), - to, - authority, - remaining_accounts, - amount, - mint.decimals, - &[], - )?; - } else { - #[allow(deprecated)] - transfer( - CpiContext::new_with_signer( - program, - Transfer { - from, - to, - authority, - }, - &[], - ), - amount, - )?; - } - - Ok(()) - } - - pub fn withdraw_spl_transfer<'info>( - &self, - amount: u64, - from: AccountInfo<'info>, - to: AccountInfo<'info>, - authority: AccountInfo<'info>, - maybe_mint: Option<&InterfaceAccount<'info, Mint>>, - program: AccountInfo<'info>, - signer_seeds: &[&[&[u8]]], - remaining_accounts: &[AccountInfo<'info>], - ) -> MarginfiResult { - debug!( - "withdraw_spl_transfer: amount: {} from {} to {}, auth {}", - amount, from.key, to.key, authority.key - ); - - if let Some(mint) = maybe_mint { - spl_token_2022::onchain::invoke_transfer_checked( - program.key, - from, - mint.to_account_info(), - to, - authority, - remaining_accounts, - amount, - mint.decimals, - signer_seeds, - )?; - } else { - // `transfer_checked` and `transfer` does the same thing, the additional `_checked` logic - // is only to assert the expected attributes by the user (mint, decimal scaling), - // - // Security of `transfer` is equal to `transfer_checked`. - #[allow(deprecated)] - transfer( - CpiContext::new_with_signer( - program, - Transfer { - from, - to, - authority, - }, - signer_seeds, - ), - amount, - )?; - } - - Ok(()) - } - - /// Socialize a loss `loss_amount` among depositors, - /// the `total_deposit_shares` stays the same, but total value of deposits is - /// reduced by `loss_amount`; - pub fn socialize_loss(&mut self, loss_amount: I80F48) -> MarginfiResult { - let total_asset_shares: I80F48 = self.total_asset_shares.into(); - let old_asset_share_value: I80F48 = self.asset_share_value.into(); - - let new_share_value = total_asset_shares - .checked_mul(old_asset_share_value) - .ok_or_else(math_error!())? - .checked_sub(loss_amount) - .ok_or_else(math_error!())? - .checked_div(total_asset_shares) - .ok_or_else(math_error!())?; - - self.asset_share_value = new_share_value.into(); - - Ok(()) - } - - pub fn assert_operational_mode( - &self, - is_asset_or_liability_amount_increasing: Option, - ) -> Result<()> { - match self.config.operational_state { - BankOperationalState::Paused => Err(MarginfiError::BankPaused.into()), - BankOperationalState::Operational => Ok(()), - BankOperationalState::ReduceOnly => { - if let Some(is_asset_or_liability_amount_increasing) = - is_asset_or_liability_amount_increasing - { - check!( - !is_asset_or_liability_amount_increasing, - MarginfiError::BankReduceOnly - ); - } - - Ok(()) - } - } - } - - 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 - } -} - -/// We use a simple interest rate model that auto settles the accrued interest into the lending account balances. -/// The plan is to move to a compound interest model in the future. -/// -/// Simple interest rate model: -/// - `P` - principal -/// - `i` - interest rate (per second) -/// - `t` - time (in seconds) -/// -/// `P_t = P_0 * (1 + i) * t` -/// -/// We use two interest rates, one for lending and one for borrowing. -/// -/// Lending interest rate: -/// - `i_l` - lending interest rate -/// - `i` - base interest rate -/// - `ur` - utilization rate -/// -/// `i_l` = `i` * `ur` -/// -/// Borrowing interest rate: -/// - `i_b` - borrowing interest rate -/// - `i` - base interest rate -/// - `f_i` - interest rate fee -/// - `f_f` - fixed fee -/// -/// `i_b = i * (1 + f_i) + f_f` -/// -fn calc_interest_rate_accrual_state_changes( - time_delta: u64, - total_assets_amount: I80F48, - total_liabilities_amount: I80F48, - interest_rate_calc: &InterestRateCalc, - asset_share_value: I80F48, - liability_share_value: I80F48, -) -> Option { - let utilization_rate = total_liabilities_amount.checked_div(total_assets_amount)?; - let computed_rates = interest_rate_calc.calc_interest_rate(utilization_rate)?; - - debug!( - "Utilization rate: {}, time delta {}s", - utilization_rate, time_delta - ); - debug!("{:#?}", computed_rates); - - let ComputedInterestRates { - lending_rate_apr, - borrowing_rate_apr, - group_fee_apr, - insurance_fee_apr, - protocol_fee_apr, - } = computed_rates; - - Some(InterestRateStateChanges { - new_asset_share_value: calc_accrued_interest_payment_per_period( - lending_rate_apr, - time_delta, - asset_share_value, - )?, - new_liability_share_value: calc_accrued_interest_payment_per_period( - borrowing_rate_apr, - time_delta, - liability_share_value, - )?, - insurance_fees_collected: calc_interest_payment_for_period( - insurance_fee_apr, - time_delta, - total_liabilities_amount, - )?, - group_fees_collected: calc_interest_payment_for_period( - group_fee_apr, - time_delta, - total_liabilities_amount, - )?, - protocol_fees_collected: calc_interest_payment_for_period( - protocol_fee_apr, - time_delta, - total_liabilities_amount, - )?, - }) -} - -struct InterestRateStateChanges { - new_asset_share_value: I80F48, - new_liability_share_value: I80F48, - insurance_fees_collected: I80F48, - group_fees_collected: I80F48, - protocol_fees_collected: I80F48, -} - -/// Calculates the fee rate for a given base rate and fees specified. -/// The returned rate is only the fee rate without the base rate. -/// -/// Used for calculating the fees charged to the borrowers. -fn calc_fee_rate(base_rate: I80F48, rate_fees: I80F48, fixed_fees: I80F48) -> Option { - if rate_fees.is_zero() { - return Some(fixed_fees); - } - - base_rate.checked_mul(rate_fees)?.checked_add(fixed_fees) -} - -/// Calculates the accrued interest payment per period `time_delta` in a principal value `value` for interest rate (in APR) `arp`. -/// Result is the new principal value. -fn calc_accrued_interest_payment_per_period( - apr: I80F48, - time_delta: u64, - value: I80F48, -) -> Option { - let ir_per_period = apr - .checked_mul(time_delta.into())? - .checked_div(SECONDS_PER_YEAR)?; - - let new_value = value.checked_mul(I80F48::ONE.checked_add(ir_per_period)?)?; - - Some(new_value) -} - -/// Calculates the interest payment for a given period `time_delta` in a principal value `value` for interest rate (in APR) `arp`. -/// Result is the interest payment. -fn calc_interest_payment_for_period(apr: I80F48, time_delta: u64, value: I80F48) -> Option { - if apr.is_zero() { - return Some(I80F48::ZERO); - } - - let interest_payment = value - .checked_mul(apr)? - .checked_mul(time_delta.into())? - .checked_div(SECONDS_PER_YEAR)?; - - Some(interest_payment) -} - #[repr(u8)] #[cfg_attr(any(feature = "test", feature = "client"), derive(PartialEq, Eq))] #[derive(Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)] @@ -1188,314 +162,6 @@ pub enum RiskTier { unsafe impl Zeroable for RiskTier {} unsafe impl Pod for RiskTier {} -#[repr(C)] -#[cfg_attr( - any(feature = "test", feature = "client"), - derive(PartialEq, Eq, TypeLayout) -)] -#[derive(AnchorDeserialize, AnchorSerialize, Debug)] -/// TODO: Convert weights to (u64, u64) to avoid precision loss (maybe?) -pub struct BankConfigCompact { - pub asset_weight_init: WrappedI80F48, - pub asset_weight_maint: WrappedI80F48, - - pub liability_weight_init: WrappedI80F48, - pub liability_weight_maint: WrappedI80F48, - - pub deposit_limit: u64, - - pub interest_rate_config: InterestRateConfigCompact, - pub operational_state: BankOperationalState, - - pub oracle_setup: OracleSetup, - pub oracle_key: Pubkey, - - pub borrow_limit: u64, - - pub risk_tier: RiskTier, - - /// Determines what kinds of assets users of this bank can interact with. - /// Options: - /// * ASSET_TAG_DEFAULT (0) - A regular asset that can be comingled with any other regular asset - /// or with `ASSET_TAG_SOL` - /// * ASSET_TAG_SOL (1) - Accounts with a SOL position can comingle with **either** - /// `ASSET_TAG_DEFAULT` or `ASSET_TAG_STAKED` positions, but not both - /// * ASSET_TAG_STAKED (2) - Staked SOL assets. Accounts with a STAKED position can only deposit - /// other STAKED assets or SOL (`ASSET_TAG_SOL`) and can only borrow SOL - pub asset_tag: u8, - - pub _pad0: [u8; 6], - - /// USD denominated limit for calculating asset value for initialization margin requirements. - /// Example, if total SOL deposits are equal to $1M and the limit it set to $500K, - /// then SOL assets will be discounted by 50%. - /// - /// In other words the max value of liabilities that can be backed by the asset is $500K. - /// This is useful for limiting the damage of orcale attacks. - /// - /// Value is UI USD value, for example value 100 -> $100 - pub total_asset_value_init_limit: u64, - - /// Time window in seconds for the oracle price feed to be considered live. - pub oracle_max_age: u16, -} - -impl From for BankConfig { - fn from(config: BankConfigCompact) -> Self { - let keys = [ - config.oracle_key, - Pubkey::default(), - Pubkey::default(), - Pubkey::default(), - Pubkey::default(), - ]; - Self { - asset_weight_init: config.asset_weight_init, - asset_weight_maint: config.asset_weight_maint, - liability_weight_init: config.liability_weight_init, - liability_weight_maint: config.liability_weight_maint, - deposit_limit: config.deposit_limit, - interest_rate_config: config.interest_rate_config.into(), - operational_state: config.operational_state, - oracle_setup: config.oracle_setup, - oracle_keys: keys, - _pad0: [0; 6], - borrow_limit: config.borrow_limit, - risk_tier: config.risk_tier, - asset_tag: config.asset_tag, - _pad1: [0; 6], - total_asset_value_init_limit: config.total_asset_value_init_limit, - oracle_max_age: config.oracle_max_age, - _padding: [0; 38], - } - } -} - -impl From for BankConfigCompact { - fn from(config: BankConfig) -> Self { - Self { - asset_weight_init: config.asset_weight_init, - asset_weight_maint: config.asset_weight_maint, - liability_weight_init: config.liability_weight_init, - liability_weight_maint: config.liability_weight_maint, - deposit_limit: config.deposit_limit, - interest_rate_config: config.interest_rate_config.into(), - operational_state: config.operational_state, - oracle_setup: config.oracle_setup, - oracle_key: config.oracle_keys[0], - borrow_limit: config.borrow_limit, - risk_tier: config.risk_tier, - asset_tag: config.asset_tag, - _pad0: [0; 6], - total_asset_value_init_limit: config.total_asset_value_init_limit, - oracle_max_age: config.oracle_max_age, - } - } -} - -assert_struct_size!(BankConfig, 544); -assert_struct_align!(BankConfig, 8); -#[zero_copy(unsafe)] -#[repr(C)] -#[cfg_attr( - any(feature = "test", feature = "client"), - derive(PartialEq, Eq, TypeLayout) -)] -#[derive(Debug)] -/// TODO: Convert weights to (u64, u64) to avoid precision loss (maybe?) -pub struct BankConfig { - pub asset_weight_init: WrappedI80F48, - pub asset_weight_maint: WrappedI80F48, - - pub liability_weight_init: WrappedI80F48, - pub liability_weight_maint: WrappedI80F48, - - pub deposit_limit: u64, - - pub interest_rate_config: InterestRateConfig, - pub operational_state: BankOperationalState, - - pub oracle_setup: OracleSetup, - pub oracle_keys: [Pubkey; MAX_ORACLE_KEYS], - - // Note: Pubkey is aligned 1, so borrow_limit is the first aligned-8 value after deposit_limit - pub _pad0: [u8; 6], // Bank state (1) + Oracle Setup (1) + 6 = 8 - - pub borrow_limit: u64, - - pub risk_tier: RiskTier, - - /// Determines what kinds of assets users of this bank can interact with. - /// Options: - /// * ASSET_TAG_DEFAULT (0) - A regular asset that can be comingled with any other regular asset - /// or with `ASSET_TAG_SOL` - /// * ASSET_TAG_SOL (1) - Accounts with a SOL position can comingle with **either** - /// `ASSET_TAG_DEFAULT` or `ASSET_TAG_STAKED` positions, but not both - /// * ASSET_TAG_STAKED (2) - Staked SOL assets. Accounts with a STAKED position can only deposit - /// other STAKED assets or SOL (`ASSET_TAG_SOL`) and can only borrow SOL - pub asset_tag: u8, - - pub _pad1: [u8; 6], - - /// USD denominated limit for calculating asset value for initialization margin requirements. - /// Example, if total SOL deposits are equal to $1M and the limit it set to $500K, - /// then SOL assets will be discounted by 50%. - /// - /// In other words the max value of liabilities that can be backed by the asset is $500K. - /// This is useful for limiting the damage of orcale attacks. - /// - /// Value is UI USD value, for example value 100 -> $100 - pub total_asset_value_init_limit: u64, - - /// Time window in seconds for the oracle price feed to be considered live. - pub oracle_max_age: u16, - - // Note: 6 bytes of padding to next 8 byte alignment, then end padding - pub _padding: [u8; 38], -} - -impl Default for BankConfig { - fn default() -> Self { - Self { - asset_weight_init: I80F48::ZERO.into(), - asset_weight_maint: I80F48::ZERO.into(), - liability_weight_init: I80F48::ONE.into(), - liability_weight_maint: I80F48::ONE.into(), - deposit_limit: 0, - borrow_limit: 0, - interest_rate_config: Default::default(), - operational_state: BankOperationalState::Paused, - oracle_setup: OracleSetup::None, - oracle_keys: [Pubkey::default(); MAX_ORACLE_KEYS], - _pad0: [0; 6], - risk_tier: RiskTier::Isolated, - asset_tag: ASSET_TAG_DEFAULT, - _pad1: [0; 6], - total_asset_value_init_limit: TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE, - oracle_max_age: 0, - _padding: [0; 38], - } - } -} - -impl BankConfig { - #[inline] - pub fn get_weights(&self, req_type: RequirementType) -> (I80F48, I80F48) { - match req_type { - RequirementType::Initial => ( - self.asset_weight_init.into(), - self.liability_weight_init.into(), - ), - RequirementType::Maintenance => ( - self.asset_weight_maint.into(), - self.liability_weight_maint.into(), - ), - RequirementType::Equity => (I80F48::ONE, I80F48::ONE), - } - } - - #[inline] - pub fn get_weight( - &self, - requirement_type: RequirementType, - balance_side: BalanceSide, - ) -> I80F48 { - match (requirement_type, balance_side) { - (RequirementType::Initial, BalanceSide::Assets) => self.asset_weight_init.into(), - (RequirementType::Initial, BalanceSide::Liabilities) => { - self.liability_weight_init.into() - } - (RequirementType::Maintenance, BalanceSide::Assets) => self.asset_weight_maint.into(), - (RequirementType::Maintenance, BalanceSide::Liabilities) => { - self.liability_weight_maint.into() - } - (RequirementType::Equity, _) => I80F48::ONE, - } - } - - pub fn validate(&self) -> MarginfiResult { - let asset_init_w = I80F48::from(self.asset_weight_init); - let asset_maint_w = I80F48::from(self.asset_weight_maint); - - check!( - asset_init_w >= I80F48::ZERO && asset_init_w <= I80F48::ONE, - MarginfiError::InvalidConfig - ); - check!(asset_maint_w >= asset_init_w, MarginfiError::InvalidConfig); - - let liab_init_w = I80F48::from(self.liability_weight_init); - let liab_maint_w = I80F48::from(self.liability_weight_maint); - - check!(liab_init_w >= I80F48::ONE, MarginfiError::InvalidConfig); - check!( - liab_maint_w <= liab_init_w && liab_maint_w >= I80F48::ONE, - MarginfiError::InvalidConfig - ); - - self.interest_rate_config.validate()?; - - if self.risk_tier == RiskTier::Isolated { - check!(asset_init_w == I80F48::ZERO, MarginfiError::InvalidConfig); - check!(asset_maint_w == I80F48::ZERO, MarginfiError::InvalidConfig); - } - - Ok(()) - } - - #[inline] - pub fn is_deposit_limit_active(&self) -> bool { - self.deposit_limit != u64::MAX - } - - #[inline] - pub fn is_borrow_limit_active(&self) -> bool { - self.borrow_limit != u64::MAX - } - - /// * lst_mint, stake_pool, sol_pool - required only if configuring - /// `OracleSetup::StakedWithPythPush` on initial setup. If configuring a staked bank after - /// initial setup, can be omitted - pub fn validate_oracle_setup( - &self, - ais: &[AccountInfo], - lst_mint: Option, - stake_pool: Option, - sol_pool: Option, - ) -> MarginfiResult { - check!( - self.oracle_max_age >= ORACLE_MIN_AGE, - MarginfiError::InvalidOracleSetup - ); - OraclePriceFeedAdapter::validate_bank_config(self, ais, lst_mint, stake_pool, sol_pool)?; - Ok(()) - } - - pub fn usd_init_limit_active(&self) -> bool { - self.total_asset_value_init_limit != TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE - } - - #[inline] - pub fn get_oracle_max_age(&self) -> u64 { - match (self.oracle_max_age, self.oracle_setup) { - (0, OracleSetup::SwitchboardV2) => MAX_SWB_ORACLE_AGE, - (0, OracleSetup::PythLegacy | OracleSetup::PythPushOracle) => MAX_PYTH_ORACLE_AGE, - (n, _) => n as u64, - } - } - - pub fn get_pyth_push_oracle_feed_id(&self) -> Option<&FeedId> { - if matches!( - self.oracle_setup, - OracleSetup::PythPushOracle | OracleSetup::StakedWithPythPush - ) { - let bytes: &[u8; 32] = self.oracle_keys[0].as_ref().try_into().unwrap(); - Some(bytes) - } else { - None - } - } -} - #[zero_copy] #[repr(C, align(8))] #[cfg_attr(any(feature = "test", feature = "client"), derive(TypeLayout))] @@ -1532,40 +198,6 @@ impl PartialEq for WrappedI80F48 { impl Eq for WrappedI80F48 {} -#[cfg_attr( - any(feature = "test", feature = "client"), - derive(Clone, PartialEq, Eq, TypeLayout) -)] -#[derive(AnchorDeserialize, AnchorSerialize, Default)] -pub struct BankConfigOpt { - pub asset_weight_init: Option, - pub asset_weight_maint: Option, - - pub liability_weight_init: Option, - pub liability_weight_maint: Option, - - pub deposit_limit: Option, - pub borrow_limit: Option, - - pub operational_state: Option, - - pub oracle: Option, - - pub interest_rate_config: Option, - - pub risk_tier: Option, - - pub asset_tag: Option, - - pub total_asset_value_init_limit: Option, - - pub oracle_max_age: Option, - - pub permissionless_bad_debt_settlement: Option, - - pub freeze_settings: Option, -} - #[cfg_attr( any(feature = "test", feature = "client"), derive(PartialEq, Eq, TypeLayout) @@ -1600,436 +232,3 @@ impl BankVaultType { } } } - -#[macro_export] -macro_rules! assert_eq_with_tolerance { - ($test_val:expr, $val:expr, $tolerance:expr) => { - assert!( - ($test_val - $val).abs() <= $tolerance, - "assertion failed: `({} - {}) <= {}`", - $test_val, - $val, - $tolerance - ); - }; -} - -#[cfg(test)] -mod tests { - use std::time::{SystemTime, UNIX_EPOCH}; - - use crate::constants::{PROTOCOL_FEE_FIXED_DEFAULT, PROTOCOL_FEE_RATE_DEFAULT}; - - use super::*; - use fixed_macro::types::I80F48; - - #[test] - /// Tests that the interest payment for a 1 year period with 100% APR is 1. - fn interest_payment_100apr_1year() { - let apr = I80F48::ONE; - let time_delta = 31_536_000; // 1 year - let value = I80F48::ONE; - - assert_eq_with_tolerance!( - calc_interest_payment_for_period(apr, time_delta, value).unwrap(), - I80F48::ONE, - I80F48!(0.001) - ); - } - - /// Tests that the interest payment for a 1 year period with 50% APR is 0.5. - #[test] - fn interest_payment_50apr_1year() { - let apr = I80F48::from_num(0.5); - let time_delta = 31_536_000; // 1 year - let value = I80F48::ONE; - - assert_eq_with_tolerance!( - calc_interest_payment_for_period(apr, time_delta, value).unwrap(), - I80F48::from_num(0.5), - I80F48!(0.001) - ); - } - /// P: 1_000_000 - /// Apr: 12% - /// Time: 1 second - #[test] - fn interest_payment_12apr_1second() { - let apr = I80F48!(0.12); - let time_delta = 1; - let value = I80F48!(1_000_000); - - assert_eq_with_tolerance!( - calc_interest_payment_for_period(apr, time_delta, value).unwrap(), - I80F48!(0.0038), - I80F48!(0.001) - ); - } - - #[test] - /// apr: 100% - /// time: 1 year - /// principal: 2 - /// expected: 4 - fn accrued_interest_apr100_year1() { - assert_eq_with_tolerance!( - calc_accrued_interest_payment_per_period(I80F48!(1), 31_536_000, I80F48!(2)).unwrap(), - I80F48!(4), - I80F48!(0.001) - ); - } - - #[test] - /// apr: 50% - /// time: 1 year - /// principal: 2 - /// expected: 3 - fn accrued_interest_apr50_year1() { - assert_eq_with_tolerance!( - calc_accrued_interest_payment_per_period(I80F48!(0.5), 31_536_000, I80F48!(2)).unwrap(), - I80F48!(3), - I80F48!(0.001) - ); - } - - #[test] - /// apr: 12% - /// time: 1 second - /// principal: 1_000_000 - /// expected: 1_038 - fn accrued_interest_apr12_year1() { - assert_eq_with_tolerance!( - calc_accrued_interest_payment_per_period(I80F48!(0.12), 1, I80F48!(1_000_000)).unwrap(), - I80F48!(1_000_000.0038), - I80F48!(0.001) - ); - } - - #[test] - /// ur: 0 - /// protocol_fixed_fee: 0.01 - fn ir_config_calc_interest_rate_pff_01() { - let config = InterestRateConfig { - optimal_utilization_rate: I80F48!(0.6).into(), - plateau_interest_rate: I80F48!(0.40).into(), - protocol_fixed_fee_apr: I80F48!(0.01).into(), - ..Default::default() - }; - - let ComputedInterestRates { - lending_rate_apr: lending_apr, - borrowing_rate_apr: borrow_apr, - group_fee_apr: group_fees_apr, - insurance_fee_apr: insurance_apr, - protocol_fee_apr, - } = config - .create_interest_rate_calculator(&MarginfiGroup::default()) - .calc_interest_rate(I80F48!(0.6)) - .unwrap(); - - assert_eq_with_tolerance!(lending_apr, I80F48!(0.24), I80F48!(0.001)); - assert_eq_with_tolerance!(borrow_apr, I80F48!(0.41), I80F48!(0.001)); - assert_eq_with_tolerance!(group_fees_apr, I80F48!(0.01), I80F48!(0.001)); - assert_eq_with_tolerance!(insurance_apr, I80F48!(0), I80F48!(0.001)); - assert_eq_with_tolerance!(protocol_fee_apr, I80F48!(0), I80F48!(0.001)); - } - - #[test] - /// ur: 0.5 - /// protocol_fixed_fee: 0.01 - /// optimal_utilization_rate: 0.5 - /// plateau_interest_rate: 0.4 - fn ir_config_calc_interest_rate_pff_01_ur_05() { - let config = InterestRateConfig { - optimal_utilization_rate: I80F48!(0.5).into(), - plateau_interest_rate: I80F48!(0.4).into(), - protocol_fixed_fee_apr: I80F48!(0.01).into(), - insurance_ir_fee: I80F48!(0.1).into(), - ..Default::default() - }; - - let ComputedInterestRates { - lending_rate_apr: lending_apr, - borrowing_rate_apr: borrow_apr, - group_fee_apr: group_fees_apr, - insurance_fee_apr: insurance_apr, - protocol_fee_apr: _, - } = config - .create_interest_rate_calculator(&MarginfiGroup::default()) - .calc_interest_rate(I80F48!(0.5)) - .unwrap(); - - assert_eq_with_tolerance!(lending_apr, I80F48!(0.2), I80F48!(0.001)); - assert_eq_with_tolerance!(borrow_apr, I80F48!(0.45), I80F48!(0.001)); - assert_eq_with_tolerance!(group_fees_apr, I80F48!(0.01), I80F48!(0.001)); - assert_eq_with_tolerance!(insurance_apr, I80F48!(0.04), I80F48!(0.001)); - } - - #[test] - fn calc_fee_rate_1() { - let rate = I80F48!(0.4); - let fee_ir = I80F48!(0.05); - let fee_fixed = I80F48!(0.01); - - assert_eq!( - calc_fee_rate(rate, fee_ir, fee_fixed).unwrap(), - I80F48!(0.03) - ); - } - - /// ur: 0.8 - /// protocol_fixed_fee: 0.01 - /// optimal_utilization_rate: 0.5 - /// plateau_interest_rate: 0.4 - /// max_interest_rate: 3 - /// insurance_ir_fee: 0.1 - #[test] - fn ir_config_calc_interest_rate_pff_01_ur_08() { - let config = InterestRateConfig { - optimal_utilization_rate: I80F48!(0.4).into(), - plateau_interest_rate: I80F48!(0.4).into(), - protocol_fixed_fee_apr: I80F48!(0.01).into(), - max_interest_rate: I80F48!(3).into(), - insurance_ir_fee: I80F48!(0.1).into(), - ..Default::default() - }; - - let ComputedInterestRates { - lending_rate_apr: lending_apr, - borrowing_rate_apr: borrow_apr, - group_fee_apr: group_fees_apr, - insurance_fee_apr: insurance_apr, - protocol_fee_apr: _, - } = config - .create_interest_rate_calculator(&MarginfiGroup::default()) - .calc_interest_rate(I80F48!(0.7)) - .unwrap(); - - assert_eq_with_tolerance!(lending_apr, I80F48!(1.19), I80F48!(0.001)); - assert_eq_with_tolerance!(borrow_apr, I80F48!(1.88), I80F48!(0.001)); - assert_eq_with_tolerance!(group_fees_apr, I80F48!(0.01), I80F48!(0.001)); - assert_eq_with_tolerance!(insurance_apr, I80F48!(0.17), I80F48!(0.001)); - } - - #[test] - fn ir_accrual_failing_fuzz_test_example() -> anyhow::Result<()> { - let ir_config = InterestRateConfig { - optimal_utilization_rate: I80F48!(0.4).into(), - plateau_interest_rate: I80F48!(0.4).into(), - protocol_fixed_fee_apr: I80F48!(0.01).into(), - max_interest_rate: I80F48!(3).into(), - insurance_ir_fee: I80F48!(0.1).into(), - ..Default::default() - }; - - let current_timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as i64; - - let mut bank = Bank { - asset_share_value: I80F48::ONE.into(), - liability_share_value: I80F48::ONE.into(), - total_liability_shares: I80F48!(207_112_621_602).into(), - total_asset_shares: I80F48!(10_000_000_000_000).into(), - last_update: current_timestamp, - config: BankConfig { - asset_weight_init: I80F48!(0.5).into(), - asset_weight_maint: I80F48!(0.75).into(), - liability_weight_init: I80F48!(1.5).into(), - liability_weight_maint: I80F48!(1.25).into(), - borrow_limit: u64::MAX, - deposit_limit: u64::MAX, - interest_rate_config: ir_config, - ..Default::default() - }, - ..Default::default() - }; - - let pre_net_assets = bank.get_asset_amount(bank.total_asset_shares.into())? - - bank.get_liability_amount(bank.total_liability_shares.into())?; - - let mut clock = Clock::default(); - - clock.unix_timestamp = current_timestamp + 3600; - - bank.accrue_interest( - current_timestamp, - &MarginfiGroup::default(), - #[cfg(not(feature = "client"))] - Pubkey::default(), - ) - .unwrap(); - - let post_collected_fees = I80F48::from(bank.collected_group_fees_outstanding) - + I80F48::from(bank.collected_insurance_fees_outstanding); - - let post_net_assets = bank.get_asset_amount(bank.total_asset_shares.into())? - + post_collected_fees - - bank.get_liability_amount(bank.total_liability_shares.into())?; - - assert_eq_with_tolerance!(pre_net_assets, post_net_assets, I80F48!(1)); - - Ok(()) - } - - #[test] - fn interest_rate_accrual_test_0() -> anyhow::Result<()> { - let ir_config = InterestRateConfig { - optimal_utilization_rate: I80F48!(0.4).into(), - plateau_interest_rate: I80F48!(0.4).into(), - protocol_fixed_fee_apr: I80F48!(0.01).into(), - max_interest_rate: I80F48!(3).into(), - insurance_ir_fee: I80F48!(0.1).into(), - ..Default::default() - }; - - let ur = I80F48!(207_112_621_602) / I80F48!(10_000_000_000_000); - let mut group = MarginfiGroup::default(); - group.group_flags = 1; - group.fee_state_cache.program_fee_fixed = PROTOCOL_FEE_FIXED_DEFAULT.into(); - group.fee_state_cache.program_fee_rate = PROTOCOL_FEE_RATE_DEFAULT.into(); - - let ComputedInterestRates { - lending_rate_apr: lending_apr, - borrowing_rate_apr: borrow_apr, - group_fee_apr, - insurance_fee_apr, - protocol_fee_apr, - } = ir_config - .create_interest_rate_calculator(&group) - .calc_interest_rate(ur) - .expect("interest rate calculation failed"); - - println!("ur: {}", ur); - println!("lending_apr: {}", lending_apr); - println!("borrow_apr: {}", borrow_apr); - println!("group_fee_apr: {}", group_fee_apr); - println!("insurance_fee_apr: {}", insurance_fee_apr); - - assert_eq_with_tolerance!( - borrow_apr, - (lending_apr / ur) + group_fee_apr + insurance_fee_apr + protocol_fee_apr, - I80F48!(0.001) - ); - - Ok(()) - } - - #[test] - fn interest_rate_accrual_test_0_no_protocol_fees() -> anyhow::Result<()> { - let ir_config = InterestRateConfig { - optimal_utilization_rate: I80F48!(0.4).into(), - plateau_interest_rate: I80F48!(0.4).into(), - protocol_fixed_fee_apr: I80F48!(0.01).into(), - max_interest_rate: I80F48!(3).into(), - insurance_ir_fee: I80F48!(0.1).into(), - ..Default::default() - }; - - let ur = I80F48!(207_112_621_602) / I80F48!(10_000_000_000_000); - - let ComputedInterestRates { - lending_rate_apr: lending_apr, - borrowing_rate_apr: borrow_apr, - group_fee_apr, - insurance_fee_apr, - protocol_fee_apr, - } = ir_config - .create_interest_rate_calculator(&MarginfiGroup::default()) - .calc_interest_rate(ur) - .expect("interest rate calculation failed"); - - println!("ur: {}", ur); - println!("lending_apr: {}", lending_apr); - println!("borrow_apr: {}", borrow_apr); - println!("group_fee_apr: {}", group_fee_apr); - println!("insurance_fee_apr: {}", insurance_fee_apr); - - assert!(protocol_fee_apr.is_zero()); - - assert_eq_with_tolerance!( - borrow_apr, - (lending_apr / ur) + group_fee_apr + insurance_fee_apr, - I80F48!(0.001) - ); - - Ok(()) - } - - #[test] - fn test_accruing_interest() -> anyhow::Result<()> { - let ir_config = InterestRateConfig { - optimal_utilization_rate: I80F48!(0.4).into(), - plateau_interest_rate: I80F48!(0.4).into(), - protocol_fixed_fee_apr: I80F48!(0.01).into(), - max_interest_rate: I80F48!(3).into(), - insurance_ir_fee: I80F48!(0.1).into(), - ..Default::default() - }; - - let mut group = MarginfiGroup::default(); - group.group_flags = 1; - group.fee_state_cache.program_fee_fixed = PROTOCOL_FEE_FIXED_DEFAULT.into(); - group.fee_state_cache.program_fee_rate = PROTOCOL_FEE_RATE_DEFAULT.into(); - - let liab_share_value = I80F48!(1.0); - let asset_share_value = I80F48!(1.0); - - let total_liability_shares = I80F48!(207_112_621_602); - let total_asset_shares = I80F48!(10_000_000_000_000); - - let old_total_liability_amount = liab_share_value * total_liability_shares; - let old_total_asset_amount = asset_share_value * total_asset_shares; - - let InterestRateStateChanges { - new_asset_share_value, - new_liability_share_value: new_liab_share_value, - insurance_fees_collected: insurance_collected, - group_fees_collected, - protocol_fees_collected, - } = calc_interest_rate_accrual_state_changes( - 3600, - total_asset_shares, - total_liability_shares, - &ir_config.create_interest_rate_calculator(&group), - asset_share_value, - liab_share_value, - ) - .unwrap(); - - let new_total_liability_amount = total_liability_shares * new_liab_share_value; - let new_total_asset_amount = total_asset_shares * new_asset_share_value; - - println!("new_asset_share_value: {}", new_asset_share_value); - println!("new_liab_share_value: {}", new_liab_share_value); - println!("group_fees_collected: {}", group_fees_collected); - println!("insurance_collected: {}", insurance_collected); - println!("protocol_fees_collected: {}", protocol_fees_collected); - - println!("new_total_liability_amount: {}", new_total_liability_amount); - println!("new_total_asset_amount: {}", new_total_asset_amount); - - println!("old_total_liability_amount: {}", old_total_liability_amount); - println!("old_total_asset_amount: {}", old_total_asset_amount); - - let total_fees_collected = - group_fees_collected + insurance_collected + protocol_fees_collected; - - println!("total_fee_collected: {}", total_fees_collected); - - println!( - "diff: {}", - ((new_total_asset_amount - new_total_liability_amount) + total_fees_collected) - - (old_total_asset_amount - old_total_liability_amount) - ); - - assert_eq_with_tolerance!( - (new_total_asset_amount - new_total_liability_amount) + total_fees_collected, - old_total_asset_amount - old_total_liability_amount, - I80F48::ONE - ); - - Ok(()) - } -} diff --git a/programs/marginfi/src/state/mod.rs b/programs/marginfi/src/state/mod.rs index 1fa883b0f..1dd97193b 100644 --- a/programs/marginfi/src/state/mod.rs +++ b/programs/marginfi/src/state/mod.rs @@ -1,5 +1,8 @@ +pub mod bank; pub mod fee_state; +pub mod interest_rate; pub mod marginfi_account; pub mod marginfi_group; pub mod price; +pub mod risk_engine; pub mod staked_settings; diff --git a/programs/marginfi/src/state/price.rs b/programs/marginfi/src/state/price.rs index f8c1396fd..b4c4bfe85 100644 --- a/programs/marginfi/src/state/price.rs +++ b/programs/marginfi/src/state/price.rs @@ -24,7 +24,7 @@ use crate::{ prelude::*, }; -use super::marginfi_group::BankConfig; +use super::bank::BankConfig; use anchor_lang::prelude::borsh; use pyth_solana_receiver_sdk::PYTH_PUSH_ORACLE_ID; diff --git a/programs/marginfi/src/state/risk_engine.rs b/programs/marginfi/src/state/risk_engine.rs new file mode 100644 index 000000000..e97a0a4e4 --- /dev/null +++ b/programs/marginfi/src/state/risk_engine.rs @@ -0,0 +1,315 @@ +use super::{ + bank::Bank, + marginfi_account::{BalanceSide, BankAccountWithPriceFeed, MarginfiAccount, RequirementType}, + marginfi_group::RiskTier, +}; +use crate::{ + check, + constants::{BANKRUPT_THRESHOLD, ZERO_AMOUNT_THRESHOLD}, + debug, math_error, + prelude::{MarginfiError, MarginfiResult}, +}; +use anchor_lang::prelude::*; +use fixed::types::I80F48; +use std::ops::Not; + +pub const IN_FLASHLOAN_FLAG: u64 = 1 << 1; + +pub enum RiskRequirementType { + Initial, + Maintenance, + Equity, +} + +impl RiskRequirementType { + pub fn to_weight_type(&self) -> RequirementType { + match self { + RiskRequirementType::Initial => RequirementType::Initial, + RiskRequirementType::Maintenance => RequirementType::Maintenance, + RiskRequirementType::Equity => RequirementType::Equity, + } + } +} +pub struct RiskEngine<'a, 'info> { + marginfi_account: &'a MarginfiAccount, + bank_accounts_with_price: Vec>, +} + +impl<'info> RiskEngine<'_, 'info> { + pub fn new<'a>( + marginfi_account: &'a MarginfiAccount, + remaining_ais: &'info [AccountInfo<'info>], + ) -> MarginfiResult> { + check!( + !marginfi_account.get_flag(IN_FLASHLOAN_FLAG), + MarginfiError::AccountInFlashloan + ); + + Self::new_no_flashloan_check(marginfi_account, remaining_ais) + } + + /// Internal constructor used either after manually checking account is not in a flashloan, + /// or explicity checking health for flashloan enabled actions. + fn new_no_flashloan_check<'a>( + marginfi_account: &'a MarginfiAccount, + remaining_ais: &'info [AccountInfo<'info>], + ) -> MarginfiResult> { + let bank_accounts_with_price = + BankAccountWithPriceFeed::load(&marginfi_account.lending_account, remaining_ais)?; + + Ok(RiskEngine { + marginfi_account, + bank_accounts_with_price, + }) + } + + /// Checks account is healthy after performing actions that increase risk (removing liquidity). + /// + /// `IN_FLASHLOAN_FLAG` behavior. + /// - Health check is skipped. + /// - `remaining_ais` can be an empty vec. + pub fn check_account_init_health<'a>( + marginfi_account: &'a MarginfiAccount, + remaining_ais: &'info [AccountInfo<'info>], + ) -> MarginfiResult<()> { + if marginfi_account.get_flag(IN_FLASHLOAN_FLAG) { + return Ok(()); + } + + Self::new_no_flashloan_check(marginfi_account, remaining_ais)? + .check_account_health(RiskRequirementType::Initial)?; + + Ok(()) + } + + /// Returns the total assets and liabilities of the account in the form of (assets, liabilities) + pub fn get_account_health_components( + &self, + requirement_type: RiskRequirementType, + ) -> MarginfiResult<(I80F48, I80F48)> { + let mut total_assets = I80F48::ZERO; + let mut total_liabilities = I80F48::ZERO; + + for a in &self.bank_accounts_with_price { + let (assets, liabilities) = + a.calc_weighted_assets_and_liabilities_values(requirement_type.to_weight_type())?; + + debug!( + "Balance {}, assets: {}, liabilities: {}", + a.balance.bank_pk, assets, liabilities + ); + + total_assets = total_assets.checked_add(assets).ok_or_else(math_error!())?; + total_liabilities = total_liabilities + .checked_add(liabilities) + .ok_or_else(math_error!())?; + } + + Ok((total_assets, total_liabilities)) + } + + pub fn get_account_health( + &'info self, + requirement_type: RiskRequirementType, + ) -> MarginfiResult { + let (total_weighted_assets, total_weighted_liabilities) = + self.get_account_health_components(requirement_type)?; + + Ok(total_weighted_assets + .checked_sub(total_weighted_liabilities) + .ok_or_else(math_error!())?) + } + + fn check_account_health(&self, requirement_type: RiskRequirementType) -> MarginfiResult { + let (total_weighted_assets, total_weighted_liabilities) = + self.get_account_health_components(requirement_type)?; + + debug!( + "check_health: assets {} - liabs: {}", + total_weighted_assets, total_weighted_liabilities + ); + + check!( + total_weighted_assets >= total_weighted_liabilities, + MarginfiError::RiskEngineInitRejected + ); + + self.check_account_risk_tiers()?; + + Ok(()) + } + + /// Checks + /// 1. Account is liquidatable + /// 2. Account has an outstanding liability for the provided liability bank + pub fn check_pre_liquidation_condition_and_get_account_health( + &self, + bank_pk: &Pubkey, + ) -> MarginfiResult { + check!( + !self.marginfi_account.get_flag(IN_FLASHLOAN_FLAG), + MarginfiError::AccountInFlashloan + ); + + let liability_bank_balance = self + .bank_accounts_with_price + .iter() + .find(|a| a.balance.bank_pk == *bank_pk) + .ok_or(MarginfiError::LendingAccountBalanceNotFound)?; + + check!( + liability_bank_balance + .is_empty(BalanceSide::Liabilities) + .not(), + MarginfiError::IllegalLiquidation + ); + + check!( + liability_bank_balance.is_empty(BalanceSide::Assets), + MarginfiError::IllegalLiquidation + ); + + let (assets, liabs) = + self.get_account_health_components(RiskRequirementType::Maintenance)?; + + let account_health = assets.checked_sub(liabs).ok_or_else(math_error!())?; + + debug!( + "pre_liquidation_health: {} ({} - {})", + account_health, assets, liabs + ); + + check!( + account_health <= I80F48::ZERO, + MarginfiError::IllegalLiquidation, + "Account not unhealthy" + ); + + Ok(account_health) + } + + /// Check that the account is at most at the maintenance requirement level post liquidation. + /// This check is used to ensure two things in the liquidation process: + /// 1. We check that the liquidatee's remaining liability is not empty + /// 2. Liquidatee account was below the maintenance requirement level before liquidation (as health can only increase, because liquidations always pay down liabilities) + /// 3. Liquidator didn't liquidate too many assets that would result in unnecessary loss for the liquidatee. + /// + /// This check works on the assumption that the liquidation always results in a reduction of risk. + /// + /// 1. We check that the paid off liability is not zero. Assuming the liquidation always pays off some liability, this ensures that the liquidation was not too large. + /// 2. We check that the account is still at most at the maintenance requirement level. This ensures that the liquidation was not too large overall. + pub fn check_post_liquidation_condition_and_get_account_health( + &self, + bank_pk: &Pubkey, + pre_liquidation_health: I80F48, + ) -> MarginfiResult { + check!( + !self.marginfi_account.get_flag(IN_FLASHLOAN_FLAG), + MarginfiError::AccountInFlashloan + ); + + let liability_bank_balance = self + .bank_accounts_with_price + .iter() + .find(|a| a.balance.bank_pk == *bank_pk) + .unwrap(); + + check!( + liability_bank_balance + .is_empty(BalanceSide::Liabilities) + .not(), + MarginfiError::IllegalLiquidation, + "Liability payoff too severe, exhausted liability" + ); + + check!( + liability_bank_balance.is_empty(BalanceSide::Assets), + MarginfiError::IllegalLiquidation, + "Liability payoff too severe, liability balance has assets" + ); + + let (assets, liabs) = + self.get_account_health_components(RiskRequirementType::Maintenance)?; + + let account_health = assets.checked_sub(liabs).ok_or_else(math_error!())?; + + check!( + account_health <= I80F48::ZERO, + MarginfiError::IllegalLiquidation, + "Liquidation too severe, account above maintenance requirement" + ); + + debug!( + "account_health: {} ({} - {}), pre_liquidation_health: {}", + account_health, assets, liabs, pre_liquidation_health, + ); + + check!( + account_health > pre_liquidation_health, + MarginfiError::IllegalLiquidation, + "Post liquidation health worse" + ); + + Ok(account_health) + } + + /// Check that the account is in a bankrupt state. + /// Account needs to be insolvent and total value of assets need to be below the bankruptcy threshold. + pub fn check_account_bankrupt(&self) -> MarginfiResult { + let (total_assets, total_liabilities) = + self.get_account_health_components(RiskRequirementType::Equity)?; + + check!( + !self.marginfi_account.get_flag(IN_FLASHLOAN_FLAG), + MarginfiError::AccountInFlashloan + ); + + msg!( + "check_bankrupt: assets {} - liabs: {}", + total_assets, + total_liabilities + ); + + check!( + total_assets < total_liabilities, + MarginfiError::AccountNotBankrupt + ); + check!( + total_assets < BANKRUPT_THRESHOLD && total_liabilities > ZERO_AMOUNT_THRESHOLD, + MarginfiError::AccountNotBankrupt + ); + + Ok(()) + } + + fn check_account_risk_tiers<'a>(&'a self) -> MarginfiResult + where + 'info: 'a, + { + let balances_with_liablities = self + .bank_accounts_with_price + .iter() + .filter(|a| a.balance.is_empty(BalanceSide::Liabilities).not()); + + let n_balances_with_liablities = balances_with_liablities.clone().count(); + + let is_in_isolated_risk_tier = balances_with_liablities.clone().any(|a| { + // SAFETY: We are shortening 'info -> 'a + let shorter_bank: &'a AccountInfo<'a> = unsafe { core::mem::transmute(&a.bank) }; + AccountLoader::::try_from(shorter_bank) + .unwrap() + .load() + .unwrap() + .config + .risk_tier + == RiskTier::Isolated + }); + + check!( + !is_in_isolated_risk_tier || n_balances_with_liablities == 1, + MarginfiError::IsolatedAccountIllegalState + ); + + Ok(()) + } +} diff --git a/programs/marginfi/src/utils.rs b/programs/marginfi/src/utils.rs index f9fd9ba0e..484bf647c 100644 --- a/programs/marginfi/src/utils.rs +++ b/programs/marginfi/src/utils.rs @@ -1,10 +1,7 @@ use crate::{ bank_authority_seed, bank_seed, constants::{ASSET_TAG_DEFAULT, ASSET_TAG_SOL, ASSET_TAG_STAKED}, - state::{ - marginfi_account::MarginfiAccount, - marginfi_group::{Bank, BankVaultType}, - }, + state::{bank::Bank, marginfi_account::MarginfiAccount, marginfi_group::BankVaultType}, MarginfiError, MarginfiResult, }; use anchor_lang::prelude::*; diff --git a/programs/marginfi/tests/admin_actions/bankruptcy.rs b/programs/marginfi/tests/admin_actions/bankruptcy.rs index a0de966e4..51cfc55ad 100644 --- a/programs/marginfi/tests/admin_actions/bankruptcy.rs +++ b/programs/marginfi/tests/admin_actions/bankruptcy.rs @@ -6,7 +6,7 @@ use fixed_macro::types::I80F48; use fixtures::{assert_custom_error, assert_eq_noise, native, prelude::*}; use marginfi::{ prelude::{GroupConfig, MarginfiError}, - state::marginfi_group::{BankConfig, BankVaultType}, + state::{bank::BankConfig, marginfi_group::BankVaultType}, }; use pretty_assertions::assert_eq; use solana_program_test::*; diff --git a/programs/marginfi/tests/admin_actions/bankruptcy_auth.rs b/programs/marginfi/tests/admin_actions/bankruptcy_auth.rs index 72a7eecd5..2203ab96d 100644 --- a/programs/marginfi/tests/admin_actions/bankruptcy_auth.rs +++ b/programs/marginfi/tests/admin_actions/bankruptcy_auth.rs @@ -6,8 +6,9 @@ use fixtures::{ use marginfi::{ errors::MarginfiError, state::{ + bank::{BankConfig, BankConfigOpt}, marginfi_account::DISABLED_FLAG, - marginfi_group::{BankConfig, BankConfigOpt, BankVaultType, GroupConfig}, + marginfi_group::{BankVaultType, GroupConfig}, }, }; use solana_program_test::tokio; diff --git a/programs/marginfi/tests/admin_actions/interest_accrual.rs b/programs/marginfi/tests/admin_actions/interest_accrual.rs index fe3cc9324..2d492e646 100644 --- a/programs/marginfi/tests/admin_actions/interest_accrual.rs +++ b/programs/marginfi/tests/admin_actions/interest_accrual.rs @@ -5,7 +5,11 @@ use fixed_macro::types::I80F48; use fixtures::{assert_eq_noise, native, prelude::*}; use marginfi::{ prelude::GroupConfig, - state::marginfi_group::{Bank, BankConfig, BankVaultType, InterestRateConfig}, + state::{ + bank::{Bank, BankConfig}, + interest_rate::InterestRateConfig, + marginfi_group::BankVaultType, + }, }; use pretty_assertions::assert_eq; use solana_program_test::*; diff --git a/programs/marginfi/tests/admin_actions/setup_bank.rs b/programs/marginfi/tests/admin_actions/setup_bank.rs index 8e408c33b..d564bd882 100644 --- a/programs/marginfi/tests/admin_actions/setup_bank.rs +++ b/programs/marginfi/tests/admin_actions/setup_bank.rs @@ -6,7 +6,10 @@ use marginfi::{ FREEZE_SETTINGS, INIT_BANK_ORIGINATION_FEE_DEFAULT, PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG, }, prelude::MarginfiError, - state::marginfi_group::{Bank, BankConfig, BankConfigOpt, BankVaultType}, + state::{ + bank::{Bank, BankConfig, BankConfigOpt}, + marginfi_group::BankVaultType, + }, }; use pretty_assertions::assert_eq; use solana_program_test::*; @@ -317,7 +320,7 @@ async fn configure_bank_success(bank_mint: BankMint) -> anyhow::Result<()> { let old_bank = bank.load().await; let config_bank_opt = BankConfigOpt { - interest_rate_config: Some(marginfi::state::marginfi_group::InterestRateConfigOpt { + interest_rate_config: Some(marginfi::state::interest_rate::InterestRateConfigOpt { optimal_utilization_rate: Some(I80F48::from_num(0.91).into()), plateau_interest_rate: Some(I80F48::from_num(0.44).into()), max_interest_rate: Some(I80F48::from_num(1.44).into()), diff --git a/programs/marginfi/tests/misc/bank_variable_oracle_staleness.rs b/programs/marginfi/tests/misc/bank_variable_oracle_staleness.rs index bba006d14..3f3f05036 100644 --- a/programs/marginfi/tests/misc/bank_variable_oracle_staleness.rs +++ b/programs/marginfi/tests/misc/bank_variable_oracle_staleness.rs @@ -5,7 +5,7 @@ use fixtures::{ PYTH_USDC_FEED, }, }; -use marginfi::{prelude::MarginfiError, state::marginfi_group::BankConfigOpt}; +use marginfi::{prelude::MarginfiError, state::bank::BankConfigOpt}; use solana_program_test::tokio; #[tokio::test] diff --git a/programs/marginfi/tests/misc/collateral_value_cap.rs b/programs/marginfi/tests/misc/collateral_value_cap.rs index 129c07622..8cc12026c 100644 --- a/programs/marginfi/tests/misc/collateral_value_cap.rs +++ b/programs/marginfi/tests/misc/collateral_value_cap.rs @@ -1,7 +1,7 @@ use fixtures::{assert_custom_error, prelude::*}; use marginfi::{ constants::TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE, prelude::MarginfiError, - state::marginfi_group::BankConfigOpt, + state::bank::BankConfigOpt, }; use pretty_assertions::assert_eq; use solana_program_test::*; diff --git a/programs/marginfi/tests/misc/operational_state.rs b/programs/marginfi/tests/misc/operational_state.rs index 88031bbab..01a774a6f 100644 --- a/programs/marginfi/tests/misc/operational_state.rs +++ b/programs/marginfi/tests/misc/operational_state.rs @@ -2,7 +2,10 @@ use fixed_macro::types::I80F48; use fixtures::{assert_custom_error, prelude::*}; use marginfi::{ prelude::{GroupConfig, MarginfiError}, - state::marginfi_group::{BankConfig, BankConfigOpt, BankOperationalState}, + state::{ + bank::{BankConfig, BankConfigOpt}, + marginfi_group::BankOperationalState, + }, }; use pretty_assertions::assert_eq; use solana_program_test::*; diff --git a/programs/marginfi/tests/misc/pyth_push.rs b/programs/marginfi/tests/misc/pyth_push.rs index 0ba0a3574..41ce1dbf2 100644 --- a/programs/marginfi/tests/misc/pyth_push.rs +++ b/programs/marginfi/tests/misc/pyth_push.rs @@ -10,7 +10,10 @@ use fixtures::{ }; use marginfi::{ errors::MarginfiError, - state::marginfi_group::{Bank, BankConfig, BankConfigOpt, BankVaultType, GroupConfig}, + state::{ + bank::{Bank, BankConfig, BankConfigOpt}, + marginfi_group::{BankVaultType, GroupConfig}, + }, }; use solana_program_test::tokio; diff --git a/programs/marginfi/tests/misc/regression.rs b/programs/marginfi/tests/misc/regression.rs index 5742cecc0..dc4ef63a0 100644 --- a/programs/marginfi/tests/misc/regression.rs +++ b/programs/marginfi/tests/misc/regression.rs @@ -7,8 +7,9 @@ use fixed::types::I80F48; use marginfi::{ constants::ASSET_TAG_DEFAULT, state::{ + bank::Bank, marginfi_account::MarginfiAccount, - marginfi_group::{Bank, BankOperationalState, RiskTier}, + marginfi_group::{BankOperationalState, RiskTier}, price::OracleSetup, }, }; diff --git a/programs/marginfi/tests/misc/risk_engine_flexible_oracle_checks.rs b/programs/marginfi/tests/misc/risk_engine_flexible_oracle_checks.rs index 6d27aa33a..48025b5d0 100644 --- a/programs/marginfi/tests/misc/risk_engine_flexible_oracle_checks.rs +++ b/programs/marginfi/tests/misc/risk_engine_flexible_oracle_checks.rs @@ -8,7 +8,10 @@ use fixtures::{ }; use marginfi::{ prelude::MarginfiError, - state::marginfi_group::{BankConfig, BankConfigOpt, BankVaultType, GroupConfig}, + state::{ + bank::{BankConfig, BankConfigOpt}, + marginfi_group::{BankVaultType, GroupConfig}, + }, }; use solana_program_test::tokio; diff --git a/programs/marginfi/tests/misc/token_extensions.rs b/programs/marginfi/tests/misc/token_extensions.rs index af3158596..ced1b6a49 100644 --- a/programs/marginfi/tests/misc/token_extensions.rs +++ b/programs/marginfi/tests/misc/token_extensions.rs @@ -9,8 +9,9 @@ use fixtures::{ test::{BankMint, TestBankSetting, TestFixture, TestSettings, DEFAULT_SOL_TEST_BANK_CONFIG}, ui_to_native, }; -use marginfi::state::marginfi_group::{ - Bank, BankConfig, BankConfigOpt, BankVaultType, GroupConfig, +use marginfi::state::{ + bank::{Bank, BankConfig, BankConfigOpt}, + marginfi_group::{BankVaultType, GroupConfig}, }; use solana_program_test::tokio; use test_case::test_case; diff --git a/programs/marginfi/tests/user_actions/borrow.rs b/programs/marginfi/tests/user_actions/borrow.rs index e905a1360..223fb6df9 100644 --- a/programs/marginfi/tests/user_actions/borrow.rs +++ b/programs/marginfi/tests/user_actions/borrow.rs @@ -6,7 +6,7 @@ use fixtures::{assert_custom_error, native, prelude::*, ui_to_native}; use marginfi::{ assert_eq_with_tolerance, prelude::*, - state::marginfi_group::{BankConfigOpt, BankVaultType}, + state::{bank::BankConfigOpt, marginfi_group::BankVaultType}, }; use pretty_assertions::assert_eq; use solana_program_test::*; diff --git a/programs/marginfi/tests/user_actions/deposit.rs b/programs/marginfi/tests/user_actions/deposit.rs index ac98cb3d7..adbd4b3d4 100644 --- a/programs/marginfi/tests/user_actions/deposit.rs +++ b/programs/marginfi/tests/user_actions/deposit.rs @@ -3,7 +3,7 @@ use anchor_spl::token::spl_token; use fixed::types::I80F48; use fixtures::prelude::*; use fixtures::{assert_custom_error, native}; -use marginfi::state::marginfi_group::{BankConfigOpt, BankVaultType}; +use marginfi::state::{bank::BankConfigOpt, marginfi_group::BankVaultType}; use marginfi::{assert_eq_with_tolerance, prelude::*}; use pretty_assertions::assert_eq; use solana_program_test::*; diff --git a/programs/marginfi/tests/user_actions/liquidate.rs b/programs/marginfi/tests/user_actions/liquidate.rs index 41d60532e..43def1409 100644 --- a/programs/marginfi/tests/user_actions/liquidate.rs +++ b/programs/marginfi/tests/user_actions/liquidate.rs @@ -6,7 +6,10 @@ use fixed_macro::types::I80F48; use fixtures::{assert_custom_error, assert_eq_noise, native, prelude::*}; use marginfi::{ prelude::*, - state::marginfi_group::{Bank, BankConfig, BankConfigOpt, BankVaultType}, + state::{ + bank::{Bank, BankConfig, BankConfigOpt}, + marginfi_group::BankVaultType, + }, }; use pretty_assertions::assert_eq; use solana_program_test::*; diff --git a/programs/marginfi/tests/user_actions/mod.rs b/programs/marginfi/tests/user_actions/mod.rs index f1c67ea06..d614481f0 100644 --- a/programs/marginfi/tests/user_actions/mod.rs +++ b/programs/marginfi/tests/user_actions/mod.rs @@ -17,8 +17,9 @@ use marginfi::{ EMISSIONS_FLAG_BORROW_ACTIVE, EMISSIONS_FLAG_LENDING_ACTIVE, MIN_EMISSIONS_START_TIME, }, prelude::*, - state::marginfi_account::{ - BankAccountWrapper, DISABLED_FLAG, FLASHLOAN_ENABLED_FLAG, IN_FLASHLOAN_FLAG, + state::{ + marginfi_account::{BankAccountWrapper, DISABLED_FLAG, FLASHLOAN_ENABLED_FLAG}, + risk_engine::IN_FLASHLOAN_FLAG, }, }; use pretty_assertions::assert_eq; diff --git a/test-utils/src/bank.rs b/test-utils/src/bank.rs index 9a24b41be..09475a903 100644 --- a/test-utils/src/bank.rs +++ b/test-utils/src/bank.rs @@ -12,7 +12,8 @@ use fixed::types::I80F48; use marginfi::{ bank_authority_seed, state::{ - marginfi_group::{Bank, BankConfigOpt, BankVaultType}, + bank::{Bank, BankConfigOpt}, + marginfi_group::BankVaultType, price::{OraclePriceFeedAdapter, OraclePriceType, PriceAdapter}, }, utils::{find_bank_vault_authority_pda, find_bank_vault_pda}, diff --git a/test-utils/src/marginfi_account.rs b/test-utils/src/marginfi_account.rs index 6eb85468e..df45f5cc5 100644 --- a/test-utils/src/marginfi_account.rs +++ b/test-utils/src/marginfi_account.rs @@ -3,8 +3,7 @@ use crate::ui_to_native; use anchor_lang::{prelude::*, system_program, InstructionData, ToAccountMetas}; use marginfi::state::{ - marginfi_account::MarginfiAccount, - marginfi_group::{Bank, BankVaultType}, + bank::Bank, marginfi_account::MarginfiAccount, marginfi_group::BankVaultType, price::OracleSetup, }; use solana_program::{instruction::Instruction, sysvar}; diff --git a/test-utils/src/marginfi_group.rs b/test-utils/src/marginfi_group.rs index 6b09bea70..5fa164d1e 100644 --- a/test-utils/src/marginfi_group.rs +++ b/test-utils/src/marginfi_group.rs @@ -13,7 +13,10 @@ use marginfi::constants::{ use marginfi::state::fee_state::FeeState; use marginfi::{ prelude::MarginfiGroup, - state::marginfi_group::{BankConfig, BankConfigOpt, BankVaultType, GroupConfig}, + state::{ + bank::{BankConfig, BankConfigOpt}, + marginfi_group::{BankVaultType, GroupConfig}, + }, }; use solana_program::sysvar; use solana_program_test::*; diff --git a/test-utils/src/test.rs b/test-utils/src/test.rs index de3e0003f..a02332dcb 100644 --- a/test-utils/src/test.rs +++ b/test-utils/src/test.rs @@ -14,9 +14,9 @@ use lazy_static::lazy_static; use marginfi::{ constants::MAX_ORACLE_KEYS, state::{ - marginfi_group::{ - BankConfig, BankOperationalState, GroupConfig, InterestRateConfig, RiskTier, - }, + bank::BankConfig, + interest_rate::InterestRateConfig, + marginfi_group::{BankOperationalState, GroupConfig, RiskTier}, price::OracleSetup, }, }; diff --git a/tools/alerting/src/main.rs b/tools/alerting/src/main.rs index 4c2f130d5..68ca5b4f9 100644 --- a/tools/alerting/src/main.rs +++ b/tools/alerting/src/main.rs @@ -8,7 +8,7 @@ use std::{ use log::{error, info, warn}; use marginfi::{ constants::{PYTH_PUSH_MARGINFI_SPONSORED_SHARD_ID, PYTH_PUSH_PYTH_SPONSORED_SHARD_ID}, - state::{marginfi_group::Bank, price::OracleSetup}, + state::{bank::Bank, price::OracleSetup}, }; use pagerduty_rs::{ eventsv2sync::EventsV2, diff --git a/tools/llama-snapshot-tool/src/bin/main.rs b/tools/llama-snapshot-tool/src/bin/main.rs index 0b0f77708..74478ad6a 100644 --- a/tools/llama-snapshot-tool/src/bin/main.rs +++ b/tools/llama-snapshot-tool/src/bin/main.rs @@ -8,7 +8,7 @@ use futures::future::join_all; use lazy_static::lazy_static; use marginfi::{ constants::{EMISSIONS_FLAG_BORROW_ACTIVE, EMISSIONS_FLAG_LENDING_ACTIVE, SECONDS_PER_YEAR}, - state::marginfi_group::{Bank, ComputedInterestRates, MarginfiGroup}, + state::{bank::Bank, interest_rate::ComputedInterestRates, marginfi_group::MarginfiGroup}, }; use reqwest::header::CONTENT_TYPE; use s3::{creds::Credentials, Bucket, Region};