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(), )) }),