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 16 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
37 changes: 35 additions & 2 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 @@ -56,10 +57,16 @@ pub fn lending_account_borrow<'info>(
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 +87,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 +126,20 @@ 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 bank fee account gains the origination fee
{
let mut bank = bank_loader.load_mut()?;
let bank_fees_before: I80F48 = bank.collected_group_fees_outstanding.into();
let bank_fees_after: I80F48 = if origination_fee.is_zero() {
bank_fees_before
} else {
bank_fees_before.saturating_add(origination_fee)
};
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
31 changes: 29 additions & 2 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 @@ -85,6 +86,11 @@ async fn marginfi_account_borrow_success(
.balance()
.await;
let pre_user_debt_accounted = I80F48::ZERO;
let pre_fee_balance: I80F48 = debt_bank_f
.load()
.await
.collected_group_fees_outstanding
.into();

let res = user_mfi_account_f
.try_bank_borrow(user_debt_token_account_f.key, debt_bank_f, borrow_amount)
Expand Down Expand Up @@ -119,6 +125,16 @@ async fn marginfi_account_borrow_success(
})
.unwrap_or(0);
let borrow_amount_pre_fee = borrow_amount_native + borrow_fee;
let origination_fee_rate: I80F48 = debt_bank_f
.load() // ?? could optimize load calls in this test?
.await
.config
.interest_rate_config
.protocol_origination_fee
.into();
let origination_fee: I80F48 = I80F48::from_num(borrow_amount_native)
.checked_mul(origination_fee_rate)
.unwrap();

let active_balance_count = marginfi_account
.lending_account
Expand All @@ -130,13 +146,24 @@ 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 post_fee_balance: I80F48 = debt_bank_f
.load()
.await
.collected_group_fees_outstanding
.into();
assert_eq!(pre_fee_balance + origination_fee, post_fee_balance);

Ok(())
}

Expand All @@ -147,7 +174,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
23 changes: 21 additions & 2 deletions programs/marginfi/tests/user_actions/repay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use test_case::test_case;
#[test_case(128932., 9834., BankMint::PyUSD, BankMint::SolSwb)]
#[test_case(240., 0.092, BankMint::PyUSD, BankMint::T22WithFee)]
#[test_case(36., 20., BankMint::T22WithFee, BankMint::Sol)]
#[test_case(200., 1.1, BankMint::Usdc, BankMint::SolSwbOrigFee)] // Sol @ ~ $153
#[tokio::test]
async fn marginfi_account_repay_success(
borrow_amount: f64,
Expand Down Expand Up @@ -142,6 +143,7 @@ async fn marginfi_account_repay_success(
#[test_case(128932., BankMint::PyUSD, BankMint::SolSwb)]
#[test_case(240., BankMint::PyUSD, BankMint::T22WithFee)]
#[test_case(36., BankMint::T22WithFee, BankMint::Sol)]
#[test_case(200., BankMint::Usdc, BankMint::SolSwbOrigFee)] // Sol @ ~ $153
#[tokio::test]
async fn marginfi_account_repay_all_success(
borrow_amount: f64,
Expand Down Expand Up @@ -261,8 +263,25 @@ async fn marginfi_account_repay_all_success(
})
.unwrap_or(0);

let expected_liquidity_delta =
I80F48::from(native!(borrow_amount, debt_bank.mint.mint.decimals, f64) + borrow_fee);
let origination_fee_rate: I80F48 = debt_bank
.load()
.await
.config
.interest_rate_config
.protocol_origination_fee
.into();
let origination_fee: I80F48 =
I80F48::from_num(native!(borrow_amount, debt_bank.mint.mint.decimals, f64))
.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 expected_liquidity_delta = I80F48::from(
native!(borrow_amount, debt_bank.mint.mint.decimals, f64)
+ borrow_fee
+ origination_fee_u64,
);
let actual_liquidity_delta = I80F48::from(post_vault_balance) - I80F48::from(pre_vault_balance);
let accounted_liquidity_delta = post_accounted_vault_balance - pre_accounted_vault_balance;

Expand Down
Loading
Loading