diff --git a/programs/marginfi/src/constants.rs b/programs/marginfi/src/constants.rs index 825a97ad0..2dd85528d 100644 --- a/programs/marginfi/src/constants.rs +++ b/programs/marginfi/src/constants.rs @@ -40,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; diff --git a/programs/marginfi/tests/admin_actions/setup_bank.rs b/programs/marginfi/tests/admin_actions/setup_bank.rs index 2d7eade8a..0fefad94e 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) @@ -106,6 +121,22 @@ async fn add_bank_success() -> anyhow::Result<()> { // 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 @@ -209,6 +254,21 @@ async fn add_bank_with_seed_success() -> anyhow::Result<()> { // 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/scripts/single-test.sh b/scripts/single-test.sh index f3174c35d..926a0e555 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 bcc54384d..9701eb6e3 100644 --- a/test-utils/src/marginfi_group.rs +++ b/test-utils/src/marginfi_group.rs @@ -4,7 +4,7 @@ use crate::utils::*; use anchor_lang::{prelude::*, solana_program::system_program, InstructionData}; use anyhow::Result; -use marginfi::constants::FEE_STATE_SEED; +use marginfi::constants::{FEE_STATE_SEED, INIT_BANK_ORIGINATION_FEE_DEFAULT}; use marginfi::state::fee_state::FeeState; use marginfi::{ prelude::MarginfiGroup, @@ -12,78 +12,24 @@ use marginfi::{ }; 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}; -pub struct FeeStateFixture { - pub fee_state: Pubkey, - pub fee_wallet: Pubkey, -} - -impl FeeStateFixture { - pub async fn new(ctx: Rc>) -> FeeStateFixture { - let (fee_state_key, _bump) = - Pubkey::find_program_address(&[FEE_STATE_SEED.as_bytes()], &marginfi::id()); - - { - let mut ctx = ctx.borrow_mut(); - - // Skip setup if the fee state was already initialized - let fee_state_account = ctx.banks_client.get_account(fee_state_key).await.unwrap(); - - if let Some(account) = fee_state_account { - if !account.data.is_empty() { - let fee_state_data: FeeState = - FeeState::try_deserialize(&mut &account.data[..]).unwrap(); - return FeeStateFixture { - fee_state: fee_state_key, - fee_wallet: fee_state_data.global_fee_wallet, - }; - } - } - - let fee_wallet = Keypair::new(); - - 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: 10000, - } - .data(), - }; - - let tx = Transaction::new_signed_with_payer( - &[init_fee_state_ix], - Some(&ctx.payer.pubkey().clone()), - &[&ctx.payer], - ctx.last_blockhash, - ); - ctx.banks_client.process_transaction(tx).await.unwrap(); - - FeeStateFixture { - fee_state: fee_state_key, - fee_wallet: fee_wallet.pubkey(), - } - } - } +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_fixture: FeeStateFixture + pub fee_state: Pubkey, + pub fee_wallet: Pubkey, } impl MarginfiGroupFixture { @@ -94,6 +40,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(); @@ -119,19 +68,70 @@ 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, + } + .data(), + }; + + let tx = Transaction::new_signed_with_payer( + &[ + initialize_marginfi_group_ix, + configure_marginfi_group_ix, + init_fee_state_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_fixture: FeeStateFixture::new(ctx).await + fee_state: fee_state_key, + fee_wallet: fee_wallet_key, } } @@ -149,8 +149,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_fixture.fee_state, - global_fee_wallet: self.fee_state_fixture.fee_wallet, + 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, @@ -228,8 +228,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_fixture.fee_state, - global_fee_wallet: self.fee_state_fixture.fee_wallet, + 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, diff --git a/tests/03_addBank.spec.ts b/tests/03_addBank.spec.ts index d5597b6b0..e818118ed 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, @@ -38,6 +40,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 +52,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); @@ -141,6 +157,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 bf72966a7..4c2378b23 100644 --- a/tests/rootHooks.ts +++ b/tests/rootHooks.ts @@ -10,8 +10,15 @@ 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"; export const ecosystem: Ecosystem = getGenericEcosystem(); export let oracles: Oracles = undefined; @@ -19,9 +26,13 @@ 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; + /** Group used for all happy-path tests */ export const marginfiGroup = Keypair.generate(); /** Bank for USDC */ @@ -70,6 +81,27 @@ 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, + }) + ); + await provider.sendAndConfirm(tx, [usdcMint, aMint, bMint]); const setupUserOptions: SetupTestUserOptions = { diff --git a/tests/utils/instructions.ts b/tests/utils/instructions.ts index 10c1f9241..dad19daaf 100644 --- a/tests/utils/instructions.ts +++ b/tests/utils/instructions.ts @@ -51,6 +51,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 +61,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); @@ -121,3 +124,49 @@ export const groupInitialize = ( return ix; }; + +export type InitGlobalFeeStateArgs = { + payer: PublicKey; + admin: PublicKey; + wallet: PublicKey; + bankInitFlatSolFee: number; +}; + +export const initGlobalFeeState = ( + program: Program, + args: InitGlobalFeeStateArgs +) => { + const ix = program.methods + .initGlobalFeeState(args.admin, args.wallet, args.bankInitFlatSolFee) + .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; +}; + +// TODO add test for this +export const editGlobalFeeState = ( + program: Program, + args: EditGlobalFeeStateArgs +) => { + const ix = program.methods + .editGlobalFeeState(args.wallet, args.bankInitFlatSolFee) + .accounts({ + globalFeeAdmin: args.admin, + // feeState = deriveGlobalFeeState(id), + }) + .instruction(); + + return ix; +}; diff --git a/tests/utils/pdas.ts b/tests/utils/pdas.ts index 2594ccfad..028b44310 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 + ); +};