Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Origination fees on borrow #239

Merged
merged 18 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,12 @@ use an x86 toolchain when compiling and running the tests.

## Rust Tests

Run the full test suite with `.scripts/test-program.sh <program_to_test>`

- e.g. `.scripts/test-program.sh all --sane`
Run the full test suite with `./scripts/test-program.sh <program_to_test>`
* e.g. `./scripts/test-program.sh all --sane`

Run a single test:
`.scripts/test-program.sh <program_to_test> <name_of_test>`

- e.g. `.scripts/test-program.sh marginfi configure_bank_success --verbose`
`./scripts/test-program.sh <program_to_test> <name_of_test>`
* e.g. `./scripts/test-program.sh marginfi configure_bank_success --verbose`

## Localnet Anchor Tests

Expand Down
4 changes: 4 additions & 0 deletions clients/rust/marginfi-cli/src/entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ pub enum BankCommand {
pf_fa: Option<f64>,
#[clap(long, help = "Protocol IR fee")]
pf_ir: Option<f64>,
#[clap(long, help = "Protocol origination fee")]
pf_or: Option<f64>,
#[clap(long, arg_enum, help = "Bank risk tier")]
risk_tier: Option<RiskTierArg>,
#[clap(long, arg_enum, help = "Bank oracle type")]
Expand Down Expand Up @@ -669,6 +671,7 @@ fn bank(subcmd: BankCommand, global_options: &GlobalOptions) -> Result<()> {
if_ir,
pf_fa,
pf_ir,
pf_or,
risk_tier,
oracle_type,
oracle_key,
Expand Down Expand Up @@ -718,6 +721,7 @@ fn bank(subcmd: BankCommand, global_options: &GlobalOptions) -> Result<()> {
insurance_ir_fee: if_ir.map(|x| I80F48::from_num(x).into()),
protocol_fixed_fee_apr: pf_fa.map(|x| I80F48::from_num(x).into()),
protocol_ir_fee: pf_ir.map(|x| I80F48::from_num(x).into()),
protocol_origination_fee: pf_or.map(|x| I80F48::from_num(x).into()),
}),
risk_tier: risk_tier.map(|x| x.into()),
total_asset_value_init_limit: usd_init_limit,
Expand Down
60 changes: 57 additions & 3 deletions programs/marginfi/src/instructions/marginfi_account/borrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{
bank_signer, check,
constants::{LIQUIDITY_VAULT_AUTHORITY_SEED, LIQUIDITY_VAULT_SEED},
events::{AccountEventHeader, LendingAccountBorrowEvent},
math_error,
prelude::{MarginfiError, MarginfiGroup, MarginfiResult},
state::{
marginfi_account::{BankAccountWrapper, MarginfiAccount, RiskEngine, DISABLED_FLAG},
Expand Down Expand Up @@ -43,6 +44,8 @@ pub fn lending_account_borrow<'info>(
)?;

let mut marginfi_account = marginfi_account_loader.load_mut()?;
let group = &marginfi_group_loader.load()?;
let program_fee_rate: I80F48 = group.fee_state_cache.program_fee_rate.into();

check!(
!marginfi_account.get_flag(DISABLED_FLAG),
Expand All @@ -51,15 +54,21 @@ pub fn lending_account_borrow<'info>(

bank_loader.load_mut()?.accrue_interest(
clock.unix_timestamp,
&*marginfi_group_loader.load()?,
group,
#[cfg(not(feature = "client"))]
bank_loader.key(),
)?;

let mut origination_fee: I80F48 = I80F48::ZERO;
{
let mut bank = bank_loader.load_mut()?;

let liquidity_vault_authority_bump = bank.liquidity_vault_authority_bump;
let origination_fee_rate: I80F48 = bank
.config
.interest_rate_config
.protocol_origination_fee
.into();

let mut bank_account = BankAccountWrapper::find_or_create(
&bank_loader.key(),
Expand All @@ -80,7 +89,21 @@ pub fn lending_account_borrow<'info>(
.transpose()?
.unwrap_or(amount);

bank_account.borrow(I80F48::from_num(amount_pre_fee))?;
let origination_fee_u64: u64;
if !origination_fee_rate.is_zero() {
origination_fee = I80F48::from_num(amount_pre_fee)
.checked_mul(origination_fee_rate)
.ok_or_else(math_error!())?;
origination_fee_u64 = origination_fee.checked_to_num().ok_or_else(math_error!())?;

// Incurs a borrow that includes the origination fee (but withdraws just the amt)
bank_account.borrow(I80F48::from_num(amount_pre_fee) + origination_fee)?;
} else {
// Incurs a borrow for the amount without any fee
origination_fee_u64 = 0;
bank_account.borrow(I80F48::from_num(amount_pre_fee))?;
}

bank_account.withdraw_spl_transfer(
amount_pre_fee,
bank_liquidity_vault.to_account_info(),
Expand All @@ -105,8 +128,39 @@ pub fn lending_account_borrow<'info>(
},
bank: bank_loader.key(),
mint: bank.mint,
amount: amount_pre_fee,
amount: amount_pre_fee + origination_fee_u64,
});
} // release mutable borrow of bank

// The program and/or group fee account gains the origination fee
{
let mut bank = bank_loader.load_mut()?;

if !origination_fee.is_zero() {
let mut bank_fees_after: I80F48 = bank.collected_group_fees_outstanding.into();

if !program_fee_rate.is_zero() {
// Some portion of the origination fee to goes to program fees
let program_fee_amount: I80F48 = origination_fee
.checked_mul(program_fee_rate)
.ok_or_else(math_error!())?;
// The remainder of the origination fee goes to group fees
bank_fees_after = bank_fees_after
.saturating_add(origination_fee.saturating_sub(program_fee_amount));

// Update the bank's program fees
let program_fees_before: I80F48 = bank.collected_program_fees_outstanding.into();
bank.collected_program_fees_outstanding = program_fees_before
.saturating_add(program_fee_amount)
.into();
} else {
// If program fee rate is zero, add the full origination fee to group fees
bank_fees_after = bank_fees_after.saturating_add(origination_fee);
}

// Update the bank's group fees
bank.collected_group_fees_outstanding = bank_fees_after.into();
}
}

// Check account health, if below threshold fail transaction
Expand Down
39 changes: 18 additions & 21 deletions programs/marginfi/src/state/marginfi_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,29 +151,22 @@ pub struct InterestRateConfigCompact {
pub insurance_ir_fee: WrappedI80F48,
pub protocol_fixed_fee_apr: WrappedI80F48,
pub protocol_ir_fee: WrappedI80F48,
pub protocol_origination_fee: WrappedI80F48,
}

impl From<InterestRateConfigCompact> for InterestRateConfig {
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],
fn from(ir_config: InterestRateConfigCompact) -> Self {
InterestRateConfig {
optimal_utilization_rate: ir_config.optimal_utilization_rate,
plateau_interest_rate: ir_config.plateau_interest_rate,
max_interest_rate: ir_config.max_interest_rate,
insurance_fee_fixed_apr: ir_config.insurance_fee_fixed_apr,
insurance_ir_fee: ir_config.insurance_ir_fee,
protocol_fixed_fee_apr: ir_config.protocol_fixed_fee_apr,
protocol_ir_fee: ir_config.protocol_ir_fee,
protocol_origination_fee: ir_config.protocol_origination_fee,
_padding0: [0; 16],
_padding1: [[0; 32]; 3],
}
}
}
Expand All @@ -188,6 +181,7 @@ impl From<InterestRateConfig> for InterestRateConfigCompact {
insurance_ir_fee: ir_config.insurance_ir_fee,
protocol_fixed_fee_apr: ir_config.protocol_fixed_fee_apr,
protocol_ir_fee: ir_config.protocol_ir_fee,
protocol_origination_fee: ir_config.protocol_origination_fee,
}
}
}
Expand Down Expand Up @@ -215,8 +209,10 @@ pub struct InterestRateConfig {
pub protocol_fixed_fee_apr: WrappedI80F48,
/// Earned by the group, goes to `collected_group_fees_outstanding`
pub protocol_ir_fee: WrappedI80F48,
pub protocol_origination_fee: WrappedI80F48,

pub _padding: [u32; 32],
pub _padding0: [u8; 16],
pub _padding1: [[u8; 32]; 3],
}

