Skip to content

Commit

Permalink
Program: charge collateral fee directly on borrowed tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
farnyser committed Jun 25, 2024
1 parent 905cc01 commit 2f0768f
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 64 deletions.
37 changes: 29 additions & 8 deletions programs/mango-v4/src/instructions/token_charge_collateral_fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) ->

let scaling = asset_usage_scaling * time_scaling;

let mut total_collateral_fees_in_usd = I80F48::ZERO;

let token_position_count = account.active_token_positions().count();
for bank_ai in &ctx.remaining_accounts[0..token_position_count] {
let mut bank = bank_ai.load_mut::<Bank>()?;
Expand All @@ -103,16 +105,11 @@ pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) ->
let fee = token_balance * scaling * I80F48::from_num(bank.collateral_fee_per_day);
assert!(fee <= token_balance);

let is_active = bank.withdraw_without_fee(token_position, fee, now_ts)?;
if !is_active {
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
}
let token_info = health_cache.token_info(bank.token_index)?;

bank.collected_fees_native += fee;
bank.collected_collateral_fees += fee;
total_collateral_fees_in_usd += fee * token_info.prices.oracle;

let token_info = health_cache.token_info(bank.token_index)?;
let token_position = account.token_position(bank.token_index)?;
bank.collected_collateral_fees += fee;

emit_stack(TokenCollateralFeeLog {
mango_group: ctx.accounts.group.key(),
Expand All @@ -122,6 +119,30 @@ pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) ->
asset_usage_fraction: asset_usage_scaling.to_bits(),
price: token_info.prices.oracle.to_bits(),
});
}

