From 8a01852874842b20de18fdcf2009e94e621953ec Mon Sep 17 00:00:00 2001 From: Jon Gurary <91919816+jgur-psyops@users.noreply.github.com> Date: Mon, 21 Oct 2024 10:58:25 -0400 Subject: [PATCH] Combined protocol "program" fees (#244) * Adds protocol fees and pool creation fees. * Adds two new accounts to the add_pool and add_pool_with_seed ixes * The FeeState account (unique per program) now administers "program fees", which go directly to the FeeState.global_fee_wallet's canonical ATA for any given bank's mint. For example, if a third party opens a new group and a new bank, the program fee will always go to get_associated_token_address(FeeState.global_fee_wallet, bank.mint). * Groups will cache the program fees and the fee state's global fee wallet, which the global fee admin can change at any time. When fees update, they will propagate to groups using the permissionless propagate_fee ix. * The global fee state admin can enable/disable program fees for any group without the group admin's permission. * Note that fees which go to the group are still called "protocol" or "group" fees for legacy purposes. The new fees are always referred to as "program" fees. --- clients/rust/marginfi-cli/Cargo.toml | 4 +- clients/rust/marginfi-cli/src/entrypoint.rs | 17 +- .../rust/marginfi-cli/src/processor/admin.rs | 4 +- .../rust/marginfi-cli/src/processor/mod.rs | 53 +- clients/rust/marginfi-cli/src/utils.rs | 6 +- .../dataflow_etls/orm/accounts.py | 16 +- .../dataflow-etls/dataflow_etls/orm/events.py | 16 +- observability/indexer/src/utils/metrics.rs | 27 +- programs/brick/Cargo.toml | 10 +- .../liquidity-incentive-program/Cargo.toml | 1 + programs/marginfi/Cargo.toml | 1 + programs/marginfi/fuzz/Cargo.lock | 128 +++- programs/marginfi/fuzz/Cargo.toml | 2 +- programs/marginfi/fuzz/fuzz_targets/lend.rs | 10 +- programs/marginfi/fuzz/src/account_state.rs | 67 +- programs/marginfi/fuzz/src/lib.rs | 68 +- programs/marginfi/fuzz/src/stubs.rs | 7 +- programs/marginfi/src/constants.rs | 10 + programs/marginfi/src/errors.rs | 2 + .../instructions/marginfi_account/borrow.rs | 2 + .../marginfi_account/close_balance.rs | 2 + .../instructions/marginfi_account/deposit.rs | 2 + .../marginfi_account/liquidate.rs | 4 + .../instructions/marginfi_account/repay.rs | 2 + .../instructions/marginfi_account/withdraw.rs | 2 + .../marginfi_group/accrue_bank_interest.rs | 1 + .../instructions/marginfi_group/add_pool.rs | 208 ++---- .../marginfi_group/add_pool_with_seed.rs | 217 ++++++ .../marginfi_group/collect_bank_fees.rs | 75 +- .../marginfi_group/config_group_fee.rs | 28 + .../marginfi_group/edit_global_fee.rs | 39 ++ .../marginfi_group/handle_bankruptcy.rs | 1 + .../marginfi_group/init_global_fee_state.rs | 55 ++ .../instructions/marginfi_group/initialize.rs | 14 + .../src/instructions/marginfi_group/mod.rs | 10 + .../marginfi_group/propagate_fee_state.rs | 28 + programs/marginfi/src/lib.rs | 50 ++ programs/marginfi/src/state/fee_state.rs | 42 ++ programs/marginfi/src/state/marginfi_group.rs | 649 +++++++++++++----- programs/marginfi/src/state/mod.rs | 1 + .../tests/admin_actions/bankruptcy.rs | 1 + .../tests/admin_actions/bankruptcy_auth.rs | 2 + .../admin_actions/create_marginfi_group.rs | 8 +- .../tests/admin_actions/interest_accrual.rs | 21 + .../tests/admin_actions/setup_bank.rs | 66 +- .../marginfi/tests/misc/operational_state.rs | 5 + programs/marginfi/tests/misc/pyth_push.rs | 3 + .../marginfi/tests/misc/real_oracle_data.rs | 2 + programs/marginfi/tests/misc/regression.rs | 2 +- .../risk_engine_flexible_oracle_checks.rs | 2 + .../marginfi/tests/misc/token_extensions.rs | 1 + .../marginfi/tests/user_actions/liquidate.rs | 3 + programs/mocks/Cargo.toml | 1 + programs/test_transfer_hook/Cargo.toml | 1 + scripts/build-program.sh | 2 +- scripts/build-workspace.sh | 2 +- scripts/single-test.sh | 2 +- test-utils/src/marginfi_group.rs | 119 +++- test-utils/src/spl.rs | 57 +- test-utils/src/test.rs | 34 + tests/01_initGroup.spec.ts | 21 +- tests/03_addBank.spec.ts | 29 +- tests/rootHooks.ts | 40 +- tests/utils/instructions.ts | 66 ++ tests/utils/pdas.ts | 7 + tests/utils/types.ts | 13 +- tools/llama-snapshot-tool/src/bin/main.rs | 41 +- 67 files changed, 1974 insertions(+), 458 deletions(-) create mode 100644 programs/marginfi/src/instructions/marginfi_group/add_pool_with_seed.rs create mode 100644 programs/marginfi/src/instructions/marginfi_group/config_group_fee.rs create mode 100644 programs/marginfi/src/instructions/marginfi_group/edit_global_fee.rs create mode 100644 programs/marginfi/src/instructions/marginfi_group/init_global_fee_state.rs create mode 100644 programs/marginfi/src/instructions/marginfi_group/propagate_fee_state.rs create mode 100644 programs/marginfi/src/state/fee_state.rs diff --git a/clients/rust/marginfi-cli/Cargo.toml b/clients/rust/marginfi-cli/Cargo.toml index 0a0d7f73..01ddeef5 100644 --- a/clients/rust/marginfi-cli/Cargo.toml +++ b/clients/rust/marginfi-cli/Cargo.toml @@ -10,8 +10,10 @@ path = "src/bin/main.rs" [features] devnet = ["marginfi/devnet"] mainnet-beta = ["marginfi/mainnet-beta"] +default = ["mainnet-beta", "admin", "dev", "lip"] +admin = [] +dev = [] staging = ["marginfi/staging"] -default = ["mainnet-beta"] lip = [] [dependencies] diff --git a/clients/rust/marginfi-cli/src/entrypoint.rs b/clients/rust/marginfi-cli/src/entrypoint.rs index b73aa40a..39d08bdd 100644 --- a/clients/rust/marginfi-cli/src/entrypoint.rs +++ b/clients/rust/marginfi-cli/src/entrypoint.rs @@ -86,6 +86,7 @@ pub enum Command { }, } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Parser)] pub enum GroupCommand { Get { @@ -133,9 +134,9 @@ pub enum GroupCommand { #[clap(long)] insurance_ir_fee: f64, #[clap(long)] - protocol_fixed_fee_apr: f64, + group_fixed_fee_apr: f64, #[clap(long)] - protocol_ir_fee: f64, + group_ir_fee: f64, #[clap(long, arg_enum)] risk_tier: RiskTierArg, #[clap(long, arg_enum)] @@ -146,6 +147,8 @@ pub enum GroupCommand { default_value = "60" )] oracle_max_age: u16, + #[clap(long)] + global_fee_wallet: Pubkey, }, HandleBankruptcy { accounts: Vec, @@ -576,13 +579,14 @@ fn group(subcmd: GroupCommand, global_options: &GlobalOptions) -> Result<()> { max_interest_rate, insurance_fee_fixed_apr, insurance_ir_fee, - protocol_fixed_fee_apr, - protocol_ir_fee, + group_fixed_fee_apr, + group_ir_fee, deposit_limit_ui, borrow_limit_ui, risk_tier, oracle_type, oracle_max_age, + global_fee_wallet, } => processor::group_add_bank( config, profile, @@ -602,11 +606,12 @@ fn group(subcmd: GroupCommand, global_options: &GlobalOptions) -> Result<()> { max_interest_rate, insurance_fee_fixed_apr, insurance_ir_fee, - protocol_fixed_fee_apr, - protocol_ir_fee, + group_fixed_fee_apr, + group_ir_fee, risk_tier, oracle_max_age, global_options.compute_unit_price, + global_fee_wallet, ), GroupCommand::HandleBankruptcy { accounts } => { diff --git a/clients/rust/marginfi-cli/src/processor/admin.rs b/clients/rust/marginfi-cli/src/processor/admin.rs index 0b1a9bb1..2c8f741d 100644 --- a/clients/rust/marginfi-cli/src/processor/admin.rs +++ b/clients/rust/marginfi-cli/src/processor/admin.rs @@ -1,6 +1,6 @@ use crate::{ config::Config, - utils::{process_transaction, ui_to_native}, + utils::{find_fee_state_pda, process_transaction, ui_to_native}, }; use anchor_client::anchor_lang::{prelude::*, InstructionData}; use anchor_spl::associated_token; @@ -32,6 +32,8 @@ pub fn process_collect_fees(config: Config, bank_pk: Pubkey) -> Result<()> { liquidity_vault_authority, liquidity_vault: bank.liquidity_vault, insurance_vault: bank.insurance_vault, + fee_state: find_fee_state_pda(&marginfi::id()).0, + fee_ata: find_fee_state_pda(&marginfi::id()).0, // TODO } .to_account_metas(Some(true)), data: marginfi::instruction::LendingPoolCollectBankFees {}.data(), diff --git a/clients/rust/marginfi-cli/src/processor/mod.rs b/clients/rust/marginfi-cli/src/processor/mod.rs index 061c3132..bcfa6f28 100644 --- a/clients/rust/marginfi-cli/src/processor/mod.rs +++ b/clients/rust/marginfi-cli/src/processor/mod.rs @@ -10,8 +10,8 @@ use { utils::{ bank_to_oracle_key, calc_emissions_rate, create_oracle_key_array, find_bank_emssions_auth_pda, find_bank_emssions_token_account_pda, - find_bank_vault_authority_pda, find_bank_vault_pda, load_observation_account_metas, - process_transaction, EXP_10_I80F48, + find_bank_vault_authority_pda, find_bank_vault_pda, find_fee_state_pda, + load_observation_account_metas, process_transaction, EXP_10_I80F48, }, }, anchor_client::{ @@ -180,10 +180,10 @@ Last Update: {:?}h ago ({}) bank.config.interest_rate_config.optimal_utilization_rate, bank.config.interest_rate_config.plateau_interest_rate, bank.config.interest_rate_config.max_interest_rate, - bank.config.interest_rate_config.insurance_ir_fee, bank.config.interest_rate_config.insurance_fee_fixed_apr, - bank.config.interest_rate_config.protocol_ir_fee, + bank.config.interest_rate_config.insurance_ir_fee, bank.config.interest_rate_config.protocol_fixed_fee_apr, + bank.config.interest_rate_config.protocol_ir_fee, bank.config.oracle_setup, bank.config.oracle_keys, bank.config.get_oracle_max_age(), @@ -231,6 +231,7 @@ pub fn group_create( .accounts(marginfi::accounts::MarginfiGroupInitialize { marginfi_group: marginfi_group_keypair.pubkey(), admin, + fee_state: find_fee_state_pda(&marginfi::id()).0, system_program: system_program::id(), }) .args(marginfi::instruction::MarginfiGroupInitialize {}) @@ -312,11 +313,12 @@ pub fn group_add_bank( max_interest_rate: f64, insurance_fee_fixed_apr: f64, insurance_ir_fee: f64, - protocol_fixed_fee_apr: f64, - protocol_ir_fee: f64, + group_fixed_fee_apr: f64, + group_ir_fee: f64, risk_tier: crate::RiskTierArg, oracle_max_age: u16, compute_unit_price: Option, + global_fee_wallet: Pubkey, ) -> Result<()> { let rpc_client = config.mfi_program.rpc(); @@ -334,8 +336,9 @@ pub fn group_add_bank( let max_interest_rate: WrappedI80F48 = I80F48::from_num(max_interest_rate).into(); let insurance_fee_fixed_apr: WrappedI80F48 = I80F48::from_num(insurance_fee_fixed_apr).into(); let insurance_ir_fee: WrappedI80F48 = I80F48::from_num(insurance_ir_fee).into(); - let protocol_fixed_fee_apr: WrappedI80F48 = I80F48::from_num(protocol_fixed_fee_apr).into(); - let protocol_ir_fee: WrappedI80F48 = I80F48::from_num(protocol_ir_fee).into(); + let group_fixed_fee_apr: WrappedI80F48 = I80F48::from_num(group_fixed_fee_apr).into(); + let group_ir_fee: WrappedI80F48 = I80F48::from_num(group_ir_fee).into(); + let mint_account = rpc_client.get_account(&bank_mint)?; let token_program = mint_account.owner; let mint = spl_token_2022::state::Mint::unpack( @@ -350,8 +353,8 @@ pub fn group_add_bank( max_interest_rate, insurance_fee_fixed_apr, insurance_ir_fee, - protocol_fixed_fee_apr, - protocol_ir_fee, + protocol_fixed_fee_apr: group_fixed_fee_apr, + protocol_ir_fee: group_ir_fee, ..InterestRateConfig::default() }; @@ -384,6 +387,7 @@ pub fn group_add_bank( oracle_setup, risk_tier, oracle_max_age, + global_fee_wallet, )? } else { create_bank_ix( @@ -404,6 +408,7 @@ pub fn group_add_bank( oracle_setup, risk_tier, oracle_max_age, + global_fee_wallet, )? }; @@ -445,6 +450,7 @@ fn create_bank_ix_with_seed( oracle_setup: crate::OracleTypeArg, risk_tier: crate::RiskTierArg, oracle_max_age: u16, + global_fee_wallet: Pubkey, ) -> Result> { use solana_sdk::commitment_config::CommitmentConfig; @@ -514,6 +520,8 @@ fn create_bank_ix_with_seed( token_program, system_program: system_program::id(), fee_payer: config.authority(), + fee_state: find_fee_state_pda(&config.program_id).0, + global_fee_wallet, }) .accounts(AccountMeta::new_readonly(oracle_key, false)) .args(marginfi::instruction::LendingPoolAddBankWithSeed { @@ -562,6 +570,7 @@ fn create_bank_ix( oracle_setup: crate::OracleTypeArg, risk_tier: crate::RiskTierArg, oracle_max_age: u16, + global_fee_wallet: Pubkey, ) -> Result> { let add_bank_ixs_builder = config.mfi_program.request(); let add_bank_ixs = add_bank_ixs_builder @@ -610,6 +619,8 @@ fn create_bank_ix( token_program, system_program: system_program::id(), fee_payer: config.explicit_fee_payer(), + fee_state: find_fee_state_pda(&config.program_id).0, + global_fee_wallet, }) .accounts(AccountMeta::new_readonly(oracle_key, false)) .args(marginfi::instruction::LendingPoolAddBank { @@ -988,7 +999,11 @@ pub fn bank_get(config: Config, bank_pk: Option) -> Result<()> { let rpc_client = config.mfi_program.rpc(); if let Some(address) = bank_pk { - let bank: Bank = config.mfi_program.account(address)?; + let mut bank: Bank = config.mfi_program.account(address)?; + let group: MarginfiGroup = config.mfi_program.account(bank.group)?; + + bank.accrue_interest(Clock::get()?.unix_timestamp, &group)?; + print_bank(&address, &bank); let liquidity_vault_balance = @@ -1046,14 +1061,7 @@ fn load_all_banks(config: &Config, marginfi_group: Option) -> Result vec![], }; - let mut clock = config.mfi_program.rpc().get_account(&sysvar::clock::ID)?; - let clock = Clock::from_account_info(&(&sysvar::clock::ID, &mut clock).into_account_info())?; - - let mut banks_with_addresses = config.mfi_program.accounts::(filters)?; - - banks_with_addresses.iter_mut().for_each(|(_, bank)| { - bank.accrue_interest(clock.unix_timestamp).unwrap(); - }); + let banks_with_addresses = config.mfi_program.accounts::(filters)?; Ok(banks_with_addresses) } @@ -2323,6 +2331,8 @@ pub fn marginfi_account_create(profile: &Profile, config: &Config) -> Result<()> #[cfg(feature = "lip")] pub fn process_list_lip_campaigns(config: &Config) { + use liquidity_incentive_program::state::Campaign; + let campaings = config.lip_program.accounts::(vec![]).unwrap(); print!("Found {} campaigns", campaings.len()); @@ -2356,6 +2366,7 @@ Max Rewards: {} #[cfg(feature = "lip")] pub fn process_list_deposits(config: &Config) { + use liquidity_incentive_program::state::{Campaign, Deposit}; use solana_sdk::clock::SECONDS_PER_DAY; let mut deposits = config.lip_program.accounts::(vec![]).unwrap(); @@ -2409,8 +2420,10 @@ Deposit start {}, end {} ({}) #[cfg(feature = "lip")] fn timestamp_to_string(timestamp: i64) -> String { + use chrono::{DateTime, Utc}; + DateTime::::from_naive_utc_and_offset( - NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap(), + DateTime::from_timestamp(timestamp, 0).unwrap().naive_utc(), Utc, ) .format("%Y-%m-%d %H:%M:%S") diff --git a/clients/rust/marginfi-cli/src/utils.rs b/clients/rust/marginfi-cli/src/utils.rs index e41e7547..f936081c 100644 --- a/clients/rust/marginfi-cli/src/utils.rs +++ b/clients/rust/marginfi-cli/src/utils.rs @@ -7,7 +7,7 @@ use { marginfi::{ bank_authority_seed, bank_seed, constants::{ - EMISSIONS_AUTH_SEED, EMISSIONS_TOKEN_ACCOUNT_SEED, MAX_ORACLE_KEYS, + EMISSIONS_AUTH_SEED, EMISSIONS_TOKEN_ACCOUNT_SEED, FEE_STATE_SEED, MAX_ORACLE_KEYS, PYTH_PUSH_PYTH_SPONSORED_SHARD_ID, }, state::{ @@ -126,6 +126,10 @@ pub fn find_bank_emssions_token_account_pda( ) } +pub fn find_fee_state_pda(program_id: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[FEE_STATE_SEED.as_bytes()], program_id) +} + pub fn create_oracle_key_array(oracle_key: Pubkey) -> [Pubkey; MAX_ORACLE_KEYS] { let mut oracle_keys = [Pubkey::default(); MAX_ORACLE_KEYS]; oracle_keys[0] = oracle_key; diff --git a/observability/etl/dataflow-etls/dataflow_etls/orm/accounts.py b/observability/etl/dataflow-etls/dataflow_etls/orm/accounts.py index a4ab8a47..66ffb602 100644 --- a/observability/etl/dataflow-etls/dataflow_etls/orm/accounts.py +++ b/observability/etl/dataflow-etls/dataflow_etls/orm/accounts.py @@ -130,8 +130,8 @@ class LendingPoolBankUpdateRecord(AccountUpdateRecordBase): "config_interest_rate_config_max_interest_rate:BIGNUMERIC", "config_interest_rate_config_insurance_fee_fixed_apr:BIGNUMERIC", "config_interest_rate_config_insurance_ir_fee:BIGNUMERIC", - "config_interest_rate_config_protocol_fixed_fee_apr:BIGNUMERIC", - "config_interest_rate_config_protocol_ir_fee:BIGNUMERIC", + "config_interest_rate_config_group_fixed_fee_apr:BIGNUMERIC", + "config_interest_rate_config_group_ir_fee:BIGNUMERIC", "config_operational_state:STRING", "config_oracle_setup:STRING", "config_oracle_keys:STRING", @@ -169,8 +169,8 @@ class LendingPoolBankUpdateRecord(AccountUpdateRecordBase): config_interest_rate_config_max_interest_rate: float config_interest_rate_config_insurance_fee_fixed_apr: float config_interest_rate_config_insurance_ir_fee: float - config_interest_rate_config_protocol_fixed_fee_apr: float - config_interest_rate_config_protocol_ir_fee: float + config_interest_rate_config_group_fixed_fee_apr: float + config_interest_rate_config_group_ir_fee: float config_operational_state: str config_oracle_setup: str config_oracle_keys: str @@ -222,10 +222,10 @@ def __init__(self, parsed_data: NamedAccountData, account_update: "AccountUpdate parsed_data.data.config.interest_rate_config.insurance_fee_fixed_apr) self.config_interest_rate_config_insurance_ir_fee = wrapped_i80f48_to_float( parsed_data.data.config.interest_rate_config.insurance_ir_fee) - self.config_interest_rate_config_protocol_fixed_fee_apr = wrapped_i80f48_to_float( - parsed_data.data.config.interest_rate_config.protocol_fixed_fee_apr) - self.config_interest_rate_config_protocol_ir_fee = wrapped_i80f48_to_float( - parsed_data.data.config.interest_rate_config.protocol_ir_fee) + self.config_interest_rate_config_group_fixed_fee_apr = wrapped_i80f48_to_float( + parsed_data.data.config.interest_rate_config.group_fixed_fee_apr) + self.config_interest_rate_config_group_ir_fee = wrapped_i80f48_to_float( + parsed_data.data.config.interest_rate_config.group_ir_fee) AccountUpdateRecordTypes = [MarginfiGroupUpdateRecord, diff --git a/observability/etl/dataflow-etls/dataflow_etls/orm/events.py b/observability/etl/dataflow-etls/dataflow_etls/orm/events.py index 115ca4db..1ccc51cb 100644 --- a/observability/etl/dataflow-etls/dataflow_etls/orm/events.py +++ b/observability/etl/dataflow-etls/dataflow_etls/orm/events.py @@ -177,8 +177,8 @@ class LendingPoolBankConfigureRecord(GroupRecordBase): "max_interest_rate:NUMERIC", "insurance_fee_fixed_apr:NUMERIC", "insurance_ir_fee:NUMERIC", - "protocol_fixed_fee_apr:NUMERIC", - "protocol_ir_fee:NUMERIC", + "group_fixed_fee_apr:NUMERIC", + "group_ir_fee:NUMERIC", ] ) @@ -204,8 +204,8 @@ class LendingPoolBankConfigureRecord(GroupRecordBase): insurance_fee_fixed_apr: Optional[float] insurance_ir_fee: Optional[float] - protocol_fixed_fee_apr: Optional[float] - protocol_ir_fee: Optional[float] + group_fixed_fee_apr: Optional[float] + group_ir_fee: Optional[float] def __init__(self, event: Event, instruction: "InstructionWithLogs", instruction_args: NamedInstruction): super().__init__(event, instruction, instruction_args) @@ -238,10 +238,10 @@ def __init__(self, event: Event, instruction: "InstructionWithLogs", instruction event.data.config.interest_rate_config.insurance_fee_fixed_apr, wrapped_i80f48_to_float) self.insurance_ir_fee = map_optional( event.data.config.interest_rate_config.insurance_ir_fee, wrapped_i80f48_to_float) - self.protocol_fixed_fee_apr = map_optional( - event.data.config.interest_rate_config.protocol_fixed_fee_apr, wrapped_i80f48_to_float) - self.protocol_ir_fee = map_optional( - event.data.config.interest_rate_config.protocol_ir_fee, wrapped_i80f48_to_float) + self.group_fixed_fee_apr = map_optional( + event.data.config.interest_rate_config.group_fixed_fee_apr, wrapped_i80f48_to_float) + self.group_ir_fee = map_optional( + event.data.config.interest_rate_config.group_ir_fee, wrapped_i80f48_to_float) @dataclass diff --git a/observability/indexer/src/utils/metrics.rs b/observability/indexer/src/utils/metrics.rs index ee0fe405..6de8dff4 100644 --- a/observability/indexer/src/utils/metrics.rs +++ b/observability/indexer/src/utils/metrics.rs @@ -6,13 +6,13 @@ use chrono::{NaiveDateTime, Utc}; use fixed::types::I80F48; use fixed_macro::types::I80F48; use itertools::Itertools; -use marginfi::constants::ZERO_AMOUNT_THRESHOLD; use marginfi::prelude::MarginfiGroup; use marginfi::state::marginfi_account::{ calc_value, MarginfiAccount, RequirementType, RiskRequirementType, }; use marginfi::state::marginfi_group::BankOperationalState; use marginfi::state::price::{OraclePriceFeedAdapter, OraclePriceType, PriceBias}; +use marginfi::{constants::ZERO_AMOUNT_THRESHOLD, state::marginfi_group::ComputedInterestRates}; use serde::Serialize; use solana_sdk::pubkey::Pubkey; use std::collections::HashMap; @@ -319,10 +319,29 @@ impl LendingPoolBankMetrics { } else { I80F48::ZERO }; - let (lending_apr, borrowing_apr, group_fee_apr, insurance_fee_apr) = bank_accounts + let group = snapshot + .marginfi_groups + .get(&bank_accounts.bank.group) + .ok_or_else(|| { + anyhow!( + "Group {} not found for bank {}", + bank_accounts.bank.group, + bank_pk + ) + })?; + let ir_calc = bank_accounts .bank .config .interest_rate_config + .create_interest_rate_calculator(&group.get_group_bank_config()); + + let ComputedInterestRates { + lending_rate_apr, + borrowing_rate_apr, + group_fee_apr, + insurance_fee_apr, + protocol_fee_apr: _, + }: marginfi::state::marginfi_group::ComputedInterestRates = ir_calc .calc_interest_rate(utilization_rate) .ok_or_else(|| anyhow!("Bad math during IR calcs"))?; @@ -345,8 +364,8 @@ impl LendingPoolBankMetrics { borrow_limit_in_usd: borrow_limit_usd, lenders_count, borrowers_count, - deposit_rate: lending_apr.to_num::(), - borrow_rate: borrowing_apr.to_num::(), + deposit_rate: lending_rate_apr.to_num::(), + borrow_rate: borrowing_rate_apr.to_num::(), group_fee: group_fee_apr.to_num::(), insurance_fee: insurance_fee_apr.to_num::(), total_assets_in_tokens: asset_amount.to_num::() diff --git a/programs/brick/Cargo.toml b/programs/brick/Cargo.toml index 4c395500..c62e5919 100644 --- a/programs/brick/Cargo.toml +++ b/programs/brick/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" [lib] crate-type = ["cdylib", "lib"] -name = "brick" [features] no-entrypoint = [] @@ -16,10 +15,9 @@ cpi = ["no-entrypoint"] default = [] idl-build = ["anchor-lang/idl-build"] test = [] - -[profile.release] -overflow-checks = true +ignore-fee-deploy = [] [dependencies] -solana-program.workspace = true -anchor-lang.workspace = true +# Remove workspace = true if already defined in the root Cargo.toml +anchor-lang = { workspace = true } +solana-program = { workspace = true } diff --git a/programs/liquidity-incentive-program/Cargo.toml b/programs/liquidity-incentive-program/Cargo.toml index a4eec987..88db143a 100644 --- a/programs/liquidity-incentive-program/Cargo.toml +++ b/programs/liquidity-incentive-program/Cargo.toml @@ -19,6 +19,7 @@ devnet = ["marginfi/devnet"] mainnet-beta = ["marginfi/mainnet-beta"] test = [] test-bpf = [] +ignore-fee-deploy = [] [dependencies] anchor-lang = { workspace = true } diff --git a/programs/marginfi/Cargo.toml b/programs/marginfi/Cargo.toml index bd5feb6f..c6ddc147 100644 --- a/programs/marginfi/Cargo.toml +++ b/programs/marginfi/Cargo.toml @@ -22,6 +22,7 @@ devnet = [] mainnet-beta = [] debug = [] staging = [] +ignore-fee-deploy = [] [dependencies] solana-program = { workspace = true } diff --git a/programs/marginfi/fuzz/Cargo.lock b/programs/marginfi/fuzz/Cargo.lock index 8ff842fe..7f6f2c81 100644 --- a/programs/marginfi/fuzz/Cargo.lock +++ b/programs/marginfi/fuzz/Cargo.lock @@ -404,7 +404,7 @@ dependencies = [ "anchor-lang 0.29.0", "solana-program", "spl-associated-token-account 2.3.0", - "spl-token", + "spl-token 4.0.0", "spl-token-2022 0.9.0", ] @@ -416,7 +416,7 @@ dependencies = [ "anchor-lang 0.30.1", "spl-associated-token-account 3.0.2", "spl-pod 0.2.2", - "spl-token", + "spl-token 4.0.0", "spl-token-2022 3.0.2", "spl-token-group-interface 0.2.3", "spl-token-metadata-interface 0.3.3", @@ -2744,6 +2744,7 @@ dependencies = [ name = "marginfi" version = "0.1.0" dependencies = [ + "anchor-lang 0.29.0", "anchor-lang 0.30.1", "anchor-spl 0.30.1", "borsh 0.10.3", @@ -2760,6 +2761,7 @@ dependencies = [ "spl-tlv-account-resolution 0.6.3", "spl-transfer-hook-interface 0.6.3", "static_assertions", + "switchboard-on-demand", "switchboard-solana", "type-layout", ] @@ -2791,7 +2793,7 @@ dependencies = [ "solana-program", "solana-program-test", "solana-sdk", - "spl-token", + "spl-token 4.0.0", "strum 0.26.3", ] @@ -2965,10 +2967,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" dependencies = [ "num-bigint 0.2.6", - "num-complex", + "num-complex 0.2.4", "num-integer", "num-iter", - "num-rational", + "num-rational 0.2.4", + "num-traits", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint 0.4.6", + "num-complex 0.4.6", + "num-integer", + "num-iter", + "num-rational 0.4.2", "num-traits", ] @@ -3003,6 +3019,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3063,6 +3088,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint 0.4.6", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3082,6 +3118,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive 0.5.11", +] + [[package]] name = "num_enum" version = "0.6.1" @@ -3100,6 +3145,18 @@ dependencies = [ "num_enum_derive 0.7.2", ] +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num_enum_derive" version = "0.6.1" @@ -3291,7 +3348,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd23b938276f14057220b707937bcb42fa76dda7560e57a2da30cb52d557937" dependencies = [ - "num", + "num 0.2.1", ] [[package]] @@ -4413,7 +4470,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58267dd2fbaa6dceecba9e3e106d2d90a2b02497c0e8b01b8759beccf5113938" dependencies = [ - "num", + "num 0.2.1", ] [[package]] @@ -4449,7 +4506,7 @@ dependencies = [ "serde_json", "solana-config-program", "solana-sdk", - "spl-token", + "spl-token 4.0.0", "spl-token-2022 1.0.0", "spl-token-group-interface 0.1.0", "spl-token-metadata-interface 0.2.0", @@ -5430,7 +5487,7 @@ dependencies = [ "solana-sdk", "spl-associated-token-account 2.3.0", "spl-memo", - "spl-token", + "spl-token 4.0.0", "spl-token-2022 1.0.0", "thiserror", ] @@ -5602,7 +5659,7 @@ dependencies = [ "num-derive 0.4.2", "num-traits", "solana-program", - "spl-token", + "spl-token 4.0.0", "spl-token-2022 1.0.0", "thiserror", ] @@ -5618,7 +5675,7 @@ dependencies = [ "num-derive 0.4.2", "num-traits", "solana-program", - "spl-token", + "spl-token 4.0.0", "spl-token-2022 3.0.2", "thiserror", ] @@ -5820,6 +5877,21 @@ dependencies = [ "spl-type-length-value 0.4.3", ] +[[package]] +name = "spl-token" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e85e168a785e82564160dcb87b2a8e04cee9bfd1f4d488c729d53d6a4bd300d" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.3.3", + "num-traits", + "num_enum 0.5.11", + "solana-program", + "thiserror", +] + [[package]] name = "spl-token" version = "4.0.0" @@ -5850,7 +5922,7 @@ dependencies = [ "solana-zk-token-sdk", "spl-memo", "spl-pod 0.1.0", - "spl-token", + "spl-token 4.0.0", "spl-token-metadata-interface 0.2.0", "spl-transfer-hook-interface 0.3.0", "spl-type-length-value 0.3.0", @@ -5873,7 +5945,7 @@ dependencies = [ "solana-zk-token-sdk", "spl-memo", "spl-pod 0.1.0", - "spl-token", + "spl-token 4.0.0", "spl-token-group-interface 0.1.0", "spl-token-metadata-interface 0.2.0", "spl-transfer-hook-interface 0.4.1", @@ -5897,7 +5969,7 @@ dependencies = [ "solana-zk-token-sdk", "spl-memo", "spl-pod 0.2.2", - "spl-token", + "spl-token 4.0.0", "spl-token-group-interface 0.2.3", "spl-token-metadata-interface 0.3.3", "spl-transfer-hook-interface 0.6.3", @@ -6206,6 +6278,34 @@ dependencies = [ "sha3 0.10.8", ] +[[package]] +name = "switchboard-on-demand" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852951c42f8876a443060b6882bda945f1621224236ead37959e80f5369cf81" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.21.7", + "bincode", + "borsh 0.10.3", + "bytemuck", + "futures", + "lazy_static", + "libsecp256k1 0.7.1", + "log", + "num 0.4.3", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.8", + "solana-address-lookup-table-program", + "solana-program", + "spl-associated-token-account 2.3.0", + "spl-token 3.5.0", + "switchboard-common", +] + [[package]] name = "switchboard-solana" version = "0.29.109" diff --git a/programs/marginfi/fuzz/Cargo.toml b/programs/marginfi/fuzz/Cargo.toml index 7a2d8256..d6fbab2a 100644 --- a/programs/marginfi/fuzz/Cargo.toml +++ b/programs/marginfi/fuzz/Cargo.toml @@ -48,7 +48,7 @@ capture_log = ["log", "log4rs"] [dependencies.marginfi] path = ".." -features = ["no-entrypoint", "debug", "client"] +features = ["no-entrypoint", "debug", "client", "ignore-fee-deploy"] # Prevent this from interfering with workspaces [workspace] diff --git a/programs/marginfi/fuzz/fuzz_targets/lend.rs b/programs/marginfi/fuzz/fuzz_targets/lend.rs index c0f41d46..a00182ac 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, state::marginfi_group::Bank}; +use marginfi::{assert_eq_with_tolerance, prelude::MarginfiGroup, state::marginfi_group::Bank}; use marginfi_fuzz::{ account_state::AccountsState, arbitrary_helpers::*, metrics::Metrics, MarginfiFuzzContext, }; @@ -145,6 +145,7 @@ fn setup_logging() -> anyhow::Result<()> { } fn verify_end_state<'a>(mga: &'a MarginfiFuzzContext<'a>) -> anyhow::Result<()> { + let group = AccountLoader::::try_from(&mga.marginfi_group).unwrap(); mga.banks.iter().try_for_each(|bank| { let bank_loader = AccountLoader::::try_from(&bank.bank).unwrap(); let mut bank_data = bank_loader.load_mut().unwrap(); @@ -155,10 +156,13 @@ fn verify_end_state<'a>(mga: &'a MarginfiFuzzContext<'a>) -> anyhow::Result<()> clock.unix_timestamp = latest_timestamp as i64 + 3600; - bank_data.accrue_interest(clock.unix_timestamp)?; + bank_data.accrue_interest( + clock.unix_timestamp , + &group.load().unwrap() + )?; let outstanding_fees = I80F48::from(bank_data.collected_group_fees_outstanding) - + I80F48::from(bank_data.collected_insurance_fees_outstanding); + + I80F48::from(bank_data.collected_insurance_fees_outstanding) + I80F48::from(bank_data.collected_program_fees_outstanding); let total_deposits = bank_data.get_asset_amount(bank_data.total_asset_shares.into())?; diff --git a/programs/marginfi/fuzz/src/account_state.rs b/programs/marginfi/fuzz/src/account_state.rs index a3ff6519..35e76e7d 100644 --- a/programs/marginfi/fuzz/src/account_state.rs +++ b/programs/marginfi/fuzz/src/account_state.rs @@ -13,7 +13,10 @@ use anchor_spl::token_2022::spl_token_2022::{ state::Mint, }; use bumpalo::Bump; -use marginfi::{constants::PYTH_ID, state::marginfi_group::BankVaultType}; +use marginfi::{ + constants::{FEE_STATE_SEED, PYTH_ID}, + state::marginfi_group::BankVaultType, +}; use pyth_sdk_solana::state::{ AccountType, PriceInfo, PriceStatus, Rational, SolanaPriceAccount, MAGIC, VERSION_2, }; @@ -39,19 +42,26 @@ impl AccountsState { .alloc(Pubkey::new(transmute_to_bytes(&rand::random::<[u64; 4]>()))) } - pub fn new_sol_account<'bump>(&'bump self, lamports: u64) -> AccountInfo<'bump> { - self.new_sol_account_with_pubkey(self.random_pubkey(), lamports) + pub fn new_sol_account<'bump>( + &'bump self, + lamports: u64, + signer: bool, + writeable: bool, + ) -> AccountInfo<'bump> { + self.new_sol_account_with_pubkey(self.random_pubkey(), lamports, signer, writeable) } pub fn new_sol_account_with_pubkey<'bump>( &'bump self, pubkey: &'bump Pubkey, lamports: u64, + signer: bool, + writeable: bool, ) -> AccountInfo<'bump> { AccountInfo::new( pubkey, - true, - false, + signer, + writeable, self.bump.alloc(lamports), &mut [], &system_program::ID, @@ -60,6 +70,25 @@ impl AccountsState { ) } + pub fn new_fee_state<'a>(&'a self, program_id: Pubkey) -> (AccountInfo<'a>, u8) { + let (fee_state_key, fee_state_bump) = + Pubkey::find_program_address(&[FEE_STATE_SEED.as_bytes()], &marginfi::id()); + + ( + AccountInfo::new( + self.bump.alloc(fee_state_key), + false, + true, + self.bump.alloc(9999999), + self.allocate_dex_owned_account(256 + 8), + self.bump.alloc(program_id), + false, + Epoch::default(), + ), + fee_state_bump, + ) + } + pub fn new_token_mint<'bump>( &'bump self, rent: Rent, @@ -244,6 +273,17 @@ impl AccountsState { ) } + pub fn new_blank_owned_account_with_key( + &self, + key: Pubkey, + owner_pubkey: Pubkey, + ) -> AccountInfo { + self.new_dex_owned_blank_account_with_key( + self.bump.alloc(key), + self.bump.alloc(owner_pubkey), + ) + } + pub fn new_dex_owned_account_with_lamports<'bump>( &'bump self, unpadded_len: usize, @@ -262,6 +302,23 @@ impl AccountsState { ) } + pub fn new_dex_owned_blank_account_with_key<'bump>( + &'bump self, + key: &'bump Pubkey, + program_id: &'bump Pubkey, + ) -> AccountInfo<'bump> { + AccountInfo::new( + key, + false, + true, + self.bump.alloc(0), + &mut [], + program_id, + false, + Epoch::default(), + ) + } + fn allocate_dex_owned_account<'bump>(&'bump self, unpadded_size: usize) -> &mut [u8] { assert_eq!(unpadded_size % 8, 0); let padded_size = unpadded_size + 12; diff --git a/programs/marginfi/fuzz/src/lib.rs b/programs/marginfi/fuzz/src/lib.rs index 1ccc86e4..c550d879 100644 --- a/programs/marginfi/fuzz/src/lib.rs +++ b/programs/marginfi/fuzz/src/lib.rs @@ -18,6 +18,7 @@ use arbitrary_helpers::{ }; use bank_accounts::{get_bank_map, BankAccounts}; use fixed_macro::types::I80F48; +use marginfi::{constants::FEE_STATE_SEED, state::fee_state::FeeState}; use marginfi::{ errors::MarginfiError, instructions::LendingPoolAddBankBumps, @@ -46,6 +47,8 @@ pub mod utils; pub struct MarginfiFuzzContext<'info> { pub marginfi_group: AccountInfo<'info>, + pub fee_state: AccountInfo<'info>, + pub fee_state_wallet: AccountInfo<'info>, pub banks: Vec>, pub marginfi_accounts: Vec>, pub owner: AccountInfo<'info>, @@ -63,13 +66,27 @@ impl<'state> MarginfiFuzzContext<'state> { n_users: u8, ) -> Self { let system_program = state.new_program(system_program::id()); - let admin = state.new_sol_account(1_000_000); + let admin = state.new_sol_account(1_000_000, true, true); + let fee_state_wallet = state.new_sol_account(1_000_000, true, true); let rent_sysvar = state.new_rent_sysvar_account(Rent::free()); - let marginfi_group = - initialize_marginfi_group(state, admin.clone(), system_program.clone()); + let fee_state = initialize_fee_state( + state, + admin.clone(), + fee_state_wallet.clone(), + rent_sysvar.clone(), + system_program.clone(), + ); + let marginfi_group = initialize_marginfi_group( + state, + admin.clone(), + fee_state.clone(), + system_program.clone(), + ); let mut marginfi_state = MarginfiFuzzContext { marginfi_group, + fee_state, + fee_state_wallet, banks: vec![], owner: admin, system_program, @@ -192,6 +209,8 @@ impl<'state> MarginfiFuzzContext<'state> { fee_vault_authority.key, bank.key, ); + let (_fee_state_key, fee_state_bump) = + Pubkey::find_program_address(&[FEE_STATE_SEED.as_bytes()], &marginfi::id()); let oracle = state.new_oracle_account( rent.clone(), @@ -207,6 +226,7 @@ impl<'state> MarginfiFuzzContext<'state> { insurance_vault: insurance_vault_bump, fee_vault_authority: fee_vault_authority_bump, fee_vault: fee_vault_bump, + fee_state: fee_state_bump, }; let token_program = match initial_bank_config.token_type { @@ -225,6 +245,8 @@ impl<'state> MarginfiFuzzContext<'state> { .unwrap(), admin: Signer::try_from(airls(&self.owner)).unwrap(), fee_payer: Signer::try_from(airls(&self.owner)).unwrap(), + fee_state: AccountLoader::try_from(airls(&self.fee_state)).unwrap(), + global_fee_wallet: ails(self.fee_state_wallet.clone()), bank_mint: Box::new(InterfaceAccount::try_from(airls(&mint)).unwrap()), bank: AccountLoader::try_from_unchecked(&marginfi::ID, airls(&bank)) .unwrap(), @@ -924,6 +946,7 @@ pub fn set_discriminator(ai: AccountInfo) { fn initialize_marginfi_group<'a>( state: &'a AccountsState, admin: AccountInfo<'a>, + fee_state: AccountInfo<'a>, system_program: AccountInfo<'a>, ) -> AccountInfo<'a> { let program_id = marginfi::id(); @@ -937,6 +960,7 @@ fn initialize_marginfi_group<'a>( marginfi_group: AccountLoader::try_from_unchecked(&program_id, airls(&marginfi_group)) .unwrap(), admin: Signer::try_from(airls(&admin)).unwrap(), + fee_state: AccountLoader::try_from_unchecked(&program_id, airls(&fee_state)).unwrap(), system_program: Program::try_from(airls(&system_program)).unwrap(), }, &[], @@ -949,6 +973,44 @@ fn initialize_marginfi_group<'a>( marginfi_group } +fn initialize_fee_state<'a>( + state: &'a AccountsState, + admin: AccountInfo<'a>, + wallet: AccountInfo<'a>, + rent: AccountInfo<'a>, + system_program: AccountInfo<'a>, +) -> AccountInfo<'a> { + let program_id = marginfi::id(); + let (fee_state, _fee_state_bump) = state.new_fee_state(program_id); + + marginfi::instructions::marginfi_group::initialize_fee_state( + Context::new( + &marginfi::id(), + &mut marginfi::instructions::InitFeeState { + payer: Signer::try_from(airls(&admin)).unwrap(), + fee_state: AccountLoader::try_from_unchecked(&program_id, airls(&fee_state)) + .unwrap(), + rent: Sysvar::from_account_info(airls(&rent)).unwrap(), + system_program: Program::try_from(airls(&system_program)).unwrap(), + }, + &[], + Default::default(), + ), + admin.key(), + wallet.key(), + // WARN: tests will fail at add_bank::system_program::transfer if this is non-zero because + // the fuzz suite does not yet support the system program. + 0, + I80F48!(0).into(), + I80F48!(0).into(), + ) + .unwrap(); + + set_discriminator::(fee_state.clone()); + + fee_state +} + #[cfg(test)] mod tests { use fixed::types::I80F48; diff --git a/programs/marginfi/fuzz/src/stubs.rs b/programs/marginfi/fuzz/src/stubs.rs index d666d2b6..a099addd 100644 --- a/programs/marginfi/fuzz/src/stubs.rs +++ b/programs/marginfi/fuzz/src/stubs.rs @@ -2,6 +2,7 @@ use anchor_lang::prelude::{AccountInfo, Clock, Pubkey}; use anchor_spl::token_2022::spl_token_2022; use lazy_static::lazy_static; use solana_program::{entrypoint::ProgramResult, instruction::Instruction, program_stubs}; +use solana_sdk::system_program; use crate::log; @@ -73,12 +74,16 @@ impl program_stubs::SyscallStubs for TestSyscallStubs { &new_account_infos, &instruction.data, ) - } else { + } else if instruction.program_id == spl_token_2022::ID { spl_token_2022::processor::Processor::process( &instruction.program_id, &new_account_infos, &instruction.data, ) + } else if instruction.program_id == system_program::ID { + panic!("System program is not yet supported"); + }else{ + panic!("program not supported"); } } diff --git a/programs/marginfi/src/constants.rs b/programs/marginfi/src/constants.rs index e3b67d31..df691a14 100644 --- a/programs/marginfi/src/constants.rs +++ b/programs/marginfi/src/constants.rs @@ -12,6 +12,8 @@ pub const LIQUIDITY_VAULT_SEED: &str = "liquidity_vault"; pub const INSURANCE_VAULT_SEED: &str = "insurance_vault"; pub const FEE_VAULT_SEED: &str = "fee_vault"; +pub const FEE_STATE_SEED: &str = "feestate"; + pub const EMISSIONS_AUTH_SEED: &str = "emissions_auth_seed"; pub const EMISSIONS_TOKEN_ACCOUNT_SEED: &str = "emissions_token_account_seed"; @@ -38,6 +40,9 @@ cfg_if::cfg_if! { pub const LIQUIDATION_LIQUIDATOR_FEE: I80F48 = I80F48!(0.025); pub const LIQUIDATION_INSURANCE_FEE: I80F48 = I80F48!(0.025); +/// The default fee, in native SOL in native decimals (i.e. lamports) used in testing +pub const INIT_BANK_ORIGINATION_FEE_DEFAULT: u32 = 10000; + pub const SECONDS_PER_YEAR: I80F48 = I80F48!(31_536_000); pub const MAX_PYTH_ORACLE_AGE: u64 = 60; @@ -136,6 +141,11 @@ pub const EXP_10: [i128; MAX_EXP_10] = [ /// Value where total_asset_value_init_limit is considered inactive pub const TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE: u64 = 0; +/// For testing, this is a typical program fee. +pub const PROTOCOL_FEE_RATE_DEFAULT: I80F48 = I80F48!(0.025); +/// For testing, this is a typical program fee. +pub const PROTOCOL_FEE_FIXED_DEFAULT: I80F48 = I80F48!(0.01); + pub const MIN_PYTH_PUSH_VERIFICATION_LEVEL: VerificationLevel = VerificationLevel::Full; pub const PYTH_PUSH_PYTH_SPONSORED_SHARD_ID: u16 = 0; pub const PYTH_PUSH_MARGINFI_SPONSORED_SHARD_ID: u16 = 3301; diff --git a/programs/marginfi/src/errors.rs b/programs/marginfi/src/errors.rs index b68837b4..98ff22ab 100644 --- a/programs/marginfi/src/errors.rs +++ b/programs/marginfi/src/errors.rs @@ -96,6 +96,8 @@ pub enum MarginfiError { IllegalAction, #[msg("Token22 Banks require mint account as first remaining account")] // 6047 T22MintRequired, + #[msg("Invalid ATA for global fee account")] // 6048 + InvalidFeeAta, } impl From for ProgramError { diff --git a/programs/marginfi/src/instructions/marginfi_account/borrow.rs b/programs/marginfi/src/instructions/marginfi_account/borrow.rs index 6f6952f7..2fca85e8 100644 --- a/programs/marginfi/src/instructions/marginfi_account/borrow.rs +++ b/programs/marginfi/src/instructions/marginfi_account/borrow.rs @@ -32,6 +32,7 @@ pub fn lending_account_borrow<'info>( token_program, bank_liquidity_vault_authority, bank: bank_loader, + marginfi_group: marginfi_group_loader, .. } = ctx.accounts; let clock = Clock::get()?; @@ -50,6 +51,7 @@ pub fn lending_account_borrow<'info>( bank_loader.load_mut()?.accrue_interest( clock.unix_timestamp, + &*marginfi_group_loader.load()?, #[cfg(not(feature = "client"))] bank_loader.key(), )?; diff --git a/programs/marginfi/src/instructions/marginfi_account/close_balance.rs b/programs/marginfi/src/instructions/marginfi_account/close_balance.rs index 83e71d42..992718d9 100644 --- a/programs/marginfi/src/instructions/marginfi_account/close_balance.rs +++ b/programs/marginfi/src/instructions/marginfi_account/close_balance.rs @@ -13,6 +13,7 @@ pub fn lending_account_close_balance(ctx: Context) - let LendingAccountCloseBalance { marginfi_account, bank: bank_loader, + marginfi_group: marginfi_group_loader, .. } = ctx.accounts; @@ -26,6 +27,7 @@ pub fn lending_account_close_balance(ctx: Context) - bank.accrue_interest( Clock::get()?.unix_timestamp, + &*marginfi_group_loader.load()?, #[cfg(not(feature = "client"))] bank_loader.key(), )?; diff --git a/programs/marginfi/src/instructions/marginfi_account/deposit.rs b/programs/marginfi/src/instructions/marginfi_account/deposit.rs index 5855bd3d..1f8cca93 100644 --- a/programs/marginfi/src/instructions/marginfi_account/deposit.rs +++ b/programs/marginfi/src/instructions/marginfi_account/deposit.rs @@ -32,6 +32,7 @@ pub fn lending_account_deposit<'info>( bank_liquidity_vault, token_program, bank: bank_loader, + marginfi_group: marginfi_group_loader, .. } = ctx.accounts; let clock = Clock::get()?; @@ -51,6 +52,7 @@ pub fn lending_account_deposit<'info>( bank.accrue_interest( clock.unix_timestamp, + &*marginfi_group_loader.load()?, #[cfg(not(feature = "client"))] bank_loader.key(), )?; diff --git a/programs/marginfi/src/instructions/marginfi_account/liquidate.rs b/programs/marginfi/src/instructions/marginfi_account/liquidate.rs index 60c81fbb..aeed8d5d 100644 --- a/programs/marginfi/src/instructions/marginfi_account/liquidate.rs +++ b/programs/marginfi/src/instructions/marginfi_account/liquidate.rs @@ -94,6 +94,7 @@ pub fn lending_account_liquidate<'info>( let LendingAccountLiquidate { liquidator_marginfi_account: liquidator_marginfi_account_loader, liquidatee_marginfi_account: liquidatee_marginfi_account_loader, + marginfi_group: marginfi_group_loader, .. } = ctx.accounts; @@ -108,13 +109,16 @@ pub fn lending_account_liquidate<'info>( ctx.accounts.token_program.key, )?; { + let group = &*marginfi_group_loader.load()?; ctx.accounts.asset_bank.load_mut()?.accrue_interest( current_timestamp, + group, #[cfg(not(feature = "client"))] ctx.accounts.asset_bank.key(), )?; ctx.accounts.liab_bank.load_mut()?.accrue_interest( current_timestamp, + group, #[cfg(not(feature = "client"))] ctx.accounts.liab_bank.key(), )?; diff --git a/programs/marginfi/src/instructions/marginfi_account/repay.rs b/programs/marginfi/src/instructions/marginfi_account/repay.rs index c511f834..3abe9a15 100644 --- a/programs/marginfi/src/instructions/marginfi_account/repay.rs +++ b/programs/marginfi/src/instructions/marginfi_account/repay.rs @@ -32,6 +32,7 @@ pub fn lending_account_repay<'info>( bank_liquidity_vault, token_program, bank: bank_loader, + marginfi_group: marginfi_group_loader, .. } = ctx.accounts; let clock = Clock::get()?; @@ -52,6 +53,7 @@ pub fn lending_account_repay<'info>( bank.accrue_interest( clock.unix_timestamp, + &*marginfi_group_loader.load()?, #[cfg(not(feature = "client"))] bank_loader.key(), )?; diff --git a/programs/marginfi/src/instructions/marginfi_account/withdraw.rs b/programs/marginfi/src/instructions/marginfi_account/withdraw.rs index 651d5013..db42c550 100644 --- a/programs/marginfi/src/instructions/marginfi_account/withdraw.rs +++ b/programs/marginfi/src/instructions/marginfi_account/withdraw.rs @@ -33,6 +33,7 @@ pub fn lending_account_withdraw<'info>( token_program, bank_liquidity_vault_authority, bank: bank_loader, + marginfi_group: marginfi_group_loader, .. } = ctx.accounts; let clock = Clock::get()?; @@ -53,6 +54,7 @@ pub fn lending_account_withdraw<'info>( bank_loader.load_mut()?.accrue_interest( clock.unix_timestamp, + &*marginfi_group_loader.load()?, #[cfg(not(feature = "client"))] bank_loader.key(), )?; 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 9db56836..838681c4 100644 --- a/programs/marginfi/src/instructions/marginfi_group/accrue_bank_interest.rs +++ b/programs/marginfi/src/instructions/marginfi_group/accrue_bank_interest.rs @@ -12,6 +12,7 @@ pub fn lending_pool_accrue_bank_interest( bank.accrue_interest( clock.unix_timestamp, + &*ctx.accounts.marginfi_group.load()?, #[cfg(not(feature = "client"))] ctx.accounts.bank.key(), )?; diff --git a/programs/marginfi/src/instructions/marginfi_group/add_pool.rs b/programs/marginfi/src/instructions/marginfi_group/add_pool.rs index 35895a1f..7b161032 100644 --- a/programs/marginfi/src/instructions/marginfi_group/add_pool.rs +++ b/programs/marginfi/src/instructions/marginfi_group/add_pool.rs @@ -1,10 +1,13 @@ use crate::{ constants::{ - FEE_VAULT_AUTHORITY_SEED, FEE_VAULT_SEED, INSURANCE_VAULT_AUTHORITY_SEED, + FEE_STATE_SEED, FEE_VAULT_AUTHORITY_SEED, FEE_VAULT_SEED, INSURANCE_VAULT_AUTHORITY_SEED, INSURANCE_VAULT_SEED, LIQUIDITY_VAULT_AUTHORITY_SEED, LIQUIDITY_VAULT_SEED, }, events::{GroupEventHeader, LendingPoolBankCreateEvent}, - state::marginfi_group::{Bank, BankConfig, BankConfigCompact, MarginfiGroup}, + state::{ + fee_state::FeeState, + marginfi_group::{Bank, BankConfig, BankConfigCompact, MarginfiGroup}, + }, MarginfiResult, }; use anchor_lang::prelude::*; @@ -19,6 +22,16 @@ pub fn lending_pool_add_bank( ctx: Context, bank_config: BankConfig, ) -> MarginfiResult { + // Transfer the flat sol init fee to the global fee wallet + let fee_state = ctx.accounts.fee_state.load()?; + let bank_init_flat_sol_fee = fee_state.bank_init_flat_sol_fee; + if bank_init_flat_sol_fee > 0 { + anchor_lang::system_program::transfer( + ctx.accounts.transfer_flat_fee(), + bank_init_flat_sol_fee as u64, + )?; + } + let LendingPoolAddBank { bank_mint, liquidity_vault, @@ -69,6 +82,10 @@ pub fn lending_pool_add_bank( Ok(()) } +/* +. Aligns line spacing for easier comparison against with_seed +. +*/ #[derive(Accounts)] #[instruction(bank_config: BankConfigCompact)] pub struct LendingPoolAddBank<'info> { @@ -80,168 +97,21 @@ pub struct LendingPoolAddBank<'info> { )] pub admin: Signer<'info>, + /// Pays to init accounts and pays `fee_state.bank_init_flat_sol_fee` lamports to the protocol #[account(mut)] pub fee_payer: Signer<'info>, - pub bank_mint: Box>, - - #[account( - init, - space = 8 + std::mem::size_of::(), - payer = fee_payer, - )] - pub bank: AccountLoader<'info, Bank>, - - /// CHECK: ⋐ ͡⋄ ω ͡⋄ ⋑ - #[account( - seeds = [ - LIQUIDITY_VAULT_AUTHORITY_SEED.as_bytes(), - bank.key().as_ref(), - ], - bump - )] - pub liquidity_vault_authority: AccountInfo<'info>, - - #[account( - init, - payer = fee_payer, - token::mint = bank_mint, - token::authority = liquidity_vault_authority, - seeds = [ - LIQUIDITY_VAULT_SEED.as_bytes(), - bank.key().as_ref(), - ], - bump, - )] - pub liquidity_vault: Box>, - - /// CHECK: ⋐ ͡⋄ ω ͡⋄ ⋑ - #[account( - seeds = [ - INSURANCE_VAULT_AUTHORITY_SEED.as_bytes(), - bank.key().as_ref(), - ], - bump - )] - pub insurance_vault_authority: AccountInfo<'info>, - + // Note: there is just one FeeState per program, so no further check is required. #[account( - init, - payer = fee_payer, - token::mint = bank_mint, - token::authority = insurance_vault_authority, - seeds = [ - INSURANCE_VAULT_SEED.as_bytes(), - bank.key().as_ref(), - ], + seeds = [FEE_STATE_SEED.as_bytes()], bump, + has_one = global_fee_wallet )] - pub insurance_vault: Box>, - - /// CHECK: ⋐ ͡⋄ ω ͡⋄ ⋑ - #[account( - seeds = [ - FEE_VAULT_AUTHORITY_SEED.as_bytes(), - bank.key().as_ref(), - ], - bump - )] - pub fee_vault_authority: AccountInfo<'info>, - - #[account( - init, - payer = fee_payer, - token::mint = bank_mint, - token::authority = fee_vault_authority, - seeds = [ - FEE_VAULT_SEED.as_bytes(), - bank.key().as_ref(), - ], - bump, - )] - pub fee_vault: Box>, - - pub rent: Sysvar<'info, Rent>, - pub token_program: Interface<'info, TokenInterface>, - pub system_program: Program<'info, System>, -} - -/// A copy of lending_pool_add_bank but with an additional bank seed provided. -/// This seed is used by the LendingPoolAddBankWithSeed.bank to generate a -/// PDA account to sign for newly added bank transactions securely. -/// The previous lending_pool_add_bank is preserved for backwards-compatibility. -pub fn lending_pool_add_bank_with_seed( - ctx: Context, - bank_config: BankConfig, - _bank_seed: u64, -) -> MarginfiResult { - let LendingPoolAddBankWithSeed { - bank_mint, - liquidity_vault, - insurance_vault, - fee_vault, - bank: bank_loader, - .. - } = ctx.accounts; - - let mut bank = bank_loader.load_init()?; - - let liquidity_vault_bump = ctx.bumps.liquidity_vault; - let liquidity_vault_authority_bump = ctx.bumps.liquidity_vault_authority; - let insurance_vault_bump = ctx.bumps.insurance_vault; - let insurance_vault_authority_bump = ctx.bumps.insurance_vault_authority; - let fee_vault_bump = ctx.bumps.fee_vault; - let fee_vault_authority_bump = ctx.bumps.fee_vault_authority; - - *bank = Bank::new( - ctx.accounts.marginfi_group.key(), - bank_config, - bank_mint.key(), - bank_mint.decimals, - liquidity_vault.key(), - insurance_vault.key(), - fee_vault.key(), - Clock::get().unwrap().unix_timestamp, - liquidity_vault_bump, - liquidity_vault_authority_bump, - insurance_vault_bump, - insurance_vault_authority_bump, - fee_vault_bump, - fee_vault_authority_bump, - ); - - bank.config.validate()?; - bank.config.validate_oracle_setup(ctx.remaining_accounts)?; - - emit!(LendingPoolBankCreateEvent { - header: GroupEventHeader { - marginfi_group: ctx.accounts.marginfi_group.key(), - signer: Some(*ctx.accounts.admin.key) - }, - bank: bank_loader.key(), - mint: bank_mint.key(), - }); - - Ok(()) -} - -/// A copy of LendingPoolAddBank but with an additional bank seed provided. -/// This seed is used by the LendingPoolAddBankWithSeed.bank to generate a -/// PDA account to sign for newly added bank transactions securely. -/// The previous LendingPoolAddBank is preserved for backwards-compatibility. -#[derive(Accounts)] -#[instruction(bank_config: BankConfigCompact, bank_seed: u64)] -pub struct LendingPoolAddBankWithSeed<'info> { - pub marginfi_group: AccountLoader<'info, MarginfiGroup>, - - #[account( - mut, - address = marginfi_group.load()?.admin, - )] - pub admin: Signer<'info>, + pub fee_state: AccountLoader<'info, FeeState>, + /// CHECK: The fee admin's native SOL wallet, validated against fee state #[account(mut)] - pub fee_payer: Signer<'info>, + pub global_fee_wallet: AccountInfo<'info>, pub bank_mint: Box>, @@ -249,12 +119,12 @@ pub struct LendingPoolAddBankWithSeed<'info> { init, space = 8 + std::mem::size_of::(), payer = fee_payer, - seeds = [ - marginfi_group.key().as_ref(), - bank_mint.key().as_ref(), - &bank_seed.to_le_bytes(), - ], - bump, + /* + In the "with seed" version of this ix, the seed is defined here + . + . + . + */ )] pub bank: AccountLoader<'info, Bank>, @@ -331,3 +201,17 @@ pub struct LendingPoolAddBankWithSeed<'info> { pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } + +impl<'info> LendingPoolAddBank<'info> { + fn transfer_flat_fee( + &self, + ) -> CpiContext<'_, '_, '_, 'info, anchor_lang::system_program::Transfer<'info>> { + CpiContext::new( + self.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: self.fee_payer.to_account_info(), + to: self.global_fee_wallet.to_account_info(), + }, + ) + } +} 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 new file mode 100644 index 00000000..a0bcee79 --- /dev/null +++ b/programs/marginfi/src/instructions/marginfi_group/add_pool_with_seed.rs @@ -0,0 +1,217 @@ +use crate::{ + constants::{ + FEE_STATE_SEED, FEE_VAULT_AUTHORITY_SEED, FEE_VAULT_SEED, INSURANCE_VAULT_AUTHORITY_SEED, + INSURANCE_VAULT_SEED, LIQUIDITY_VAULT_AUTHORITY_SEED, LIQUIDITY_VAULT_SEED, + }, + events::{GroupEventHeader, LendingPoolBankCreateEvent}, + state::{ + fee_state::FeeState, + marginfi_group::{Bank, BankConfig, BankConfigCompact, MarginfiGroup}, + }, + MarginfiResult, +}; +use anchor_lang::prelude::*; +use anchor_spl::token_interface::*; + +/// A copy of lending_pool_add_bank but with an additional bank seed provided. +/// This seed is used by the LendingPoolAddBankWithSeed.bank to generate a +/// PDA account to sign for newly added bank transactions securely. +/// The previous lending_pool_add_bank is preserved for backwards-compatibility. +pub fn lending_pool_add_bank_with_seed( + ctx: Context, + bank_config: BankConfig, + _bank_seed: u64, +) -> MarginfiResult { + // Transfer the flat sol init fee to the global fee wallet + let fee_state = ctx.accounts.fee_state.load()?; + let bank_init_flat_sol_fee = fee_state.bank_init_flat_sol_fee; + if bank_init_flat_sol_fee > 0 { + anchor_lang::system_program::transfer( + ctx.accounts.transfer_flat_fee(), + bank_init_flat_sol_fee as u64, + )?; + } + + let LendingPoolAddBankWithSeed { + bank_mint, + liquidity_vault, + insurance_vault, + fee_vault, + bank: bank_loader, + .. + } = ctx.accounts; + + let mut bank = bank_loader.load_init()?; + + let liquidity_vault_bump = ctx.bumps.liquidity_vault; + let liquidity_vault_authority_bump = ctx.bumps.liquidity_vault_authority; + let insurance_vault_bump = ctx.bumps.insurance_vault; + let insurance_vault_authority_bump = ctx.bumps.insurance_vault_authority; + let fee_vault_bump = ctx.bumps.fee_vault; + let fee_vault_authority_bump = ctx.bumps.fee_vault_authority; + + *bank = Bank::new( + ctx.accounts.marginfi_group.key(), + bank_config, + bank_mint.key(), + bank_mint.decimals, + liquidity_vault.key(), + insurance_vault.key(), + fee_vault.key(), + Clock::get().unwrap().unix_timestamp, + liquidity_vault_bump, + liquidity_vault_authority_bump, + insurance_vault_bump, + insurance_vault_authority_bump, + fee_vault_bump, + fee_vault_authority_bump, + ); + + bank.config.validate()?; + bank.config.validate_oracle_setup(ctx.remaining_accounts)?; + + emit!(LendingPoolBankCreateEvent { + header: GroupEventHeader { + marginfi_group: ctx.accounts.marginfi_group.key(), + signer: Some(*ctx.accounts.admin.key) + }, + bank: bank_loader.key(), + mint: bank_mint.key(), + }); + + Ok(()) +} + +/// A copy of LendingPoolAddBank but with an additional bank seed provided. +/// This seed is used by the LendingPoolAddBankWithSeed.bank to generate a +/// PDA account to sign for newly added bank transactions securely. +/// The previous LendingPoolAddBank is preserved for backwards-compatibility. +#[derive(Accounts)] +#[instruction(bank_config: BankConfigCompact, bank_seed: u64)] +pub struct LendingPoolAddBankWithSeed<'info> { + pub marginfi_group: AccountLoader<'info, MarginfiGroup>, + + #[account( + mut, + address = marginfi_group.load()?.admin, + )] + pub admin: Signer<'info>, + + /// Pays to init accounts and pays `fee_state.bank_init_flat_sol_fee` lamports to the protocol + #[account(mut)] + pub fee_payer: Signer<'info>, + + // Note: there is just one FeeState per program, so no further check is required. + #[account( + seeds = [FEE_STATE_SEED.as_bytes()], + bump, + has_one = global_fee_wallet + )] + pub fee_state: AccountLoader<'info, FeeState>, + + /// CHECK: The fee admin's native SOL wallet, validated against fee state + #[account(mut)] + pub global_fee_wallet: AccountInfo<'info>, + + pub bank_mint: Box>, + + #[account( + init, + space = 8 + std::mem::size_of::(), + payer = fee_payer, + seeds = [ + marginfi_group.key().as_ref(), + bank_mint.key().as_ref(), + &bank_seed.to_le_bytes(), + ], + bump, + )] + pub bank: AccountLoader<'info, Bank>, + + /// CHECK: ⋐ ͡⋄ ω ͡⋄ ⋑ + #[account( + seeds = [ + LIQUIDITY_VAULT_AUTHORITY_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump + )] + pub liquidity_vault_authority: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + token::mint = bank_mint, + token::authority = liquidity_vault_authority, + seeds = [ + LIQUIDITY_VAULT_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump, + )] + pub liquidity_vault: Box>, + + /// CHECK: ⋐ ͡⋄ ω ͡⋄ ⋑ + #[account( + seeds = [ + INSURANCE_VAULT_AUTHORITY_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump + )] + pub insurance_vault_authority: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + token::mint = bank_mint, + token::authority = insurance_vault_authority, + seeds = [ + INSURANCE_VAULT_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump, + )] + pub insurance_vault: Box>, + + /// CHECK: ⋐ ͡⋄ ω ͡⋄ ⋑ + #[account( + seeds = [ + FEE_VAULT_AUTHORITY_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump + )] + pub fee_vault_authority: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + token::mint = bank_mint, + token::authority = fee_vault_authority, + seeds = [ + FEE_VAULT_SEED.as_bytes(), + bank.key().as_ref(), + ], + bump, + )] + pub fee_vault: Box>, + + pub rent: Sysvar<'info, Rent>, + pub token_program: Interface<'info, TokenInterface>, + pub system_program: Program<'info, System>, +} + +impl<'info> LendingPoolAddBankWithSeed<'info> { + fn transfer_flat_fee( + &self, + ) -> CpiContext<'_, '_, '_, 'info, anchor_lang::system_program::Transfer<'info>> { + CpiContext::new( + self.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: self.fee_payer.to_account_info(), + to: self.global_fee_wallet.to_account_info(), + }, + ) + } +} 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 aec5bb29..dbd366fb 100644 --- a/programs/marginfi/src/instructions/marginfi_group/collect_bank_fees.rs +++ b/programs/marginfi/src/instructions/marginfi_group/collect_bank_fees.rs @@ -1,6 +1,6 @@ -use crate::constants::{FEE_VAULT_AUTHORITY_SEED, INSURANCE_VAULT_AUTHORITY_SEED}; +use crate::constants::{FEE_STATE_SEED, FEE_VAULT_AUTHORITY_SEED, INSURANCE_VAULT_AUTHORITY_SEED}; use crate::events::{GroupEventHeader, LendingPoolBankCollectFeesEvent}; -use crate::utils; +use crate::state::fee_state::FeeState; use crate::{ bank_signer, constants::{ @@ -10,7 +10,9 @@ use crate::{ state::marginfi_group::{Bank, BankVaultType, MarginfiGroup}, MarginfiResult, }; +use crate::{check, utils, MarginfiError}; use anchor_lang::prelude::*; +use anchor_spl::associated_token::get_associated_token_address_with_program_id; use anchor_spl::token_interface::{TokenAccount, TokenInterface}; use fixed::types::I80F48; use std::cmp::min; @@ -18,16 +20,32 @@ use std::cmp::min; pub fn lending_pool_collect_bank_fees<'info>( mut ctx: Context<'_, '_, 'info, 'info, LendingPoolCollectBankFees<'info>>, ) -> MarginfiResult { + let mut bank = ctx.accounts.bank.load_mut()?; + + // Validate the program fee ata is correct + { + let mint = &bank.mint; + let global_fee_wallet = &ctx.accounts.fee_state.load()?.global_fee_wallet; + let token_program_id = &ctx.accounts.token_program.key(); + let program_fee_ata = &ctx.accounts.fee_ata.key(); + let ata_expected = + get_associated_token_address_with_program_id(global_fee_wallet, mint, token_program_id); + check!( + program_fee_ata.eq(&ata_expected), + MarginfiError::InvalidFeeAta + ); + } + let LendingPoolCollectBankFees { liquidity_vault_authority, insurance_vault, fee_vault, token_program, liquidity_vault, + fee_ata, .. } = ctx.accounts; - let mut bank = ctx.accounts.bank.load_mut()?; let maybe_bank_mint = utils::maybe_take_bank_mint(&mut ctx.remaining_accounts, &bank, token_program.key)?; @@ -105,6 +123,44 @@ pub fn lending_pool_collect_bank_fees<'info>( ctx.remaining_accounts, )?; + // Transfer the program fee + let (program_fee_transfer_amount, new_outstanding_program_fees) = { + let outstanding = I80F48::from(bank.collected_program_fees_outstanding); + let transfer_amount = min(outstanding, available_liquidity).int(); + + ( + transfer_amount.int(), + outstanding + .checked_sub(transfer_amount) + .ok_or_else(math_error!())?, + ) + }; + + available_liquidity = available_liquidity + .checked_sub(program_fee_transfer_amount) + .ok_or_else(math_error!())?; + + assert!(available_liquidity >= I80F48::ZERO); + + bank.collected_program_fees_outstanding = new_outstanding_program_fees.into(); + + bank.withdraw_spl_transfer( + program_fee_transfer_amount + .checked_to_num() + .ok_or_else(math_error!())?, + liquidity_vault.to_account_info(), + fee_ata.to_account_info(), + liquidity_vault_authority.to_account_info(), + maybe_bank_mint.as_ref(), + token_program.to_account_info(), + bank_signer!( + BankVaultType::Liquidity, + ctx.accounts.bank.key(), + bank.liquidity_vault_authority_bump + ), + ctx.remaining_accounts, + )?; + emit!(LendingPoolBankCollectFeesEvent { header: GroupEventHeader { marginfi_group: ctx.accounts.marginfi_group.key(), @@ -174,6 +230,19 @@ pub struct LendingPoolCollectBankFees<'info> { )] pub fee_vault: AccountInfo<'info>, + // Note: there is just one FeeState per program, so no further check is required. + #[account( + seeds = [FEE_STATE_SEED.as_bytes()], + bump, + )] + pub fee_state: AccountLoader<'info, FeeState>, + + /// CHECK: Cannonical ATA of the `FeeState.global_fee_wallet` for the mint used by this bank + /// (validated in handler). Must already exist, may require initializing the ATA if it does not + /// already exist prior to this ix. + #[account(mut)] + pub fee_ata: InterfaceAccount<'info, TokenAccount>, + pub token_program: Interface<'info, TokenInterface>, } diff --git a/programs/marginfi/src/instructions/marginfi_group/config_group_fee.rs b/programs/marginfi/src/instructions/marginfi_group/config_group_fee.rs new file mode 100644 index 00000000..303f3cac --- /dev/null +++ b/programs/marginfi/src/instructions/marginfi_group/config_group_fee.rs @@ -0,0 +1,28 @@ +use anchor_lang::prelude::*; + +use crate::{constants::FEE_STATE_SEED, state::fee_state::FeeState, MarginfiGroup, MarginfiResult}; + +#[derive(Accounts)] +pub struct ConfigGroupFee<'info> { + #[account(mut)] + pub marginfi_group: AccountLoader<'info, MarginfiGroup>, + + /// `global_fee_admin` of the FeeState + pub global_fee_admin: Signer<'info>, + + // Note: there is just one FeeState per program, so no further check is required. + #[account( + seeds = [FEE_STATE_SEED.as_bytes()], + bump, + has_one = global_fee_admin + )] + pub fee_state: AccountLoader<'info, FeeState>, +} + +pub fn config_group_fee(ctx: Context, flag: u64) -> MarginfiResult { + let mut marginfi_group = ctx.accounts.marginfi_group.load_mut()?; + + marginfi_group.set_flags(flag)?; + + Ok(()) +} diff --git a/programs/marginfi/src/instructions/marginfi_group/edit_global_fee.rs b/programs/marginfi/src/instructions/marginfi_group/edit_global_fee.rs new file mode 100644 index 00000000..e538bb51 --- /dev/null +++ b/programs/marginfi/src/instructions/marginfi_group/edit_global_fee.rs @@ -0,0 +1,39 @@ +// Global fee admin calls this to edit the fee rate or the fee wallet. + +use crate::constants::FEE_STATE_SEED; +use crate::state::fee_state; +use crate::state::marginfi_group::WrappedI80F48; +use anchor_lang::prelude::*; +use fee_state::FeeState; + +pub fn edit_fee_state( + ctx: Context, + fee_wallet: Pubkey, + bank_init_flat_sol_fee: u32, + program_fee_fixed: WrappedI80F48, + program_fee_rate: WrappedI80F48, +) -> Result<()> { + let mut fee_state = ctx.accounts.fee_state.load_mut()?; + fee_state.global_fee_wallet = fee_wallet; + fee_state.bank_init_flat_sol_fee = bank_init_flat_sol_fee; + fee_state.program_fee_fixed = program_fee_fixed; + fee_state.program_fee_rate = program_fee_rate; + + Ok(()) +} + +#[derive(Accounts)] +pub struct EditFeeState<'info> { + /// Admin of the global FeeState + #[account(mut)] + pub global_fee_admin: Signer<'info>, + + // Note: there is just one FeeState per program, so no further check is required. + #[account( + mut, + seeds = [FEE_STATE_SEED.as_bytes()], + bump, + has_one = global_fee_admin + )] + pub fee_state: AccountLoader<'info, FeeState>, +} diff --git a/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs b/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs index accdc428..5a94e42e 100644 --- a/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs +++ b/programs/marginfi/src/instructions/marginfi_group/handle_bankruptcy.rs @@ -59,6 +59,7 @@ pub fn lending_pool_handle_bankruptcy<'info>( bank.accrue_interest( clock.unix_timestamp, + &*marginfi_group_loader.load()?, #[cfg(not(feature = "client"))] bank_loader.key(), )?; diff --git a/programs/marginfi/src/instructions/marginfi_group/init_global_fee_state.rs b/programs/marginfi/src/instructions/marginfi_group/init_global_fee_state.rs new file mode 100644 index 00000000..11476b89 --- /dev/null +++ b/programs/marginfi/src/instructions/marginfi_group/init_global_fee_state.rs @@ -0,0 +1,55 @@ +// Runs once per program to init the global fee state. +use crate::constants::FEE_STATE_SEED; +use crate::state::fee_state; +use crate::state::marginfi_group::WrappedI80F48; +use anchor_lang::prelude::*; +use fee_state::FeeState; + +#[allow(unused_variables)] +pub fn initialize_fee_state( + ctx: Context, + admin_key: Pubkey, + fee_wallet: Pubkey, + bank_init_flat_sol_fee: u32, + program_fee_fixed: WrappedI80F48, + program_fee_rate: WrappedI80F48, +) -> Result<()> { + let mut fee_state = ctx.accounts.fee_state.load_init()?; + cfg_if::cfg_if! { + if #[cfg(all(feature = "mainnet-beta", not(feature = "ignore-fee-deploy")))] { + if ctx.accounts.payer.key != &pubkey!("3HGdGLrnK9DsnHi1mCrUMLGfQHcu6xUrXhMY14GYjqvM") { + panic!("The mrgn program multisig must sign on mainnet."); + } + } + } + fee_state.global_fee_admin = admin_key; + fee_state.global_fee_wallet = fee_wallet; + fee_state.key = ctx.accounts.fee_state.key(); + fee_state.bank_init_flat_sol_fee = bank_init_flat_sol_fee; + fee_state.bump_seed = ctx.bumps.fee_state; + fee_state.program_fee_fixed = program_fee_fixed; + fee_state.program_fee_rate = program_fee_rate; + + Ok(()) +} + +#[derive(Accounts)] +pub struct InitFeeState<'info> { + /// Pays the init fee + #[account(mut)] + pub payer: Signer<'info>, + + #[account( + init, + seeds = [ + FEE_STATE_SEED.as_bytes() + ], + bump, + payer = payer, + space = 8 + FeeState::LEN, + )] + pub fee_state: AccountLoader<'info, FeeState>, + + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} diff --git a/programs/marginfi/src/instructions/marginfi_group/initialize.rs b/programs/marginfi/src/instructions/marginfi_group/initialize.rs index 2399e3ff..4b70804e 100644 --- a/programs/marginfi/src/instructions/marginfi_group/initialize.rs +++ b/programs/marginfi/src/instructions/marginfi_group/initialize.rs @@ -1,4 +1,6 @@ +use crate::constants::FEE_STATE_SEED; use crate::events::{GroupEventHeader, MarginfiGroupCreateEvent}; +use crate::state::fee_state::FeeState; use crate::{state::marginfi_group::MarginfiGroup, MarginfiResult}; use anchor_lang::prelude::*; @@ -7,6 +9,12 @@ pub fn initialize_group(ctx: Context) -> MarginfiResult marginfi_group.set_initial_configuration(ctx.accounts.admin.key()); + let fee_state = ctx.accounts.fee_state.load()?; + + marginfi_group.fee_state_cache.global_fee_wallet = fee_state.global_fee_wallet; + marginfi_group.fee_state_cache.program_fee_fixed = fee_state.program_fee_fixed; + marginfi_group.fee_state_cache.program_fee_rate = fee_state.program_fee_rate; + emit!(MarginfiGroupCreateEvent { header: GroupEventHeader { marginfi_group: ctx.accounts.marginfi_group.key(), @@ -29,5 +37,11 @@ pub struct MarginfiGroupInitialize<'info> { #[account(mut)] pub admin: Signer<'info>, + #[account( + seeds = [FEE_STATE_SEED.as_bytes()], + bump, + )] + pub fee_state: AccountLoader<'info, FeeState>, + pub system_program: Program<'info, System>, } diff --git a/programs/marginfi/src/instructions/marginfi_group/mod.rs b/programs/marginfi/src/instructions/marginfi_group/mod.rs index 33bc6a91..47ae9d15 100644 --- a/programs/marginfi/src/instructions/marginfi_group/mod.rs +++ b/programs/marginfi/src/instructions/marginfi_group/mod.rs @@ -1,15 +1,25 @@ mod accrue_bank_interest; mod add_pool; +mod add_pool_with_seed; mod collect_bank_fees; +mod config_group_fee; mod configure; mod configure_bank; +mod edit_global_fee; mod handle_bankruptcy; +mod init_global_fee_state; mod initialize; +mod propagate_fee_state; pub use accrue_bank_interest::*; pub use add_pool::*; +pub use add_pool_with_seed::*; pub use collect_bank_fees::*; +pub use config_group_fee::*; pub use configure::*; pub use configure_bank::*; +pub use edit_global_fee::*; pub use handle_bankruptcy::*; +pub use init_global_fee_state::*; pub use initialize::*; +pub use propagate_fee_state::*; diff --git a/programs/marginfi/src/instructions/marginfi_group/propagate_fee_state.rs b/programs/marginfi/src/instructions/marginfi_group/propagate_fee_state.rs new file mode 100644 index 00000000..3b71e369 --- /dev/null +++ b/programs/marginfi/src/instructions/marginfi_group/propagate_fee_state.rs @@ -0,0 +1,28 @@ +use anchor_lang::prelude::*; + +use crate::{constants::FEE_STATE_SEED, state::fee_state::FeeState, MarginfiGroup}; + +#[derive(Accounts)] +pub struct PropagateFee<'info> { + // Note: there is just one FeeState per program, so no further check is required. + #[account( + seeds = [FEE_STATE_SEED.as_bytes()], + bump, + )] + pub fee_state: AccountLoader<'info, FeeState>, + + /// Any group, this ix is permisionless and can propogate the fee to any group + #[account(mut)] + pub marginfi_group: AccountLoader<'info, MarginfiGroup>, +} + +pub fn propagate_fee(ctx: Context) -> Result<()> { + let mut group = ctx.accounts.marginfi_group.load_mut()?; + let fee_state = ctx.accounts.fee_state.load()?; + + group.fee_state_cache.global_fee_wallet = fee_state.global_fee_wallet; + group.fee_state_cache.program_fee_fixed = fee_state.program_fee_fixed; + group.fee_state_cache.program_fee_rate = fee_state.program_fee_rate; + + Ok(()) +} diff --git a/programs/marginfi/src/lib.rs b/programs/marginfi/src/lib.rs index d9dbd9f7..5f3a76df 100644 --- a/programs/marginfi/src/lib.rs +++ b/programs/marginfi/src/lib.rs @@ -10,6 +10,7 @@ pub mod utils; use anchor_lang::prelude::*; use instructions::*; use prelude::*; +use state::marginfi_group::WrappedI80F48; use state::marginfi_group::{BankConfigCompact, BankConfigOpt}; cfg_if::cfg_if! { @@ -214,6 +215,55 @@ pub mod marginfi { pub fn marginfi_account_close(ctx: Context) -> MarginfiResult { marginfi_account::close_account(ctx) } + + /// (Runs once per program) Configures the fee state account, where the global admin sets fees + /// that are assessed to the protocol + pub fn init_global_fee_state( + ctx: Context, + admin: Pubkey, + fee_wallet: Pubkey, + bank_init_flat_sol_fee: u32, + program_fee_fixed: WrappedI80F48, + program_fee_rate: WrappedI80F48, + ) -> MarginfiResult { + marginfi_group::initialize_fee_state( + ctx, + admin, + fee_wallet, + bank_init_flat_sol_fee, + program_fee_fixed, + program_fee_rate, + ) + } + + /// (global fee admin only) Adjust fees or the destination wallet + pub fn edit_global_fee_state( + ctx: Context, + fee_wallet: Pubkey, + bank_init_flat_sol_fee: u32, + program_fee_fixed: WrappedI80F48, + program_fee_rate: WrappedI80F48, + ) -> MarginfiResult { + marginfi_group::edit_fee_state( + ctx, + fee_wallet, + bank_init_flat_sol_fee, + program_fee_fixed, + program_fee_rate, + ) + } + + /// (Permissionless) Force any group to adopt the current FeeState settings + pub fn propagate_fee_state(ctx: Context) -> MarginfiResult { + marginfi_group::propagate_fee(ctx) + } + + /// (global fee admin only) Enable or disable program fees for any group. Does not require the + /// group admin to sign: the global fee state admin can turn program fees on or off for any + /// group + pub fn config_group_fee(ctx: Context, flag: u64) -> MarginfiResult { + marginfi_group::config_group_fee(ctx, flag) + } } #[cfg(not(feature = "no-entrypoint"))] diff --git a/programs/marginfi/src/state/fee_state.rs b/programs/marginfi/src/state/fee_state.rs new file mode 100644 index 00000000..0d7f6142 --- /dev/null +++ b/programs/marginfi/src/state/fee_state.rs @@ -0,0 +1,42 @@ +use anchor_lang::prelude::*; + +use crate::{assert_struct_align, assert_struct_size}; + +use super::marginfi_group::WrappedI80F48; + +assert_struct_size!(FeeState, 256); +assert_struct_align!(FeeState, 8); + +/// Unique per-program. The Program Owner uses this account to administrate fees collected by the protocol +#[account(zero_copy)] +#[repr(C)] +pub struct FeeState { + /// The fee state's own key. A PDA derived from just `b"feestate"` + pub key: Pubkey, + /// Can modify fees + pub global_fee_admin: Pubkey, + /// The base wallet for all protocol fees. All SOL fees go to this wallet. All non-SOL fees go + /// to the cannonical ATA of this wallet for that asset. + pub global_fee_wallet: Pubkey, + // Reserved for future use, forces 8-byte alignment + pub placeholder0: u64, + /// Flat fee assessed when a new bank is initialized, in lamports. + /// * In SOL, in native decimals. + pub bank_init_flat_sol_fee: u32, + pub bump_seed: u8, + // Pad to next 8-byte multiple + _padding0: [u8; 4], + // Pad to 128 bytes + _padding1: [u8; 15], + /// Fee collected by the program owner from all groups + pub program_fee_fixed: WrappedI80F48, + /// Fee collected by the program owner from all groups + pub program_fee_rate: WrappedI80F48, + // Reserved for future use + _reserved0: [u8; 32], + _reserved1: [u8; 64], +} + +impl FeeState { + pub const LEN: usize = std::mem::size_of::(); +} diff --git a/programs/marginfi/src/state/marginfi_group.rs b/programs/marginfi/src/state/marginfi_group.rs index ead16d9d..f7a55392 100644 --- a/programs/marginfi/src/state/marginfi_group.rs +++ b/programs/marginfi/src/state/marginfi_group.rs @@ -23,6 +23,7 @@ use crate::{ 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; @@ -36,6 +37,7 @@ use std::{ #[cfg(any(feature = "test", feature = "client"))] use type_layout::TypeLayout; +assert_struct_size!(MarginfiGroup, 1056); #[account(zero_copy)] #[cfg_attr( any(feature = "test", feature = "client"), @@ -44,11 +46,34 @@ use type_layout::TypeLayout; #[derive(Default)] pub struct MarginfiGroup { pub admin: Pubkey, - pub _padding_0: [[u64; 2]; 32], + /// Bitmask for group settings flags. + /// * Bit 0: If set, program-level fees are enabled. + /// * Bits 1-63: Reserved for future use. + pub group_flags: u64, + /// Caches information from the global `FeeState` so the FeeState can be omitted on certain ixes + pub fee_state_cache: FeeStateCache, + pub _padding_0: [[u64; 2]; 27], pub _padding_1: [[u64; 2]; 32], + pub _padding_3: u64, +} + +#[derive( + AnchorSerialize, AnchorDeserialize, Clone, Copy, Default, Zeroable, Pod, Debug, PartialEq, Eq, +)] +#[repr(C)] +pub struct FeeStateCache { + pub global_fee_wallet: Pubkey, + pub program_fee_fixed: WrappedI80F48, + pub program_fee_rate: WrappedI80F48, } impl MarginfiGroup { + const PROGRAM_FEES_ENABLED: u64 = 1; + + /// Bits in use for flag settings. + const ALLOWED_FLAGS: u64 = Self::PROGRAM_FEES_ENABLED; + // To add: const ALLOWED_FLAGS: u64 = PROGRAM_FEES_ENABLED | ANOTHER_FEATURE_BIT; + /// Configure the group parameters. /// This function validates config values so the group remains in a valid state. /// Any modification of group config should happen through this function. @@ -64,6 +89,34 @@ impl MarginfiGroup { #[allow(clippy::too_many_arguments)] pub fn set_initial_configuration(&mut self, admin_pk: Pubkey) { self.admin = admin_pk; + self.group_flags = Self::PROGRAM_FEES_ENABLED; + } + + pub fn get_group_bank_config(&self) -> GroupBankConfig { + GroupBankConfig { + program_fees: self.group_flags == Self::PROGRAM_FEES_ENABLED, + } + } + + /// Validates that only allowed flags are being set. + pub fn validate_flags(flag: u64) -> MarginfiResult { + // Note: 0xnnnn & 0x1110, is nonzero for 0x1000 & 0x1110 + let flag_ok = flag & !Self::ALLOWED_FLAGS == 0; + check!(flag_ok, MarginfiError::IllegalFlag); + + Ok(()) + } + + /// Sets flag and errors if a disallowed flag is set + pub fn set_flags(&mut self, flag: u64) -> MarginfiResult { + Self::validate_flags(flag)?; + self.group_flags = flag; + Ok(()) + } + + /// True if program fees are enabled + pub fn program_fees_enabled(&self) -> bool { + (self.group_flags & Self::PROGRAM_FEES_ENABLED) != 0 } } @@ -101,16 +154,26 @@ pub struct InterestRateConfigCompact { } 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, - _padding: [[0; 2]; 8], + fn from( + InterestRateConfigCompact { + optimal_utilization_rate, + plateau_interest_rate, + max_interest_rate, + insurance_fee_fixed_apr, + insurance_ir_fee, + protocol_fixed_fee_apr, + protocol_ir_fee, + }: InterestRateConfigCompact, + ) -> Self { + Self { + optimal_utilization_rate, + plateau_interest_rate, + max_interest_rate, + insurance_fee_fixed_apr, + insurance_ir_fee, + protocol_fixed_fee_apr, + protocol_ir_fee, + _padding: [0; 32], } } } @@ -129,6 +192,7 @@ impl From for InterestRateConfigCompact { } } +assert_struct_size!(InterestRateConfig, 240); #[zero_copy] #[repr(C)] #[cfg_attr( @@ -143,88 +207,36 @@ pub struct InterestRateConfig { 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 _padding: [[u64; 2]; 8], // 16 * 8 = 128 bytes + pub _padding: [u32; 32], } impl InterestRateConfig { - /// Return interest rate charged to borrowers and to depositors. - /// Rate is denominated in APR (0-). - /// - /// Return (`lending_rate`, `borrowing_rate`, `group_fees_apr`, `insurance_fees_apr`) - pub fn calc_interest_rate( - &self, - utilization_ratio: I80F48, - ) -> Option<(I80F48, I80F48, I80F48, I80F48)> { - let protocol_ir_fee = I80F48::from(self.protocol_ir_fee); - let insurance_ir_fee = I80F48::from(self.insurance_ir_fee); - - let protocol_fixed_fee_apr = I80F48::from(self.protocol_fixed_fee_apr); - let insurance_fee_fixed_apr = I80F48::from(self.insurance_fee_fixed_apr); - - let rate_fee = protocol_ir_fee + insurance_ir_fee; - let total_fixed_fee_apr = protocol_fixed_fee_apr + insurance_fee_fixed_apr; - - 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 = 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 = base_rate - .checked_mul(I80F48::ONE.checked_add(rate_fee)?)? - .checked_add(total_fixed_fee_apr)?; - - let group_fees_apr = calc_fee_rate( - base_rate, - self.protocol_ir_fee.into(), - self.protocol_fixed_fee_apr.into(), - )?; - - let insurance_fees_apr = calc_fee_rate( - base_rate, - self.insurance_ir_fee.into(), - self.insurance_fee_fixed_apr.into(), - )?; - - assert!(lending_rate >= I80F48::ZERO); - assert!(borrowing_rate >= I80F48::ZERO); - assert!(group_fees_apr >= I80F48::ZERO); - assert!(insurance_fees_apr >= I80F48::ZERO); - - // TODO: Add liquidation discount check - - Some(( - lending_rate, - borrowing_rate, - group_fees_apr, - insurance_fees_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 = self.optimal_utilization_rate.into(); - let plateau_ir = self.plateau_interest_rate.into(); - let max_ir: I80F48 = self.max_interest_rate.into(); - - 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 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(), } } @@ -264,6 +276,134 @@ impl InterestRateConfig { } } +#[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) @@ -280,6 +420,12 @@ pub struct InterestRateConfigOpt { pub protocol_ir_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))] @@ -312,6 +458,7 @@ pub struct Bank { 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, @@ -320,6 +467,7 @@ pub struct Bank { 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, @@ -342,7 +490,10 @@ pub struct Bank { pub emissions_remaining: WrappedI80F48, pub emissions_mint: Pubkey, - pub _padding_0: [[u64; 2]; 28], + /// 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 } @@ -570,6 +721,7 @@ impl Bank { 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"))] @@ -602,37 +754,58 @@ impl Bank { return Ok(()); } - - let (asset_share_value, liability_share_value, fees_collected, insurance_collected) = - calc_interest_rate_accrual_state_changes( - time_delta, - total_assets, - total_liabilities, - &self.config.interest_rate_config, - self.asset_share_value.into(), - self.liability_share_value.into(), - ) - .ok_or_else(math_error!())?; + 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, fees_collected, insurance_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(); - self.collected_group_fees_outstanding = { - fees_collected - .checked_add(self.collected_group_fees_outstanding.into()) - .ok_or_else(math_error!())? - .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() + }; + } - self.collected_insurance_fees_outstanding = { - insurance_collected - .checked_add(self.collected_insurance_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"))] { @@ -647,8 +820,8 @@ impl Bank { bank, mint: self.mint, delta: time_delta, - fees_collected: fees_collected.to_num::(), - insurance_collected: insurance_collected.to_num::(), + fees_collected: group_fees_collected.to_num::(), + insurance_collected: insurance_fees_collected.to_num::(), }); } @@ -858,30 +1031,62 @@ fn calc_interest_rate_accrual_state_changes( time_delta: u64, total_assets_amount: I80F48, total_liabilities_amount: I80F48, - interest_rate_config: &InterestRateConfig, + interest_rate_calc: &InterestRateCalc, asset_share_value: I80F48, liability_share_value: I80F48, -) -> Option<(I80F48, I80F48, I80F48, I80F48)> { +) -> Option { let utilization_rate = total_liabilities_amount.checked_div(total_assets_amount)?; - let (lending_apr, borrowing_apr, group_fee_apr, insurance_fee_apr) = - interest_rate_config.calc_interest_rate(utilization_rate)?; + let computed_rates = interest_rate_calc.calc_interest_rate(utilization_rate)?; debug!( - "Accruing interest for {} seconds. Utilization rate: {}. Lending APR: {}. Borrowing APR: {}. Group fee APR: {}. Insurance fee APR: {}.", - time_delta, - utilization_rate, - lending_apr, - borrowing_apr, - group_fee_apr, - insurance_fee_apr + "Utilization rate: {}, time delta {}s", + utilization_rate, time_delta ); + debug!("{:#?}", computed_rates); - Some(( - calc_accrued_interest_payment_per_period(lending_apr, time_delta, asset_share_value)?, - calc_accrued_interest_payment_per_period(borrowing_apr, time_delta, liability_share_value)?, - calc_interest_payment_for_period(group_fee_apr, time_delta, total_liabilities_amount)?, - calc_interest_payment_for_period(insurance_fee_apr, time_delta, total_liabilities_amount)?, - )) + 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. @@ -889,6 +1094,10 @@ fn calc_interest_rate_accrual_state_changes( /// /// 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) } @@ -911,6 +1120,10 @@ fn calc_accrued_interest_payment_per_period( /// 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())? @@ -1222,10 +1435,7 @@ impl BankConfig { #[zero_copy] #[repr(C, align(8))] -#[cfg_attr( - any(feature = "test", feature = "client"), - derive(PartialEq, Eq, TypeLayout) -)] +#[cfg_attr(any(feature = "test", feature = "client"), derive(TypeLayout))] #[derive(Default, BorshDeserialize, BorshSerialize)] pub struct WrappedI80F48 { pub value: [u8; 16], @@ -1251,6 +1461,14 @@ impl From for I80F48 { } } +impl PartialEq for WrappedI80F48 { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl Eq for WrappedI80F48 {} + #[cfg_attr( any(feature = "test", feature = "client"), derive(Clone, PartialEq, Eq, TypeLayout) @@ -1333,6 +1551,8 @@ macro_rules! assert_eq_with_tolerance { 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; @@ -1429,13 +1649,22 @@ mod tests { ..Default::default() }; - let (lending_apr, borrow_apr, group_fees_apr, insurance_apr) = - config.calc_interest_rate(I80F48!(0)).unwrap(); + 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), I80F48!(0.001)); - assert_eq_with_tolerance!(borrow_apr, I80F48!(0.01), I80F48!(0.001)); + 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] @@ -1452,8 +1681,16 @@ mod tests { ..Default::default() }; - let (lending_apr, borrow_apr, group_fees_apr, insurance_apr) = - config.calc_interest_rate(I80F48!(0.5)).unwrap(); + 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)); @@ -1461,6 +1698,18 @@ mod tests { 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 @@ -1478,8 +1727,16 @@ mod tests { ..Default::default() }; - let (lending_apr, borrow_apr, group_fees_apr, insurance_apr) = - config.calc_interest_rate(I80F48!(0.7)).unwrap(); + 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)); @@ -1531,6 +1788,7 @@ mod tests { bank.accrue_interest( current_timestamp, + &MarginfiGroup::default(), #[cfg(not(feature = "client"))] Pubkey::default(), ) @@ -1560,20 +1818,72 @@ mod tests { }; 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); - let (lending_apr, borrow_apr, fees_apr, insurance_apr) = ir_config + 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!("fees_apr: {}", fees_apr); - println!("insurance_apr: {}", insurance_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) + fees_apr + insurance_apr, + (lending_apr / ur) + group_fee_apr + insurance_fee_apr, I80F48!(0.001) ); @@ -1591,6 +1901,11 @@ mod tests { ..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); @@ -1600,24 +1915,30 @@ mod tests { let old_total_liability_amount = liab_share_value * total_liability_shares; let old_total_asset_amount = asset_share_value * total_asset_shares; - let (new_asset_share_value, new_liab_share_value, fees_collected, insurance_collected) = - calc_interest_rate_accrual_state_changes( - 3600, - total_asset_shares, - total_liability_shares, - &ir_config, - asset_share_value, - liab_share_value, - ) - .unwrap(); + 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!("fees_collected: {}", fees_collected); + 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); @@ -1625,23 +1946,19 @@ mod tests { println!("old_total_liability_amount: {}", old_total_liability_amount); println!("old_total_asset_amount: {}", old_total_asset_amount); - println!( - "total_fee_collected: {}", - fees_collected + insurance_collected - ); + 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) - + fees_collected - + insurance_collected) + ((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) - + fees_collected - + insurance_collected, + (new_total_asset_amount - new_total_liability_amount) + total_fees_collected, old_total_asset_amount - old_total_liability_amount, I80F48::ONE ); diff --git a/programs/marginfi/src/state/mod.rs b/programs/marginfi/src/state/mod.rs index 71075562..7b5dec9e 100644 --- a/programs/marginfi/src/state/mod.rs +++ b/programs/marginfi/src/state/mod.rs @@ -1,3 +1,4 @@ +pub mod fee_state; pub mod marginfi_account; pub mod marginfi_group; pub mod price; diff --git a/programs/marginfi/tests/admin_actions/bankruptcy.rs b/programs/marginfi/tests/admin_actions/bankruptcy.rs index 7c72fef2..ff28e01a 100644 --- a/programs/marginfi/tests/admin_actions/bankruptcy.rs +++ b/programs/marginfi/tests/admin_actions/bankruptcy.rs @@ -854,6 +854,7 @@ async fn marginfi_group_handle_bankruptcy_success_not_insured_3_depositors() -> }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; diff --git a/programs/marginfi/tests/admin_actions/bankruptcy_auth.rs b/programs/marginfi/tests/admin_actions/bankruptcy_auth.rs index 31e36932..72a7eecd 100644 --- a/programs/marginfi/tests/admin_actions/bankruptcy_auth.rs +++ b/programs/marginfi/tests/admin_actions/bankruptcy_auth.rs @@ -30,6 +30,7 @@ async fn marginfi_group_handle_bankruptcy_unauthorized() -> anyhow::Result<()> { }), }, ], + ..Default::default() })) .await; @@ -124,6 +125,7 @@ async fn marginfi_group_handle_bankruptcy_perimssionless() -> anyhow::Result<()> }), }, ], + ..Default::default() })) .await; diff --git a/programs/marginfi/tests/admin_actions/create_marginfi_group.rs b/programs/marginfi/tests/admin_actions/create_marginfi_group.rs index 762459cc..efcd49b7 100644 --- a/programs/marginfi/tests/admin_actions/create_marginfi_group.rs +++ b/programs/marginfi/tests/admin_actions/create_marginfi_group.rs @@ -1,10 +1,10 @@ use anchor_lang::{InstructionData, ToAccountMetas}; use fixtures::prelude::*; -use marginfi::prelude::MarginfiGroup; +use marginfi::{constants::FEE_STATE_SEED, prelude::MarginfiGroup}; use pretty_assertions::assert_eq; use solana_program::{instruction::Instruction, system_program}; use solana_program_test::*; -use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction}; #[tokio::test] async fn marginfi_group_create_success() -> anyhow::Result<()> { @@ -13,9 +13,13 @@ async fn marginfi_group_create_success() -> anyhow::Result<()> { // Create & initialize marginfi group let marginfi_group_key = Keypair::new(); + let (fee_state_key, _bump) = + Pubkey::find_program_address(&[FEE_STATE_SEED.as_bytes()], &marginfi::id()); + let accounts = marginfi::accounts::MarginfiGroupInitialize { marginfi_group: marginfi_group_key.pubkey(), admin: test_f.payer(), + fee_state: fee_state_key, system_program: system_program::id(), }; let init_marginfi_group_ix = Instruction { diff --git a/programs/marginfi/tests/admin_actions/interest_accrual.rs b/programs/marginfi/tests/admin_actions/interest_accrual.rs index 75bde559..fe3cc932 100644 --- a/programs/marginfi/tests/admin_actions/interest_accrual.rs +++ b/programs/marginfi/tests/admin_actions/interest_accrual.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::Clock; +use anchor_spl::associated_token::get_associated_token_address_with_program_id; use fixed::types::I80F48; use fixed_macro::types::I80F48; use fixtures::{assert_eq_noise, native, prelude::*}; @@ -33,6 +34,7 @@ async fn marginfi_group_accrue_interest_rates_success_1() -> anyhow::Result<()> }), }, ], + protocol_fees: false, })) .await; @@ -119,6 +121,7 @@ async fn marginfi_group_accrue_interest_rates_success_2() -> anyhow::Result<()> }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; @@ -160,6 +163,24 @@ async fn marginfi_group_accrue_interest_rates_success_2() -> anyhow::Result<()> .try_accrue_interest(usdc_bank_f) .await?; + // The program fee ata needs to exist, but doesn't need any assets. + { + let ctx = test_f.context.clone(); + let ata = TokenAccountFixture::new_from_ata( + ctx, + &test_f.usdc_mint.key, + &test_f.marginfi_group.fee_wallet, + &test_f.usdc_mint.token_program, + ) + .await; + let ata_expected = get_associated_token_address_with_program_id( + &test_f.marginfi_group.fee_wallet, + &test_f.usdc_mint.key, + &test_f.usdc_mint.token_program, + ); + assert_eq!(ata.key, ata_expected); + } + test_f.marginfi_group.try_collect_fees(usdc_bank_f).await?; let borrower_mfi_account = borrower_mfi_account_f.load().await; diff --git a/programs/marginfi/tests/admin_actions/setup_bank.rs b/programs/marginfi/tests/admin_actions/setup_bank.rs index 2d7eade8..9aeeddf0 100644 --- a/programs/marginfi/tests/admin_actions/setup_bank.rs +++ b/programs/marginfi/tests/admin_actions/setup_bank.rs @@ -2,7 +2,7 @@ use fixed::types::I80F48; use fixed_macro::types::I80F48; use fixtures::{assert_custom_error, prelude::*}; use marginfi::{ - constants::PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG, + constants::{INIT_BANK_ORIGINATION_FEE_DEFAULT, PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG}, prelude::MarginfiError, state::marginfi_group::{Bank, BankConfig, BankConfigOpt, BankVaultType}, }; @@ -16,6 +16,8 @@ async fn add_bank_success() -> anyhow::Result<()> { // Setup test executor with non-admin payer let test_f = TestFixture::new(None).await; + let fee_wallet = test_f.marginfi_group.fee_wallet; + let mints = vec![ ( MintFixture::new(test_f.context.clone(), None, None).await, @@ -38,6 +40,19 @@ async fn add_bank_success() -> anyhow::Result<()> { ]; for (mint_f, bank_config) in mints { + // Load the fee state before the start of the test + let fee_balance_before: u64; + { + let mut ctx = test_f.context.borrow_mut(); + fee_balance_before = ctx + .banks_client + .get_account(fee_wallet) + .await + .unwrap() + .unwrap() + .lamports; + } + let res = test_f .marginfi_group .try_lending_pool_add_bank(&mint_f, bank_config) @@ -100,12 +115,28 @@ async fn add_bank_success() -> anyhow::Result<()> { assert_eq!(emissions_mint, Pubkey::new_from_array([0; 32])); assert_eq!(emissions_remaining, I80F48!(0.0).into()); - assert_eq!(_padding_0, <[[u64; 2]; 28] as Default>::default()); + assert_eq!(_padding_0, <[[u64; 2]; 27] as Default>::default()); assert_eq!(_padding_1, <[[u64; 2]; 32] as Default>::default()); // this is the only loosely checked field assert!(last_update >= 0 && last_update <= 5); }; + + // Load the fee state after the test + let fee_balance_after: u64; + { + let mut ctx = test_f.context.borrow_mut(); + fee_balance_after = ctx + .banks_client + .get_account(fee_wallet) + .await + .unwrap() + .unwrap() + .lamports; + } + let expected_fee_delta = INIT_BANK_ORIGINATION_FEE_DEFAULT as u64; + let actual_fee_delta = fee_balance_after - fee_balance_before; + assert_eq!(expected_fee_delta, actual_fee_delta); } Ok(()) @@ -116,6 +147,8 @@ async fn add_bank_with_seed_success() -> anyhow::Result<()> { // Setup test executor with non-admin payer let test_f = TestFixture::new(None).await; + let fee_wallet = test_f.marginfi_group.fee_wallet; + let mints = vec![ ( MintFixture::new(test_f.context.clone(), None, None).await, @@ -138,6 +171,18 @@ async fn add_bank_with_seed_success() -> anyhow::Result<()> { ]; for (mint_f, bank_config) in mints { + let fee_balance_before: u64; + { + let mut ctx = test_f.context.borrow_mut(); + fee_balance_before = ctx + .banks_client + .get_account(fee_wallet) + .await + .unwrap() + .unwrap() + .lamports; + } + let bank_seed = 1200_u64; let res = test_f @@ -203,12 +248,27 @@ async fn add_bank_with_seed_success() -> anyhow::Result<()> { assert_eq!(emissions_mint, Pubkey::new_from_array([0; 32])); assert_eq!(emissions_remaining, I80F48!(0.0).into()); - assert_eq!(_padding_0, <[[u64; 2]; 28] as Default>::default()); + assert_eq!(_padding_0, <[[u64; 2]; 27] as Default>::default()); assert_eq!(_padding_1, <[[u64; 2]; 32] as Default>::default()); // this is the only loosely checked field assert!(last_update >= 0 && last_update <= 5); }; + + let fee_balance_after: u64; + { + let mut ctx = test_f.context.borrow_mut(); + fee_balance_after = ctx + .banks_client + .get_account(fee_wallet) + .await + .unwrap() + .unwrap() + .lamports; + } + let expected_fee_delta = INIT_BANK_ORIGINATION_FEE_DEFAULT as u64; + let actual_fee_delta = fee_balance_after - fee_balance_before; + assert_eq!(expected_fee_delta, actual_fee_delta); } Ok(()) diff --git a/programs/marginfi/tests/misc/operational_state.rs b/programs/marginfi/tests/misc/operational_state.rs index 657ddce8..88031bba 100644 --- a/programs/marginfi/tests/misc/operational_state.rs +++ b/programs/marginfi/tests/misc/operational_state.rs @@ -15,6 +15,7 @@ async fn marginfi_group_bank_paused_should_error() -> anyhow::Result<()> { config: None, }], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; @@ -54,6 +55,7 @@ async fn marginfi_group_bank_reduce_only_withdraw_success() -> anyhow::Result<() config: None, }], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; @@ -101,6 +103,7 @@ async fn marginfi_group_bank_reduce_only_deposit_success() -> anyhow::Result<()> }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; @@ -160,6 +163,7 @@ async fn marginfi_group_bank_reduce_only_borrow_failure() -> anyhow::Result<()> }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; @@ -207,6 +211,7 @@ async fn marginfi_group_bank_reduce_only_deposit_failure() -> anyhow::Result<()> config: None, }], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; diff --git a/programs/marginfi/tests/misc/pyth_push.rs b/programs/marginfi/tests/misc/pyth_push.rs index 7f7bcf98..0ba0a357 100644 --- a/programs/marginfi/tests/misc/pyth_push.rs +++ b/programs/marginfi/tests/misc/pyth_push.rs @@ -28,6 +28,7 @@ async fn pyth_push_fullv_borrow() -> anyhow::Result<()> { }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; @@ -96,6 +97,7 @@ async fn pyth_push_partv_borrow() -> anyhow::Result<()> { }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; @@ -159,6 +161,7 @@ async fn pyth_push_fullv_liquidate() -> anyhow::Result<()> { }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; diff --git a/programs/marginfi/tests/misc/real_oracle_data.rs b/programs/marginfi/tests/misc/real_oracle_data.rs index 1bbeb005..d29a5c64 100644 --- a/programs/marginfi/tests/misc/real_oracle_data.rs +++ b/programs/marginfi/tests/misc/real_oracle_data.rs @@ -23,6 +23,7 @@ async fn real_oracle_marginfi_account_borrow_success() -> anyhow::Result<()> { }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; @@ -97,6 +98,7 @@ async fn real_oracle_pyth_push_marginfi_account_borrow_success() -> anyhow::Resu }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; diff --git a/programs/marginfi/tests/misc/regression.rs b/programs/marginfi/tests/misc/regression.rs index a8b855f2..823ddebe 100644 --- a/programs/marginfi/tests/misc/regression.rs +++ b/programs/marginfi/tests/misc/regression.rs @@ -655,7 +655,7 @@ async fn bank_field_values_reg() -> anyhow::Result<()> { pubkey!("2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo") ); - assert_eq!(bank._padding_0, [[0, 0]; 28]); + assert_eq!(bank._padding_0, [[0, 0]; 27]); assert_eq!(bank._padding_1, [[0, 0]; 32]); Ok(()) 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 e1d1fe4d..6d27aa33 100644 --- a/programs/marginfi/tests/misc/risk_engine_flexible_oracle_checks.rs +++ b/programs/marginfi/tests/misc/risk_engine_flexible_oracle_checks.rs @@ -240,6 +240,7 @@ async fn re_liquidaiton_fail() -> anyhow::Result<()> { }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; @@ -337,6 +338,7 @@ async fn re_bankruptcy_fail() -> anyhow::Result<()> { }), }, ], + protocol_fees: false, })) .await; diff --git a/programs/marginfi/tests/misc/token_extensions.rs b/programs/marginfi/tests/misc/token_extensions.rs index a9eb0625..af315859 100644 --- a/programs/marginfi/tests/misc/token_extensions.rs +++ b/programs/marginfi/tests/misc/token_extensions.rs @@ -55,6 +55,7 @@ async fn marginfi_account_liquidation_success_with_extension( }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, }), &extensions, ) diff --git a/programs/marginfi/tests/user_actions/liquidate.rs b/programs/marginfi/tests/user_actions/liquidate.rs index 613f8a7a..41d60532 100644 --- a/programs/marginfi/tests/user_actions/liquidate.rs +++ b/programs/marginfi/tests/user_actions/liquidate.rs @@ -402,6 +402,7 @@ async fn marginfi_account_liquidation_success_swb() -> anyhow::Result<()> { }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; @@ -524,6 +525,7 @@ async fn marginfi_account_liquidation_failure_liquidatee_not_unhealthy() -> anyh }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; @@ -627,6 +629,7 @@ async fn marginfi_account_liquidation_failure_liquidator_no_collateral() -> anyh }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, })) .await; diff --git a/programs/mocks/Cargo.toml b/programs/mocks/Cargo.toml index e1a5ff09..66765715 100644 --- a/programs/mocks/Cargo.toml +++ b/programs/mocks/Cargo.toml @@ -22,6 +22,7 @@ devnet = [] mainnet-beta = [] debug = [] staging = [] +ignore-fee-deploy = [] [dependencies] anchor-lang = { workspace = true } diff --git a/programs/test_transfer_hook/Cargo.toml b/programs/test_transfer_hook/Cargo.toml index b23f23a7..02687bc9 100644 --- a/programs/test_transfer_hook/Cargo.toml +++ b/programs/test_transfer_hook/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [features] idl-build = [] no-entrypoint = [] +ignore-fee-deploy = [] [lib] crate-type = ["cdylib", "lib"] diff --git a/scripts/build-program.sh b/scripts/build-program.sh index 1ca3556f..c1e0eb73 100755 --- a/scripts/build-program.sh +++ b/scripts/build-program.sh @@ -21,6 +21,6 @@ else exit 1 fi -cmd="anchor build -p $program_lib_name -- $features" +cmd="anchor build -p $program_lib_name -- $features ignore-fee-deploy" echo "Running: $cmd" eval "$cmd" diff --git a/scripts/build-workspace.sh b/scripts/build-workspace.sh index 77055ead..65899b82 100755 --- a/scripts/build-workspace.sh +++ b/scripts/build-workspace.sh @@ -2,6 +2,6 @@ ROOT=$(git rev-parse --show-toplevel) cd $ROOT -cmd="anchor build --no-idl" +cmd="anchor build --no-idl -- --features ignore-fee-deploy" echo "Running: $cmd" eval "$cmd" diff --git a/scripts/single-test.sh b/scripts/single-test.sh index f3174c35..926a0e55 100755 --- a/scripts/single-test.sh +++ b/scripts/single-test.sh @@ -20,7 +20,7 @@ cd $ROOT SBF_OUT_DIR="$ROOT/target/deploy" RUST_LOG="solana_runtime::message_processor::stable_log=debug" -CARGO_CMD="SBF_OUT_DIR=$SBF_OUT_DIR RUST_LOG=$RUST_LOG cargo nextest run --package $program_name --features=test,test-bpf --test-threads=1 -- $test_name" +CARGO_CMD="SBF_OUT_DIR=$SBF_OUT_DIR RUST_LOG=$RUST_LOG cargo nextest run --package $program_name --features=test,test-bpf --nocapture -- $test_name" echo "Running: $CARGO_CMD" diff --git a/test-utils/src/marginfi_group.rs b/test-utils/src/marginfi_group.rs index 583141d0..6b09bea7 100644 --- a/test-utils/src/marginfi_group.rs +++ b/test-utils/src/marginfi_group.rs @@ -3,22 +3,38 @@ use crate::prelude::{get_oracle_id_from_feed_id, MintFixture}; use crate::utils::*; use anchor_lang::{prelude::*, solana_program::system_program, InstructionData}; +use anchor_spl::associated_token::get_associated_token_address_with_program_id; use anyhow::Result; +use bytemuck::bytes_of; +use marginfi::constants::{ + FEE_STATE_SEED, INIT_BANK_ORIGINATION_FEE_DEFAULT, PROTOCOL_FEE_FIXED_DEFAULT, + PROTOCOL_FEE_RATE_DEFAULT, +}; +use marginfi::state::fee_state::FeeState; use marginfi::{ prelude::MarginfiGroup, state::marginfi_group::{BankConfig, BankConfigOpt, BankVaultType, GroupConfig}, }; use solana_program::sysvar; use solana_program_test::*; +use solana_sdk::system_transaction; use solana_sdk::{ compute_budget::ComputeBudgetInstruction, instruction::Instruction, signature::Keypair, signer::Signer, transaction::Transaction, }; use std::{cell::RefCell, mem, rc::Rc}; +async fn airdrop_sol(context: &mut ProgramTestContext, key: &Pubkey, amount: u64) { + let recent_blockhash = context.banks_client.get_latest_blockhash().await.unwrap(); + let tx = system_transaction::transfer(&context.payer, key, amount, recent_blockhash); + context.banks_client.process_transaction(tx).await.unwrap(); +} + pub struct MarginfiGroupFixture { ctx: Rc>, pub key: Pubkey, + pub fee_state: Pubkey, + pub fee_wallet: Pubkey, } impl MarginfiGroupFixture { @@ -29,6 +45,9 @@ impl MarginfiGroupFixture { let ctx_ref = ctx.clone(); let group_key = Keypair::new(); + let fee_wallet_key: Pubkey; + let (fee_state_key, _bump) = + Pubkey::find_program_address(&[FEE_STATE_SEED.as_bytes()], &marginfi::id()); { let mut ctx = ctx.borrow_mut(); @@ -38,6 +57,7 @@ impl MarginfiGroupFixture { accounts: marginfi::accounts::MarginfiGroupInitialize { marginfi_group: group_key.pubkey(), admin: ctx.payer.pubkey(), + fee_state: fee_state_key, system_program: system_program::id(), } .to_account_metas(Some(true)), @@ -54,18 +74,72 @@ impl MarginfiGroupFixture { data: marginfi::instruction::MarginfiGroupConfigure { config }.data(), }; - let tx = Transaction::new_signed_with_payer( - &[initialize_marginfi_group_ix, configure_marginfi_group_ix], - Some(&ctx.payer.pubkey().clone()), - &[&ctx.payer, &group_key], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); + // Check if the fee state account already exists + let fee_state_account = ctx.banks_client.get_account(fee_state_key).await.unwrap(); + + // Account exists, read it and proceed with group initialization + if let Some(account) = fee_state_account { + if !account.data.is_empty() { + // Deserialize the account data to extract the fee_wallet public key + let fee_state_data: FeeState = + FeeState::try_deserialize(&mut &account.data[..]).unwrap(); + fee_wallet_key = fee_state_data.global_fee_wallet; + + let tx = Transaction::new_signed_with_payer( + &[initialize_marginfi_group_ix, configure_marginfi_group_ix], + Some(&ctx.payer.pubkey().clone()), + &[&ctx.payer, &group_key], + ctx.last_blockhash, + ); + ctx.banks_client.process_transaction(tx).await.unwrap(); + } else { + panic!("Fee state exists but is empty") + } + } else { + // Account does not exist, proceed with group and fee state initialization + let fee_wallet = Keypair::new(); + // The wallet needs some sol to be rent exempt + airdrop_sol(&mut ctx, &fee_wallet.pubkey(), 1_000_000).await; + fee_wallet_key = fee_wallet.pubkey(); + + let init_fee_state_ix = Instruction { + program_id: marginfi::id(), + accounts: marginfi::accounts::InitFeeState { + payer: ctx.payer.pubkey(), + fee_state: fee_state_key, + rent: sysvar::rent::id(), + system_program: system_program::id(), + } + .to_account_metas(Some(true)), + data: marginfi::instruction::InitGlobalFeeState { + admin: ctx.payer.pubkey(), + fee_wallet: fee_wallet.pubkey(), + bank_init_flat_sol_fee: INIT_BANK_ORIGINATION_FEE_DEFAULT, + program_fee_fixed: PROTOCOL_FEE_FIXED_DEFAULT.into(), + program_fee_rate: PROTOCOL_FEE_RATE_DEFAULT.into(), + } + .data(), + }; + + let tx = Transaction::new_signed_with_payer( + &[ + init_fee_state_ix, + initialize_marginfi_group_ix, + configure_marginfi_group_ix, + ], + Some(&ctx.payer.pubkey().clone()), + &[&ctx.payer, &group_key], + ctx.last_blockhash, + ); + ctx.banks_client.process_transaction(tx).await.unwrap(); + } } MarginfiGroupFixture { ctx: ctx_ref.clone(), key: group_key.pubkey(), + fee_state: fee_state_key, + fee_wallet: fee_wallet_key, } } @@ -83,6 +157,8 @@ impl MarginfiGroupFixture { marginfi_group: self.key, admin: self.ctx.borrow().payer.pubkey(), fee_payer: self.ctx.borrow().payer.pubkey(), + fee_state: self.fee_state, + global_fee_wallet: self.fee_wallet, bank_mint, bank: bank_key.pubkey(), liquidity_vault_authority: bank_fixture.get_vault_authority(BankVaultType::Liquidity).0, @@ -160,6 +236,8 @@ impl MarginfiGroupFixture { marginfi_group: self.key, admin: self.ctx.borrow().payer.pubkey(), fee_payer: self.ctx.borrow().payer.pubkey(), + fee_state: self.fee_state, + global_fee_wallet: self.fee_wallet, bank_mint, bank: pda, liquidity_vault_authority: bank_fixture.get_vault_authority(BankVaultType::Liquidity).0, @@ -307,6 +385,12 @@ impl MarginfiGroupFixture { pub async fn try_collect_fees(&self, bank: &BankFixture) -> Result<()> { let mut ctx = self.ctx.borrow_mut(); + let fee_ata = get_associated_token_address_with_program_id( + &self.fee_wallet, + &bank.mint.key, + &bank.get_token_program(), + ); + let mut accounts = marginfi::accounts::LendingPoolCollectBankFees { marginfi_group: self.key, bank: bank.key, @@ -315,6 +399,8 @@ impl MarginfiGroupFixture { insurance_vault: bank.get_vault(BankVaultType::Insurance).0, fee_vault: bank.get_vault(BankVaultType::Fee).0, token_program: bank.get_token_program(), + fee_state: self.fee_state, + fee_ata, } .to_account_metas(Some(true)); if bank.mint.token_program == spl_token_2022::ID { @@ -406,4 +492,23 @@ impl MarginfiGroupFixture { ) .await } + + pub async fn set_protocol_fees_flag(&self, enabled: bool) { + let mut group = self.load().await; + let mut ctx = self.ctx.borrow_mut(); + let mut account = ctx + .banks_client + .get_account(self.key) + .await + .unwrap() + .unwrap(); + + group.group_flags = if enabled { 1 } else { 0 }; + + let data = bytes_of(&group); + + account.data[8..].copy_from_slice(data); + + ctx.set_account(&self.key, &account.into()) + } } diff --git a/test-utils/src/spl.rs b/test-utils/src/spl.rs index fd622fae..01346e0a 100644 --- a/test-utils/src/spl.rs +++ b/test-utils/src/spl.rs @@ -1,6 +1,10 @@ use crate::{transfer_hook::TEST_HOOK_ID, ui_to_native}; use anchor_lang::prelude::*; use anchor_spl::{ + associated_token::{ + get_associated_token_address_with_program_id, + spl_associated_token_account::instruction::create_associated_token_account, + }, token::{spl_token, Mint, TokenAccount}, token_2022::{ self, @@ -297,7 +301,7 @@ impl MintFixture { self.create_token_account_and_mint_to(0.0).await } - pub async fn create_token_account_and_mint_to>( + pub async fn create_token_account_and_mint_to<'a, T: Into>( &self, ui_amount: T, ) -> TokenAccountFixture { @@ -505,6 +509,57 @@ impl TokenAccountFixture { } } + pub async fn new_from_ata( + ctx: Rc>, + mint_pk: &Pubkey, + owner_pk: &Pubkey, + token_program: &Pubkey, + ) -> Self { + let ctx_ref = ctx.clone(); + let ata_address = + get_associated_token_address_with_program_id(owner_pk, mint_pk, token_program); + + { + let create_ata_ix = create_associated_token_account( + &ctx.borrow().payer.pubkey(), + owner_pk, + mint_pk, + token_program, + ); + + let tx = Transaction::new_signed_with_payer( + &[create_ata_ix], + Some(&ctx.borrow().payer.pubkey()), + &[&ctx.borrow().payer], + ctx.borrow().last_blockhash, + ); + + ctx.borrow_mut() + .banks_client + .process_transaction(tx) + .await + .unwrap(); + } + + // Now retrieve the account info for the newly created ATA + let mut ctx = ctx.borrow_mut(); + let account = ctx + .banks_client + .get_account(ata_address) + .await + .unwrap() + .unwrap(); + + Self { + ctx: ctx_ref.clone(), + key: ata_address, // Use the ATA address as the key + token: StateWithExtensionsOwned::::unpack(account.data) + .unwrap() + .base, + token_program: *token_program, + } + } + pub async fn new( ctx: Rc>, mint_fixture: &MintFixture, diff --git a/test-utils/src/test.rs b/test-utils/src/test.rs index d457afde..50740318 100644 --- a/test-utils/src/test.rs +++ b/test-utils/src/test.rs @@ -30,6 +30,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc}; pub struct TestSettings { pub group_config: Option, pub banks: Vec, + pub protocol_fees: bool, } impl TestSettings { @@ -76,6 +77,7 @@ impl TestSettings { Self { banks, group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, } } @@ -93,6 +95,33 @@ impl TestSettings { }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, + } + } + + pub fn all_banks_one_isolated() -> Self { + Self { + banks: vec![ + TestBankSetting { + mint: BankMint::Usdc, + ..TestBankSetting::default() + }, + TestBankSetting { + mint: BankMint::Sol, + ..TestBankSetting::default() + }, + TestBankSetting { + mint: BankMint::SolEquivalent, + config: Some(BankConfig { + risk_tier: RiskTier::Isolated, + asset_weight_maint: I80F48!(0).into(), + asset_weight_init: I80F48!(0).into(), + ..*DEFAULT_SOL_EQUIVALENT_TEST_BANK_CONFIG + }), + }, + ], + group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, } } @@ -141,6 +170,7 @@ impl TestSettings { }, ], group_config: Some(GroupConfig { admin: None }), + protocol_fees: false, } } } @@ -599,6 +629,10 @@ impl TestFixture { ) .await; + tester_group + .set_protocol_fees_flag(test_settings.clone().unwrap_or_default().protocol_fees) + .await; + let mut banks = HashMap::new(); if let Some(test_settings) = test_settings.clone() { for bank in test_settings.banks.iter() { diff --git a/tests/01_initGroup.spec.ts b/tests/01_initGroup.spec.ts index 03a90dcd..bf9467bd 100644 --- a/tests/01_initGroup.spec.ts +++ b/tests/01_initGroup.spec.ts @@ -1,12 +1,15 @@ -import { - Program, - workspace, -} from "@coral-xyz/anchor"; +import { Program, workspace } from "@coral-xyz/anchor"; import { Transaction } from "@solana/web3.js"; import { groupInitialize } from "./utils/instructions"; import { Marginfi } from "../target/types/marginfi"; -import { groupAdmin, marginfiGroup } from "./rootHooks"; -import { assertKeysEqual } from "./utils/genericTests"; +import { + globalFeeWallet, + groupAdmin, + marginfiGroup, + PROGRAM_FEE_FIXED, + PROGRAM_FEE_RATE, +} from "./rootHooks"; +import { assertI80F48Approx, assertKeysEqual } from "./utils/genericTests"; describe("Init group", () => { const program = workspace.Marginfi as Program; @@ -29,5 +32,11 @@ describe("Init group", () => { marginfiGroup.publicKey ); assertKeysEqual(group.admin, groupAdmin.wallet.publicKey); + + const feeCache = group.feeStateCache; + const tolerance = 0.00001; + assertI80F48Approx(feeCache.programFeeFixed, PROGRAM_FEE_FIXED, tolerance); + assertI80F48Approx(feeCache.programFeeRate, PROGRAM_FEE_RATE, tolerance); + assertKeysEqual(feeCache.globalFeeWallet, globalFeeWallet); }); }); diff --git a/tests/03_addBank.spec.ts b/tests/03_addBank.spec.ts index d5597b6b..d05fdef1 100644 --- a/tests/03_addBank.spec.ts +++ b/tests/03_addBank.spec.ts @@ -6,7 +6,9 @@ import { bankKeypairA, bankKeypairUsdc, ecosystem, + globalFeeWallet, groupAdmin, + INIT_POOL_ORIGINATION_FEE, marginfiGroup, oracles, verbose, @@ -29,6 +31,7 @@ import { } from "./utils/pdas"; import { assert } from "chai"; import { printBufferGroups } from "./utils/tools"; +import { wrappedI80F48toBigNumber } from "@mrgnlabs/mrgn-common"; describe("Lending pool add bank (add bank to group)", () => { const program = workspace.Marginfi as Program; @@ -38,6 +41,10 @@ describe("Lending pool add bank (add bank to group)", () => { let bankKey = bankKeypairUsdc.publicKey; const now = Date.now() / 1000; + const feeAccSolBefore = await program.provider.connection.getBalance( + globalFeeWallet + ); + await groupAdmin.userMarginProgram!.provider.sendAndConfirm!( new Transaction().add( await addBank(program, { @@ -46,16 +53,26 @@ describe("Lending pool add bank (add bank to group)", () => { feePayer: groupAdmin.wallet.publicKey, bankMint: ecosystem.usdcMint.publicKey, bank: bankKey, + // globalFeeWallet: globalFeeWallet, config: setConfig, }) ), [bankKeypairUsdc] ); + const feeAccSolAfter = await program.provider.connection.getBalance( + globalFeeWallet + ); + if (verbose) { console.log("*init USDC bank " + bankKey); + console.log( + " Origination fee collected: " + (feeAccSolAfter - feeAccSolBefore) + ); } + assert.equal(feeAccSolAfter - feeAccSolBefore, INIT_POOL_ORIGINATION_FEE); + let bankData = ( await program.provider.connection.getAccountInfo(bankKey) ).data.subarray(8); @@ -116,10 +133,11 @@ describe("Lending pool add bank (add bank to group)", () => { assertI80F48Approx(interest.optimalUtilizationRate, 0.5, tolerance); assertI80F48Approx(interest.plateauInterestRate, 0.6, tolerance); assertI80F48Approx(interest.maxInterestRate, 3, tolerance); - assertI80F48Equal(interest.insuranceFeeFixedApr, 0); - assertI80F48Equal(interest.insuranceIrFee, 0); - assertI80F48Equal(interest.protocolFixedFeeApr, 0); - assertI80F48Equal(interest.protocolIrFee, 0); + + assertI80F48Approx(interest.insuranceFeeFixedApr, 0.01, tolerance); + assertI80F48Approx(interest.insuranceIrFee, 0.02, tolerance); + assertI80F48Approx(interest.protocolFixedFeeApr, 0.03, tolerance); + assertI80F48Approx(interest.protocolIrFee, 0.04, tolerance); assert.deepEqual(config.operationalState, { operational: {} }); assert.deepEqual(config.oracleSetup, { pythLegacy: {} }); @@ -127,6 +145,8 @@ describe("Lending pool add bank (add bank to group)", () => { assert.deepEqual(config.riskTier, { collateral: {} }); assertBNEqual(config.totalAssetValueInitLimit, 100_000_000_000); assert.equal(config.oracleMaxAge, 100); + + assertI80F48Equal(bank.collectedProgramFeesOutstanding, 0); }); it("(admin) Add bank (token A) - happy path", async () => { @@ -141,6 +161,7 @@ describe("Lending pool add bank (add bank to group)", () => { feePayer: groupAdmin.wallet.publicKey, bankMint: ecosystem.tokenAMint.publicKey, bank: bankKey, + // globalFeeWallet: globalFeeWallet, config: config, }) ), diff --git a/tests/rootHooks.ts b/tests/rootHooks.ts index bf72966a..a4885eb4 100644 --- a/tests/rootHooks.ts +++ b/tests/rootHooks.ts @@ -10,8 +10,16 @@ import { SetupTestUserOptions, } from "./utils/mocks"; import { Marginfi } from "../target/types/marginfi"; -import { Keypair, Transaction } from "@solana/web3.js"; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + Transaction, +} from "@solana/web3.js"; import { setupPythOracles } from "./utils/pyth_mocks"; +import { initGlobalFeeState } from "./utils/instructions"; +import { bigNumberToWrappedI80F48 } from "@mrgnlabs/mrgn-common"; export const ecosystem: Ecosystem = getGenericEcosystem(); export let oracles: Oracles = undefined; @@ -19,9 +27,16 @@ export const verbose = true; /** The program owner is also the provider wallet */ export let globalProgramAdmin: mockUser = undefined; export let groupAdmin: mockUser = undefined; +export let globalFeeWallet: PublicKey = undefined; export const users: mockUser[] = []; export const numUsers = 2; +/** Lamports charged when creating any pool */ +export const INIT_POOL_ORIGINATION_FEE = 1000; + +export const PROGRAM_FEE_FIXED = 0.01; +export const PROGRAM_FEE_RATE = 0.02; + /** Group used for all happy-path tests */ export const marginfiGroup = Keypair.generate(); /** Bank for USDC */ @@ -70,6 +85,29 @@ export const mochaHooks = { tx.add(...aIxes); tx.add(...bIxes); + let globalFeeKeypair = Keypair.generate(); + globalFeeWallet = globalFeeKeypair.publicKey; + // Send some sol to the global fee wallet for rent + tx.add( + SystemProgram.transfer({ + fromPubkey: wallet.publicKey, + toPubkey: globalFeeWallet, + lamports: 10 * LAMPORTS_PER_SOL, + }) + ); + + // Init the global fee state + tx.add( + await initGlobalFeeState(program, { + payer: provider.publicKey, + admin: wallet.payer.publicKey, + wallet: globalFeeWallet, + bankInitFlatSolFee: INIT_POOL_ORIGINATION_FEE, + programFeeFixed: bigNumberToWrappedI80F48(PROGRAM_FEE_FIXED), + programFeeRate: bigNumberToWrappedI80F48(PROGRAM_FEE_RATE), + }) + ); + await provider.sendAndConfirm(tx, [usdcMint, aMint, bMint]); const setupUserOptions: SetupTestUserOptions = { diff --git a/tests/utils/instructions.ts b/tests/utils/instructions.ts index 10c1f924..c72cff0c 100644 --- a/tests/utils/instructions.ts +++ b/tests/utils/instructions.ts @@ -11,6 +11,7 @@ import { } from "./pdas"; import { BankConfig } from "./types"; import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { WrappedI80F48 } from "@mrgnlabs/mrgn-common"; export const MAX_ORACLE_KEYS = 5; @@ -51,6 +52,7 @@ export const addBank = (program: Program, args: AddBankArgs) => { oracleKey: args.config.oracleKey, borrowLimit: args.config.borrowLimit, riskTier: args.config.riskTier, + pad0: [0, 0, 0, 0, 0, 0, 0], totalAssetValueInitLimit: args.config.totalAssetValueInitLimit, oracleMaxAge: args.config.oracleMaxAge, }) @@ -60,6 +62,8 @@ export const addBank = (program: Program, args: AddBankArgs) => { feePayer: args.feePayer, bankMint: args.bankMint, bank: args.bank, + // globalFeeState: deriveGlobalFeeState(id), + // globalFeeWallet: args.globalFeeWallet, // liquidityVaultAuthority = deriveLiquidityVaultAuthority(id, bank); // liquidityVault = deriveLiquidityVault(id, bank); // insuranceVaultAuthority = deriveInsuranceVaultAuthority(id, bank); @@ -114,6 +118,7 @@ export const groupInitialize = ( .marginfiGroupInitialize() .accounts({ marginfiGroup: args.marginfiGroup, + // feeState: deriveGlobalFeeState(id), admin: args.admin, // systemProgram: SystemProgram.programId, }) @@ -121,3 +126,64 @@ export const groupInitialize = ( return ix; }; + +export type InitGlobalFeeStateArgs = { + payer: PublicKey; + admin: PublicKey; + wallet: PublicKey; + bankInitFlatSolFee: number; + programFeeFixed: WrappedI80F48; + programFeeRate: WrappedI80F48; +}; + +export const initGlobalFeeState = ( + program: Program, + args: InitGlobalFeeStateArgs +) => { + const ix = program.methods + .initGlobalFeeState( + args.admin, + args.wallet, + args.bankInitFlatSolFee, + args.programFeeFixed, + args.programFeeRate + ) + .accounts({ + payer: args.payer, + // feeState = deriveGlobalFeeState(id), + // rent = SYSVAR_RENT_PUBKEY, + // systemProgram: SystemProgram.programId, + }) + .instruction(); + + return ix; +}; + +export type EditGlobalFeeStateArgs = { + admin: PublicKey; + wallet: PublicKey; + bankInitFlatSolFee: number; + programFeeFixed: WrappedI80F48; + programFeeRate: WrappedI80F48; +}; + +// TODO add test for this +export const editGlobalFeeState = ( + program: Program, + args: EditGlobalFeeStateArgs +) => { + const ix = program.methods + .editGlobalFeeState( + args.wallet, + args.bankInitFlatSolFee, + args.programFeeFixed, + args.programFeeRate + ) + .accounts({ + globalFeeAdmin: args.admin, + // feeState = deriveGlobalFeeState(id), + }) + .instruction(); + + return ix; +}; diff --git a/tests/utils/pdas.ts b/tests/utils/pdas.ts index 2594ccfa..028b4431 100644 --- a/tests/utils/pdas.ts +++ b/tests/utils/pdas.ts @@ -50,3 +50,10 @@ export const deriveFeeVault = (programId: PublicKey, bank: PublicKey) => { programId ); }; + +export const deriveGlobalFeeState = (programId: PublicKey) => { + return PublicKey.findProgramAddressSync( + [Buffer.from("feestate", "utf-8")], + programId + ); +}; diff --git a/tests/utils/types.ts b/tests/utils/types.ts index 078dc781..855dd460 100644 --- a/tests/utils/types.ts +++ b/tests/utils/types.ts @@ -93,7 +93,10 @@ export type InterestRateConfig = { * * optimalUtilizationRate = .5 * * plateauInterestRate = .6 * * maxInterestRate = 3 - * * All others values = 0 + * * insuranceFeeFixedApr = .01 + * * insuranceIrFee = .02 + * * protocolFixedFeeApr = .03 + * * protocolIrFee = .04 * @returns */ export const defaultInterestRateConfig = () => { @@ -101,10 +104,10 @@ export const defaultInterestRateConfig = () => { optimalUtilizationRate: bigNumberToWrappedI80F48(0.5), plateauInterestRate: bigNumberToWrappedI80F48(0.6), maxInterestRate: bigNumberToWrappedI80F48(3), - insuranceFeeFixedApr: I80F48_ZERO, - insuranceIrFee: I80F48_ZERO, - protocolFixedFeeApr: I80F48_ZERO, - protocolIrFee: I80F48_ZERO, + insuranceFeeFixedApr: bigNumberToWrappedI80F48(0.01), + insuranceIrFee: bigNumberToWrappedI80F48(0.02), + protocolFixedFeeApr: bigNumberToWrappedI80F48(0.03), + protocolIrFee: bigNumberToWrappedI80F48(0.04), }; return config; }; diff --git a/tools/llama-snapshot-tool/src/bin/main.rs b/tools/llama-snapshot-tool/src/bin/main.rs index 7894d7f1..0b0f7770 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, + state::marginfi_group::{Bank, ComputedInterestRates, MarginfiGroup}, }; use reqwest::header::CONTENT_TYPE; use s3::{creds::Credentials, Bucket, Region}; @@ -60,13 +60,20 @@ async fn main() -> Result<()> { let rpc = program.rpc(); let banks = program.accounts::(vec![])?; + let groups = program.accounts::(vec![])?; + let groups_map = groups + .iter() + .map(|(pk, group)| (*pk, group)) + .collect::>(); println!("Found {} banks", banks.len()); let snapshot = join_all( banks .iter() - .map(|(bank_pk, bank)| DefiLammaPoolInfo::from_bank(bank, bank_pk, &rpc)) + .map(|(bank_pk, bank)| { + DefiLammaPoolInfo::from_bank(bank, bank_pk, &rpc, groups_map.get(bank_pk).unwrap()) + }) .collect::>(), ) .await @@ -123,7 +130,12 @@ struct DefiLammaPoolInfo { } impl DefiLammaPoolInfo { - pub async fn from_bank(bank: &Bank, bank_pk: &Pubkey, rpc_client: &RpcClient) -> Result { + pub async fn from_bank( + bank: &Bank, + bank_pk: &Pubkey, + rpc_client: &RpcClient, + group: &MarginfiGroup, + ) -> Result { let ltv = I80F48::ONE / I80F48::from(bank.config.liability_weight_init); let reward_tokens = if bank.emissions_mint != Pubkey::default() { vec![bank.emissions_mint.to_string()] @@ -152,13 +164,18 @@ impl DefiLammaPoolInfo { I80F48::ZERO }; - let (lending_rate, borrowing_rate, _, _) = bank + let ir_calc = bank .config .interest_rate_config - .calc_interest_rate(ur) - .ok_or_else(|| { - anyhow::anyhow!("Failed to calculate interest rate for bank {}", bank_pk) - })?; + .create_interest_rate_calculator(group); + + let ComputedInterestRates { + lending_rate_apr, + borrowing_rate_apr, + .. + } = ir_calc.calc_interest_rate(ur).ok_or_else(|| { + anyhow::anyhow!("Failed to calculate interest rate for bank {}", bank_pk) + })?; let (apr_reward, apr_reward_borrow) = if bank.emissions_mint.ne(&Pubkey::default()) { let emissions_token_price = fetch_price_from_birdeye(&bank.emissions_mint).await?; @@ -202,22 +219,22 @@ impl DefiLammaPoolInfo { ltv: ltv.to_num(), reward_tokens, apy_base: dec_to_percentage(apr_to_apy( - lending_rate.to_num(), + lending_rate_apr.to_num(), SECONDS_PER_YEAR.to_num(), )), apy_reward: apr_reward.map(|a| { dec_to_percentage(apr_to_apy( - (lending_rate + a).to_num(), + (lending_rate_apr + a).to_num(), SECONDS_PER_YEAR.to_num(), )) }), apy_base_borrow: dec_to_percentage(apr_to_apy( - borrowing_rate.to_num(), + borrowing_rate_apr.to_num(), SECONDS_PER_YEAR.to_num(), )), apy_reward_borrow: apr_reward_borrow.map(|a| { dec_to_percentage(apr_to_apy( - (borrowing_rate + a).to_num(), + (borrowing_rate_apr + a).to_num(), SECONDS_PER_YEAR.to_num(), )) }),