impl InterestRateConfig {
Expand Down Expand Up @@ -418,6 +414,7 @@ pub struct InterestRateConfigOpt {
pub insurance_ir_fee: Option<WrappedI80F48>,
pub protocol_fixed_fee_apr: Option<WrappedI80F48>,
pub protocol_ir_fee: Option<WrappedI80F48>,
pub protocol_origination_fee: Option<WrappedI80F48>,
}

/// Group level configuration to be used in bank accounts.
Expand Down
13 changes: 13 additions & 0 deletions programs/marginfi/tests/admin_actions/bankruptcy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ async fn marginfi_group_handle_bankruptcy_success(
#[test_case(10_000., BankMint::PyUSD, BankMint::SolSwb)]
#[test_case(10_000., BankMint::PyUSD, BankMint::T22WithFee)]
#[test_case(10_000., BankMint::T22WithFee, BankMint::Sol)]
#[test_case(10_000., BankMint::Usdc, BankMint::SolSwbOrigFee)] // Sol @ ~ $153
#[tokio::test]
async fn marginfi_group_handle_bankruptcy_success_fully_insured(
borrow_amount: f64,
Expand Down Expand Up @@ -447,7 +448,18 @@ async fn marginfi_group_handle_bankruptcy_success_fully_insured(
test_f.get_bank(&debt_mint).mint.mint.decimals,
f64
);
let origination_fee_rate: I80F48 = debt_bank
.config
.interest_rate_config
.protocol_origination_fee
.into();
let origination_fee: I80F48 = I80F48::from_num(borrow_amount_native)
.checked_mul(origination_fee_rate)
.unwrap()
.ceil(); // Round up when repaying
let origination_fee_u64: u64 = origination_fee.checked_to_num().expect("out of bounds");
let actual_borrow_position = borrow_amount_native
+ origination_fee_u64
+ debt_bank_mint_state
.get_extension::<TransferFeeConfig>()
.map(|tf| {
Expand All @@ -466,6 +478,7 @@ async fn marginfi_group_handle_bankruptcy_success_fully_insured(
})
.unwrap_or(0),
);

let expected_liquidity_vault_delta = I80F48::from(actual_borrow_position);

let actual_liquidity_vault_delta = post_liquidity_vault_balance - pre_liquidity_vault_balance;
Expand Down
1 change: 1 addition & 0 deletions programs/marginfi/tests/admin_actions/setup_bank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ async fn configure_bank_success(bank_mint: BankMint) -> anyhow::Result<()> {
insurance_ir_fee: Some(I80F48::from_num(0.11).into()),
protocol_fixed_fee_apr: Some(I80F48::from_num(0.51).into()),
protocol_ir_fee: Some(I80F48::from_num(0.011).into()),
protocol_origination_fee: Some(I80F48::ZERO.into()),
}),
..BankConfigOpt::default()
};
Expand Down
46 changes: 41 additions & 5 deletions programs/marginfi/tests/user_actions/borrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use test_case::test_case;
#[test_case(128932.0, 9834.0, BankMint::PyUSD, BankMint::SolSwb)]
#[test_case(240., 0.092, BankMint::PyUSD, BankMint::T22WithFee)]
#[test_case(36., 1.7, BankMint::T22WithFee, BankMint::Sol)]
#[test_case(200., 1.1, BankMint::Usdc, BankMint::SolSwbOrigFee)] // Sol @ ~ $153
#[tokio::test]
async fn marginfi_account_borrow_success(
deposit_amount: f64,
Expand Down Expand Up @@ -78,13 +79,16 @@ async fn marginfi_account_borrow_success(
// -------------------------------------------------------------------------

let debt_bank_f = test_f.get_bank(&debt_mint);
let bank_before = debt_bank_f.load().await;

let pre_vault_balance = debt_bank_f
.get_vault_token_account(BankVaultType::Liquidity)
.await
.balance()
.await;
let pre_user_debt_accounted = I80F48::ZERO;
let pre_fee_group_fees: I80F48 = bank_before.collected_group_fees_outstanding.into();
let pre_fee_program_fees: I80F48 = bank_before.collected_program_fees_outstanding.into();

let res = user_mfi_account_f
.try_bank_borrow(user_debt_token_account_f.key, debt_bank_f, borrow_amount)
Expand All @@ -101,9 +105,7 @@ async fn marginfi_account_borrow_success(
.lending_account
.get_balance(&debt_bank_f.key)
.unwrap();
let post_user_debt_accounted = debt_bank_f
.load()
.await
let post_user_debt_accounted = bank_before
.get_asset_amount(balance.liability_shares.into())
.unwrap();

Expand All @@ -119,6 +121,23 @@ async fn marginfi_account_borrow_success(
})
.unwrap_or(0);
let borrow_amount_pre_fee = borrow_amount_native + borrow_fee;
let origination_fee_rate: I80F48 = bank_before
.config
.interest_rate_config
.protocol_origination_fee
.into();
let program_fee_rate: I80F48 = test_f
.marginfi_group
.load()
.await
.fee_state_cache
.program_fee_rate
.into();
let origination_fee: I80F48 = I80F48::from_num(borrow_amount_native)
.checked_mul(origination_fee_rate)
.unwrap();
let program_origination_fee: I80F48 = origination_fee.checked_mul(program_fee_rate).unwrap();
let group_origination_fee: I80F48 = origination_fee.saturating_sub(program_origination_fee);

let active_balance_count = marginfi_account
.lending_account
Expand All @@ -130,13 +149,30 @@ async fn marginfi_account_borrow_success(
let actual_liquidity_vault_delta = post_vault_balance as i64 - pre_vault_balance as i64;
let accounted_user_balance_delta = post_user_debt_accounted - pre_user_debt_accounted;

// The liquidity vault paid out just the pre-origination fee amount (e.g. what the user borrowed
// before accounting for the fee)
assert_eq!(expected_liquidity_vault_delta, actual_liquidity_vault_delta);
assert_eq_with_tolerance!(
I80F48::from(expected_liquidity_vault_delta),
// Note: the user still gains debt which includes the origination fee
I80F48::from(expected_liquidity_vault_delta) - origination_fee,
-accounted_user_balance_delta,
1
);

// The outstanding origination fee is recorded
let bank_after = debt_bank_f.load().await;
let post_fee_program_fees: I80F48 = bank_after.collected_program_fees_outstanding.into();
assert_eq!(
pre_fee_program_fees + program_origination_fee,
post_fee_program_fees
);

let post_fee_group_fees: I80F48 = bank_after.collected_group_fees_outstanding.into();
assert_eq!(
pre_fee_group_fees + group_origination_fee,
post_fee_group_fees
);

Ok(())
}

Expand All @@ -147,7 +183,7 @@ async fn marginfi_account_borrow_success(
#[test_case(128_932., 10_000., 15_000.0, BankMint::PyUSD, BankMint::SolSwb)]
#[test_case(240., 0.092, 500., BankMint::PyUSD, BankMint::T22WithFee)]
#[test_case(36., 1.7, 1.9, BankMint::T22WithFee, BankMint::Sol)]
#[test_case(1., 100., 155.1, BankMint::SolSwbPull, BankMint::Usdc)] // Sol @ $155
#[test_case(1., 100., 155.1, BankMint::SolSwbPull, BankMint::Usdc)] // Sol @ ~ $153
#[tokio::test]
async fn marginfi_account_borrow_failure_not_enough_collateral(
deposit_amount: f64,
Expand Down
Loading
Loading