for bank_ai in &ctx.remaining_accounts[0..token_position_count] {
let mut bank = bank_ai.load_mut::<Bank>()?;

let (token_position, raw_token_index) = account.token_position_mut(bank.token_index)?;
let token_balance = token_position.native(&bank);
let token_info = health_cache.token_info(bank.token_index)?;
let health = token_info.health_contribution(HealthType::Maint, token_balance);

if health >= 0 {
continue;
}

let borrow_scaling = (health / total_liab_health).abs();
let fee = borrow_scaling * total_collateral_fees_in_usd / token_info.prices.oracle;

let is_active = bank.withdraw_without_fee(token_position, fee, now_ts)?;
if !is_active {
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
}

bank.collected_fees_native += fee;
let token_position = account.token_position(bank.token_index)?;

emit_stack(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
Expand Down
270 changes: 214 additions & 56 deletions programs/mango-v4/tests/cases/test_collateral_fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use super::*;
use anchor_spl::token::accessor::mint;
use std::collections::HashMap;
use std::sync::Arc;

#[tokio::test]
async fn test_collateral_fees() -> Result<(), TransportError> {
Expand Down Expand Up @@ -82,37 +83,8 @@ async fn test_collateral_fees() -> Result<(), TransportError> {
.await
.unwrap();

send_tx(
solana,
TokenEdit {
group,
admin,
mint: mints[0].pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
collateral_fee_per_day_opt: Some(0.1),
..token_edit_instruction_default()
},
},
)
.await
.unwrap();

send_tx(
solana,
TokenEdit {
group,
admin,
mint: mints[1].pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
loan_origination_fee_rate_opt: Some(0.0),
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
set_collateral_fees(solana, admin, mints, group, 0, 0.1).await;
set_loan_orig_fee(solana, admin, mints, group, 1, 0.0).await;

//
// TEST: It works on empty accounts
Expand Down Expand Up @@ -166,62 +138,248 @@ async fn test_collateral_fees() -> Result<(), TransportError> {
// TEST: With borrows, there's an effect depending on the time that has passed
//

send_tx(
solana,
TokenWithdrawInstruction {
amount: 500, // maint: -1.2 * 500 = -600 (half of 1200)
allow_borrow: true,
account,
owner,
token_account: context.users[1].token_accounts[1],
bank_index: 0,
},
)
.await
.unwrap();
withdraw(&context, solana, owner, account, 500, 1).await; // maint: -1.2 * 500 = -600 (half of 1200)

solana.set_clock_timestamp(last_time + 9 * hour).await;

send_tx(solana, TokenChargeCollateralFeesInstruction { account })
.await
.unwrap();
last_time = solana.clock_timestamp().await;

let fee = 1500.0 * (0.1 * (9.0 / 24.0) * (600.0 / 1200.0));
println!("fee -> {}", fee);
assert_eq_f64!(
account_position_f64(solana, account, tokens[0].bank).await,
1500.0 * (1.0 - 0.1 * (9.0 / 24.0) * (600.0 / 1200.0)),
1500.0,
0.01
);
assert_eq_f64!(
account_position_f64(solana, account, tokens[1].bank).await,
-500.0 - fee,
0.01
);
let last_balance = account_position_f64(solana, account, tokens[0].bank).await;
let last_balance = account_position_f64(solana, account, tokens[1].bank).await;

//
// TEST: More borrows
//

withdraw(&context, solana, owner, account, 100, 1).await; // maint: -1.2 * 600 = -720

solana.set_clock_timestamp(last_time + 7 * hour).await;

send_tx(solana, TokenChargeCollateralFeesInstruction { account })
.await
.unwrap();
//last_time = solana.clock_timestamp().await;
let fee = 1500.0 * 0.1 * (7.0 / 24.0) * ((last_balance.abs() + 100.0) * 1.2 / (1500.0 * 0.8));
println!("fee -> {}", fee);
assert_eq_f64!(
account_position_f64(solana, account, tokens[0].bank).await,
1500.0,
0.01
);
assert_eq_f64!(
account_position_f64(solana, account, tokens[1].bank).await,
-(last_balance.abs() + 100.0) - fee,
0.01
);

Ok(())
}

#[tokio::test]
async fn test_collateral_fees_multi() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();

let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..4];
let mut prices = HashMap::new();

prices.insert(mints[0].pubkey, 1_000_000f64); // 1 unit = 1$
prices.insert(mints[1].pubkey, 3_000_000f64); // 1 unit = 3$
prices.insert(mints[2].pubkey, 5_000_000f64); // 1 unit = 5$
prices.insert(mints[3].pubkey, 20_000_000f64); // 1 unit = 20$

let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
prices,
..mango_setup::GroupWithTokensConfig::default()
}
.create(solana)
.await;

// fund the vaults to allow borrowing
create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
1_000_000,
0,
)
.await;

let account = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[0..2],
1_500, // maint: 0.8 * 1500 = 1200
0,
)
.await;

let hour = 60 * 60;

send_tx(
solana,
TokenWithdrawInstruction {
amount: 100, // maint: -1.2 * 600 = -720
allow_borrow: true,
account,
owner,
token_account: context.users[1].token_accounts[1],
bank_index: 0,
GroupEdit {
group,
admin,
options: mango_v4::instruction::GroupEdit {
collateral_fee_interval_opt: Some(6 * hour),
..group_edit_instruction_default()
},
},
)
.await
.unwrap();

solana.set_clock_timestamp(last_time + 7 * hour).await;
// Set fees

set_collateral_fees(solana, admin, mints, group, 0, 0.1).await;
set_collateral_fees(solana, admin, mints, group, 1, 0.2).await;
set_loan_orig_fee(solana, admin, mints, group, 2, 0.0).await;
set_loan_orig_fee(solana, admin, mints, group, 3, 0.0).await;

//
// TEST: With borrows, there's an effect depending on the time that has passed
//

withdraw(&context, solana, owner, account, 50, 2).await; // maint: -1.2 * 50 = -60 (250$ -> 300$)
withdraw(&context, solana, owner, account, 100, 3).await; // maint: -1.2 * 100 = -120 (2000$ -> 2400$)

send_tx(solana, TokenChargeCollateralFeesInstruction { account })
.await
.unwrap();
//last_time = solana.clock_timestamp().await;
let mut last_time = solana.clock_timestamp().await;
solana.set_clock_timestamp(last_time + 9 * hour).await;

// send it twice, because the first time will never charge anything
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
.await
.unwrap();
last_time = solana.clock_timestamp().await;

let usage_factor = (60.0 * 5.0 + 120.0 * 20.0) / ((1500.0 + 1500.0 * 3.0) * 0.8);
let time_factor = 9.0 / 24.0;
let collateral_fee_factor = 1500.0 * 0.1 + 1500.0 * 3.0 * 0.2;
let collateral_fee = collateral_fee_factor * time_factor * usage_factor;
println!("fee -> {}", collateral_fee);
assert_eq_f64!(
account_position_f64(solana, account, tokens[0].bank).await,
last_balance * (1.0 - 0.1 * (7.0 / 24.0) * (720.0 / (last_balance * 0.8))),
1500.0,
0.01
);
assert_eq_f64!(
account_position_f64(solana, account, tokens[1].bank).await,
1500.0,
0.01
);
assert_eq_f64!(
account_position_f64(solana, account, tokens[2].bank).await,
-50.0 - (300.0 / 2700.0) * collateral_fee / 5.0,
0.01
);
assert_eq_f64!(
account_position_f64(solana, account, tokens[3].bank).await,
-100.0 - (2400.0 / 2700.0) * collateral_fee / 20.0,
0.01
);

Ok(())
}

async fn withdraw(
context: &TestContext,
solana: &Arc<SolanaCookie>,
owner: TestKeypair,
account: Pubkey,
amount: u64,
token_index: usize,
) {
send_tx(
solana,
TokenWithdrawInstruction {
amount: amount,
allow_borrow: true,
account,
owner,
token_account: context.users[1].token_accounts[token_index],
bank_index: 0,
},
)
.await
.unwrap();
}

async fn set_loan_orig_fee(
solana: &Arc<SolanaCookie>,
admin: TestKeypair,
mints: &[MintCookie],
group: Pubkey,
token_index: usize,
rate: f32,
) {
send_tx(
solana,
TokenEdit {
group,
admin,
mint: mints[token_index].pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
loan_origination_fee_rate_opt: Some(rate),
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
}

async fn set_collateral_fees(
solana: &Arc<SolanaCookie>,
admin: TestKeypair,
mints: &[MintCookie],
group: Pubkey,
token_index: usize,
rate: f32,
) {
send_tx(
solana,
TokenEdit {
group,
admin,
mint: mints[token_index].pubkey,
fallback_oracle: Pubkey::default(),
options: mango_v4::instruction::TokenEdit {
collateral_fee_per_day_opt: Some(rate),
..token_edit_instruction_default()
},
},
)
.await
.unwrap();
}

0 comments on commit 2f0768f

Please sign in to comment.