Add collateral fees (#868)

- New permissionless instruction to regularly charge collateral fees
- Bank and group configuration to set rate and interval
- Keeper addition to call the instruction
This commit is contained in:
Christian Kamm 2024-02-13 12:39:28 +01:00 committed by GitHub
parent ae833621ad
commit e57dcdc2a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1024 additions and 29 deletions

View File

@ -1,12 +1,27 @@
use std::{collections::HashSet, sync::Arc, time::Duration, time::Instant};
use std::{
collections::HashSet,
sync::Arc,
time::Instant,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use crate::MangoClient;
use anyhow::Context;
use itertools::Itertools;
use anchor_lang::{__private::bytemuck::cast_ref, solana_program};
use anchor_lang::{__private::bytemuck::cast_ref, solana_program, Discriminator};
use futures::Future;
use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, TokenIndex};
use mango_v4_client::PerpMarketContext;
use mango_v4::{
accounts_zerocopy::AccountReader,
state::{
EventQueue, EventType, FillEvent, Group, MangoAccount, MangoAccountValue, OutEvent,
TokenIndex,
},
};
use mango_v4_client::{
account_fetcher_fetch_anchor_account, AccountFetcher, PerpMarketContext, PreparedInstructions,
RpcAccountFetcher, TransactionBuilder,
};
use prometheus::{register_histogram, Encoder, Histogram, IntCounter, Registry};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
@ -81,6 +96,7 @@ pub async fn runner(
interval_consume_events: u64,
interval_update_funding: u64,
interval_check_for_changes_and_abort: u64,
interval_charge_collateral_fees: u64,
extra_jobs: Vec<JoinHandle<()>>,
) -> Result<(), anyhow::Error> {
let handles1 = mango_client
@ -140,6 +156,7 @@ pub async fn runner(
futures::future::join_all(handles1),
futures::future::join_all(handles2),
futures::future::join_all(handles3),
loop_charge_collateral_fees(mango_client.clone(), interval_charge_collateral_fees),
MangoClient::loop_check_for_context_changes_and_abort(
mango_client.clone(),
Duration::from_secs(interval_check_for_changes_and_abort),
@ -412,3 +429,122 @@ pub async fn loop_update_funding(
}
}
}
pub async fn loop_charge_collateral_fees(mango_client: Arc<MangoClient>, interval: u64) {
if interval == 0 {
return;
}
// Make a new one separate from the mango_client.account_fetcher,
// because we don't want cached responses
let fetcher = RpcAccountFetcher {
rpc: mango_client.client.new_rpc_async(),
};
let group: Group = account_fetcher_fetch_anchor_account(&fetcher, &mango_client.context.group)
.await
.unwrap();
let collateral_fee_interval = group.collateral_fee_interval;
let mut interval = mango_v4_client::delay_interval(Duration::from_secs(interval));
loop {
interval.tick().await;
match charge_collateral_fees_inner(&mango_client, &fetcher, collateral_fee_interval).await {
Ok(()) => {}
Err(err) => {
error!("charge_collateral_fees error: {err:?}");
}
}
}
}
async fn charge_collateral_fees_inner(
client: &MangoClient,
fetcher: &RpcAccountFetcher,
collateral_fee_interval: u64,
) -> anyhow::Result<()> {
let mango_accounts = fetcher
.fetch_program_accounts(&mango_v4::id(), MangoAccount::DISCRIMINATOR)
.await
.context("fetching mango accounts")?
.into_iter()
.filter_map(
|(pk, data)| match MangoAccountValue::from_bytes(&data.data()[8..]) {
Ok(acc) => Some((pk, acc)),
Err(err) => {
error!(pk=%pk, "charge_collateral_fees could not parse account: {err:?}");
None
}
},
);
let mut ix_to_send = Vec::new();
let now_ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as u64;
for (pk, account) in mango_accounts {
let should_reset =
collateral_fee_interval == 0 && account.fixed.last_collateral_fee_charge > 0;
let should_charge = collateral_fee_interval > 0
&& now_ts > account.fixed.last_collateral_fee_charge + collateral_fee_interval;
if !(should_reset || should_charge) {
continue;
}
let ixs = match client
.token_charge_collateral_fees_instruction((&pk, &account))
.await
{
Ok(ixs) => ixs,
Err(err) => {
error!(pk=%pk, "charge_collateral_fees could not build instruction: {err:?}");
continue;
}
};
ix_to_send.push(ixs);
}
send_batched_log_errors_no_confirm(
client.transaction_builder().await?,
&client.client,
&ix_to_send,
)
.await;
Ok(())
}
/// Try to batch the instructions into transactions and send them
async fn send_batched_log_errors_no_confirm(
mut tx_builder: TransactionBuilder,
client: &mango_v4_client::Client,
ixs_list: &[PreparedInstructions],
) {
let mut current_batch = PreparedInstructions::new();
for ixs in ixs_list {
let previous_batch = current_batch.clone();
current_batch.append(ixs.clone());
tx_builder.instructions = current_batch.clone().to_instructions();
if !tx_builder.transaction_size().is_ok() {
tx_builder.instructions = previous_batch.to_instructions();
match tx_builder.send(client).await {
Err(err) => error!("could not send transaction: {err:?}"),
_ => {}
}
current_batch = ixs.clone();
}
}
if !current_batch.is_empty() {
tx_builder.instructions = current_batch.to_instructions();
match tx_builder.send(client).await {
Err(err) => error!("could not send transaction: {err:?}"),
_ => {}
}
}
}

View File

@ -61,6 +61,9 @@ struct Cli {
#[clap(long, env, default_value_t = 120)]
interval_check_new_listings_and_abort: u64,
#[clap(long, env, default_value_t = 300)]
interval_charge_collateral_fees: u64,
#[clap(long, env, default_value_t = 10)]
timeout: u64,
@ -153,6 +156,7 @@ async fn main() -> Result<(), anyhow::Error> {
cli.interval_consume_events,
cli.interval_update_funding,
cli.interval_check_new_listings_and_abort,
cli.interval_charge_collateral_fees,
prio_jobs,
)
.await

View File

@ -1487,6 +1487,43 @@ impl MangoClient {
))
}
pub async fn token_charge_collateral_fees_instruction(
&self,
account: (&Pubkey, &MangoAccountValue),
) -> anyhow::Result<PreparedInstructions> {
let (mut health_remaining_ams, health_cu) = self
.derive_health_check_remaining_account_metas(account.1, vec![], vec![], vec![])
.await
.unwrap();
// The instruction requires mutable banks
for am in &mut health_remaining_ams[0..account.1.active_token_positions().count()] {
am.is_writable = true;
}
let ix = Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::TokenChargeCollateralFees {
group: self.group(),
account: *account.0,
},
None,
);
ams.extend(health_remaining_ams);
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::TokenChargeCollateralFees {},
),
};
Ok(PreparedInstructions::from_single(
ix,
self.instruction_cu(health_cu),
))
}
//
// Liquidation
//

View File

@ -277,6 +277,12 @@
"type": {
"option": "u16"
}
},
{
"name": "collateralFeeIntervalOpt",
"type": {
"option": "u64"
}
}
]
},
@ -635,6 +641,10 @@
{
"name": "disableAssetLiquidation",
"type": "bool"
},
{
"name": "collateralFeePerDay",
"type": "f32"
}
]
},
@ -1051,6 +1061,12 @@
"type": {
"option": "bool"
}
},
{
"name": "collateralFeePerDayOpt",
"type": {
"option": "f32"
}
}
]
},
@ -5963,6 +5979,25 @@
}
]
},
{
"name": "tokenChargeCollateralFees",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": []
},
{
"name": "altSet",
"accounts": [
@ -7531,12 +7566,30 @@
"defined": "I80F48"
}
},
{
"name": "collectedCollateralFees",
"docs": [
"Collateral fees that have been collected (in native tokens)",
"",
"See also collected_fees_native and fees_withdrawn."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "collateralFeePerDay",
"docs": [
"The daily collateral fees rate for fully utilized collateral."
],
"type": "f32"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1920
1900
]
}
}
@ -7664,12 +7717,28 @@
],
"type": "u16"
},
{
"name": "padding2",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "collateralFeeInterval",
"docs": [
"Intervals in which collateral fee is applied"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1812
1800
]
}
}
@ -7791,12 +7860,27 @@
],
"type": "u64"
},
{
"name": "temporaryDelegate",
"type": "publicKey"
},
{
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"docs": [
"Time at which the last collateral fee was charged"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
200
152
]
}
},
@ -9566,12 +9650,16 @@
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
160
152
]
}
}
@ -13699,6 +13787,36 @@
"index": false
}
]
},
{
"name": "TokenCollateralFeeLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "assetUsageFraction",
"type": "i128",
"index": false
},
{
"name": "fee",
"type": "i128",
"index": false
}
]
}
],
"errors": [

View File

@ -59,6 +59,7 @@ pub use stub_oracle_close::*;
pub use stub_oracle_create::*;
pub use stub_oracle_set::*;
pub use token_add_bank::*;
pub use token_charge_collateral_fees::*;
pub use token_conditional_swap_cancel::*;
pub use token_conditional_swap_create::*;
pub use token_conditional_swap_start::*;
@ -135,6 +136,7 @@ mod stub_oracle_close;
mod stub_oracle_create;
mod stub_oracle_set;
mod token_add_bank;
mod token_charge_collateral_fees;
mod token_conditional_swap_cancel;
mod token_conditional_swap_create;
mod token_conditional_swap_start;

View File

@ -0,0 +1,16 @@
use crate::error::MangoError;
use crate::state::*;
use anchor_lang::prelude::*;
/// Charges collateral fees on an account
#[derive(Accounts)]
pub struct TokenChargeCollateralFees<'info> {
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
}

View File

@ -19,6 +19,7 @@ pub fn group_edit(
mngo_token_index_opt: Option<TokenIndex>,
buyback_fees_expiry_interval_opt: Option<u64>,
allowed_fast_listings_per_interval_opt: Option<u16>,
collateral_fee_interval_opt: Option<u64>,
) -> Result<()> {
let mut group = ctx.accounts.group.load_mut()?;
@ -116,5 +117,14 @@ pub fn group_edit(
group.allowed_fast_listings_per_interval = allowed_fast_listings_per_interval;
}
if let Some(collateral_fee_interval) = collateral_fee_interval_opt {
msg!(
"Collateral fee interval old {:?}, new {:?}",
group.collateral_fee_interval,
collateral_fee_interval
);
group.collateral_fee_interval = collateral_fee_interval;
}
Ok(())
}

View File

@ -50,6 +50,7 @@ pub use stub_oracle_close::*;
pub use stub_oracle_create::*;
pub use stub_oracle_set::*;
pub use token_add_bank::*;
pub use token_charge_collateral_fees::*;
pub use token_conditional_swap_cancel::*;
pub use token_conditional_swap_create::*;
pub use token_conditional_swap_start::*;
@ -117,6 +118,7 @@ mod stub_oracle_close;
mod stub_oracle_create;
mod stub_oracle_set;
mod token_add_bank;
mod token_charge_collateral_fees;
mod token_conditional_swap_cancel;
mod token_conditional_swap_create;
mod token_conditional_swap_start;

View File

@ -0,0 +1,111 @@
use crate::accounts_zerocopy::*;
use crate::health::*;
use crate::state::*;
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use crate::accounts_ix::*;
use crate::logs::{emit_stack, TokenCollateralFeeLog};
pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) -> Result<()> {
let group = ctx.accounts.group.load()?;
let mut account = ctx.accounts.account.load_full_mut()?;
let now_ts: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
if group.collateral_fee_interval == 0 {
// By resetting, a new enabling of collateral fees will not immediately create a charge
account.fixed.last_collateral_fee_charge = 0;
return Ok(());
}
// When collateral fees are enabled the first time, don't immediately charge
if account.fixed.last_collateral_fee_charge == 0 {
account.fixed.last_collateral_fee_charge = now_ts;
return Ok(());
}
// Is the next fee-charging due?
let last_charge_ts = account.fixed.last_collateral_fee_charge;
if now_ts < last_charge_ts + group.collateral_fee_interval {
return Ok(());
}
account.fixed.last_collateral_fee_charge = now_ts;
// Charge the user at most for 2x the interval. So if no one calls this for a long time
// there won't be a huge charge based only on the end state.
let charge_seconds = (now_ts - last_charge_ts).min(2 * group.collateral_fee_interval);
// The fees are configured in "interest per day" so we need to get the fraction of days
// that has passed since the last update for scaling
let inv_seconds_per_day = I80F48::from_num(1.157407407407e-5); // 1 / (24 * 60 * 60)
let time_scaling = I80F48::from(charge_seconds) * inv_seconds_per_day;
let health_cache = {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
new_health_cache(&account.borrow(), &retriever, now_ts)?
};
// We want to find the total asset health and total liab health, but don't want
// to treat borrows that moved into open orders accounts as realized. Hence we
// pretend all spot orders are closed and settled and add their funds back to
// the token positions.
let mut token_balances = health_cache.effective_token_balances(HealthType::Maint);
for s3info in health_cache.serum3_infos.iter() {
token_balances[s3info.base_info_index].spot_and_perp += s3info.reserved_base;
token_balances[s3info.quote_info_index].spot_and_perp += s3info.reserved_quote;
}
let mut total_liab_health = I80F48::ZERO;
let mut total_asset_health = I80F48::ZERO;
for (info, balance) in health_cache.token_infos.iter().zip(token_balances.iter()) {
let health = info.health_contribution(HealthType::Maint, balance.spot_and_perp);
if health.is_positive() {
total_asset_health += health;
} else {
total_liab_health -= health;
}
}
// Users only pay for assets that are actively used to cover their liabilities.
let asset_usage_scaling = (total_liab_health / total_asset_health)
.max(I80F48::ZERO)
.min(I80F48::ONE);
let scaling = asset_usage_scaling * time_scaling;
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>()?;
if bank.collateral_fee_per_day <= 0.0 {
continue;
}
let (token_position, raw_token_index) = account.token_position_mut(bank.token_index)?;
let token_balance = token_position.native(&bank);
if token_balance <= 0 {
continue;
}
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());
}
bank.collected_fees_native += fee;
bank.collected_collateral_fees += fee;
emit_stack(TokenCollateralFeeLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index: bank.token_index,
fee: fee.to_bits(),
asset_usage_fraction: asset_usage_scaling.to_bits(),
})
}
Ok(())
}

View File

@ -54,6 +54,7 @@ pub fn token_edit(
zero_util_rate: Option<f32>,
platform_liquidation_fee: Option<f32>,
disable_asset_liquidation_opt: Option<bool>,
collateral_fee_per_day: Option<f32>,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
@ -483,7 +484,21 @@ pub fn token_edit(
platform_liquidation_fee
);
bank.platform_liquidation_fee = I80F48::from_num(platform_liquidation_fee);
require_group_admin = true;
if platform_liquidation_fee != 0.0 {
require_group_admin = true;
}
}
if let Some(collateral_fee_per_day) = collateral_fee_per_day {
msg!(
"Collateral fee per day old {:?}, new {:?}",
bank.collateral_fee_per_day,
collateral_fee_per_day
);
bank.collateral_fee_per_day = collateral_fee_per_day;
if collateral_fee_per_day != 0.0 {
require_group_admin = true;
}
}
if let Some(disable_asset_liquidation) = disable_asset_liquidation_opt {

View File

@ -45,6 +45,7 @@ pub fn token_register(
zero_util_rate: f32,
platform_liquidation_fee: f32,
disable_asset_liquidation: bool,
collateral_fee_per_day: f32,
) -> Result<()> {
// Require token 0 to be in the insurance token
if token_index == INSURANCE_TOKEN_INDEX {
@ -129,7 +130,9 @@ pub fn token_register(
zero_util_rate: I80F48::from_num(zero_util_rate),
platform_liquidation_fee: I80F48::from_num(platform_liquidation_fee),
collected_liquidation_fees: I80F48::ZERO,
reserved: [0; 1920],
collected_collateral_fees: I80F48::ZERO,
collateral_fee_per_day,
reserved: [0; 1900],
};
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;

View File

@ -108,7 +108,9 @@ pub fn token_register_trustless(
deposit_limit: 0,
zero_util_rate: I80F48::ZERO,
collected_liquidation_fees: I80F48::ZERO,
reserved: [0; 1920],
collected_collateral_fees: I80F48::ZERO,
collateral_fee_per_day: 0.0, // TODO
reserved: [0; 1900],
};
let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?;
if let Ok(oracle_price) = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None)

View File

@ -84,6 +84,7 @@ pub mod mango_v4 {
mngo_token_index_opt: Option<TokenIndex>,
buyback_fees_expiry_interval_opt: Option<u64>,
allowed_fast_listings_per_interval_opt: Option<u16>,
collateral_fee_interval_opt: Option<u64>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::group_edit(
@ -100,6 +101,7 @@ pub mod mango_v4 {
mngo_token_index_opt,
buyback_fees_expiry_interval_opt,
allowed_fast_listings_per_interval_opt,
collateral_fee_interval_opt,
)?;
Ok(())
}
@ -158,6 +160,7 @@ pub mod mango_v4 {
zero_util_rate: f32,
platform_liquidation_fee: f32,
disable_asset_liquidation: bool,
collateral_fee_per_day: f32,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_register(
@ -192,6 +195,7 @@ pub mod mango_v4 {
zero_util_rate,
platform_liquidation_fee,
disable_asset_liquidation,
collateral_fee_per_day,
)?;
Ok(())
}
@ -248,6 +252,7 @@ pub mod mango_v4 {
zero_util_rate_opt: Option<f32>,
platform_liquidation_fee_opt: Option<f32>,
disable_asset_liquidation_opt: Option<bool>,
collateral_fee_per_day_opt: Option<f32>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_edit(
@ -291,6 +296,7 @@ pub mod mango_v4 {
zero_util_rate_opt,
platform_liquidation_fee_opt,
disable_asset_liquidation_opt,
collateral_fee_per_day_opt,
)?;
Ok(())
}
@ -1609,6 +1615,12 @@ pub mod mango_v4 {
Ok(())
}
pub fn token_charge_collateral_fees(ctx: Context<TokenChargeCollateralFees>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::token_charge_collateral_fees(ctx)?;
Ok(())
}
pub fn alt_set(ctx: Context<AltSet>, index: u8) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::alt_set(ctx, index)?;

View File

@ -779,3 +779,12 @@ pub struct TokenConditionalSwapStartLog {
pub incentive_token_index: u16,
pub incentive_amount: u64,
}
#[event]
pub struct TokenCollateralFeeLog {
pub mango_group: Pubkey,
pub mango_account: Pubkey,
pub token_index: u16,
pub asset_usage_fraction: i128,
pub fee: i128,
}

View File

@ -221,8 +221,16 @@ pub struct Bank {
/// See also collected_fees_native and fees_withdrawn.
pub collected_liquidation_fees: I80F48,
/// Collateral fees that have been collected (in native tokens)
///
/// See also collected_fees_native and fees_withdrawn.
pub collected_collateral_fees: I80F48,
/// The daily collateral fees rate for fully utilized collateral.
pub collateral_fee_per_day: f32,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 1920],
pub reserved: [u8; 1900],
}
const_assert_eq!(
size_of::<Bank>(),
@ -259,8 +267,9 @@ const_assert_eq!(
+ 16 * 3
+ 32
+ 8
+ 16 * 3
+ 1920
+ 16 * 4
+ 4
+ 1900
);
const_assert_eq!(size_of::<Bank>(), 3064);
const_assert_eq!(size_of::<Bank>() % 8, 0);
@ -304,6 +313,7 @@ impl Bank {
indexed_borrows: I80F48::ZERO,
collected_fees_native: I80F48::ZERO,
collected_liquidation_fees: I80F48::ZERO,
collected_collateral_fees: I80F48::ZERO,
fees_withdrawn: 0,
dust: I80F48::ZERO,
flash_loan_approved_amount: 0,
@ -368,7 +378,8 @@ impl Bank {
deposit_limit: existing_bank.deposit_limit,
zero_util_rate: existing_bank.zero_util_rate,
platform_liquidation_fee: existing_bank.platform_liquidation_fee,
reserved: [0; 1920],
collateral_fee_per_day: existing_bank.collateral_fee_per_day,
reserved: [0; 1900],
}
}
@ -405,6 +416,7 @@ impl Bank {
require!(self.are_borrows_reduce_only(), MangoError::SomeError);
require_eq!(self.maint_asset_weight, I80F48::ZERO);
}
require_gte!(self.collateral_fee_per_day, 0.0);
Ok(())
}

View File

@ -98,11 +98,32 @@ pub struct Group {
/// Number of fast listings that are allowed per interval
pub allowed_fast_listings_per_interval: u16,
pub reserved: [u8; 1812],
pub padding2: [u8; 4],
/// Intervals in which collateral fee is applied
pub collateral_fee_interval: u64,
pub reserved: [u8; 1800],
}
const_assert_eq!(
size_of::<Group>(),
32 + 4 + 32 * 2 + 4 + 32 * 2 + 4 + 4 + 20 * 32 + 32 + 8 + 16 + 32 + 8 + 8 + 2 * 2 + 1812
32 + 4
+ 32 * 2
+ 4
+ 32 * 2
+ 4
+ 4
+ 20 * 32
+ 32
+ 8
+ 16
+ 32
+ 8
+ 8
+ 2 * 2
+ 4
+ 8
+ 1800
);
const_assert_eq!(size_of::<Group>(), 2736);
const_assert_eq!(size_of::<Group>() % 8, 0);

View File

@ -151,8 +151,14 @@ pub struct MangoAccount {
/// Next id to use when adding a token condition swap
pub next_token_conditional_swap_id: u64,
pub temporary_delegate: Pubkey,
pub temporary_delegate_expiry: u64,
/// Time at which the last collateral fee was charged
pub last_collateral_fee_charge: u64,
#[derivative(Debug = "ignore")]
pub reserved: [u8; 200],
pub reserved: [u8; 152],
// dynamic
pub header_version: u8,
@ -203,7 +209,10 @@ impl MangoAccount {
buyback_fees_accrued_previous: 0,
buyback_fees_expiry_timestamp: 0,
next_token_conditional_swap_id: 0,
reserved: [0; 200],
temporary_delegate: Pubkey::default(),
temporary_delegate_expiry: 0,
last_collateral_fee_charge: 0,
reserved: [0; 152],
header_version: DEFAULT_MANGO_ACCOUNT_VERSION,
padding3: Default::default(),
padding4: Default::default(),
@ -327,11 +336,12 @@ pub struct MangoAccountFixed {
pub next_token_conditional_swap_id: u64,
pub temporary_delegate: Pubkey,
pub temporary_delegate_expiry: u64,
pub reserved: [u8; 160],
pub last_collateral_fee_charge: u64,
pub reserved: [u8; 152],
}
const_assert_eq!(
size_of::<MangoAccountFixed>(),
32 * 4 + 8 + 8 * 8 + 32 + 8 + 160
32 * 4 + 8 + 8 * 8 + 32 + 8 + 8 + 152
);
const_assert_eq!(size_of::<MangoAccountFixed>(), 400);
const_assert_eq!(size_of::<MangoAccountFixed>() % 8, 0);

View File

@ -17,6 +17,7 @@ mod test_bankrupt_tokens;
mod test_basic;
mod test_benchmark;
mod test_borrow_limits;
mod test_collateral_fees;
mod test_delegate;
mod test_fees_buyback_with_mngo;
mod test_force_close;

View File

@ -0,0 +1,171 @@
use super::*;
#[tokio::test]
async fn test_collateral_fees() -> 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..2];
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..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..1],
1_500, // maint: 0.8 * 1500 = 1200
0,
)
.await;
let hour = 60 * 60;
send_tx(
solana,
GroupEdit {
group,
admin,
options: mango_v4::instruction::GroupEdit {
collateral_fee_interval_opt: Some(6 * hour),
..group_edit_instruction_default()
},
},
)
.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();
//
// TEST: Without borrows, charging collateral fees has no effect
//
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
.await
.unwrap();
let mut last_time = solana.clock_timestamp().await;
// no effect
assert_eq!(
account_position(solana, account, tokens[0].bank).await,
1_500
);
//
// 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();
solana.set_clock_timestamp(last_time + 9 * hour).await;
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
.await
.unwrap();
last_time = solana.clock_timestamp().await;
assert!(assert_equal_f64_f64(
account_position_f64(solana, account, tokens[0].bank).await,
1500.0 * (1.0 - 0.1 * (9.0 / 24.0) * (600.0 / 1200.0)),
0.01
));
let last_balance = account_position_f64(solana, account, tokens[0].bank).await;
//
// TEST: More borrows
//
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,
},
)
.await
.unwrap();
solana.set_clock_timestamp(last_time + 7 * hour).await;
send_tx(solana, TokenChargeCollateralFeesInstruction { account })
.await
.unwrap();
//last_time = solana.clock_timestamp().await;
assert!(assert_equal_f64_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))),
0.01
));
Ok(())
}

View File

@ -1078,6 +1078,7 @@ impl ClientInstruction for TokenRegisterInstruction {
zero_util_rate: 0.0,
platform_liquidation_fee: self.platform_liquidation_fee,
disable_asset_liquidation: false,
collateral_fee_per_day: 0.0,
};
let bank = Pubkey::find_program_address(
@ -1326,6 +1327,7 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
zero_util_rate_opt: None,
platform_liquidation_fee_opt: None,
disable_asset_liquidation_opt: None,
collateral_fee_per_day_opt: None,
}
}
@ -1844,6 +1846,7 @@ pub fn group_edit_instruction_default() -> mango_v4::instruction::GroupEdit {
mngo_token_index_opt: None,
buyback_fees_expiry_interval_opt: None,
allowed_fast_listings_per_interval_opt: None,
collateral_fee_interval_opt: None,
}
}
@ -5038,3 +5041,48 @@ impl ClientInstruction for TokenConditionalSwapStartInstruction {
vec![self.liqor_owner]
}
}
#[derive(Clone)]
pub struct TokenChargeCollateralFeesInstruction {
pub account: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for TokenChargeCollateralFeesInstruction {
type Accounts = mango_v4::accounts::TokenChargeCollateralFees;
type Instruction = mango_v4::instruction::TokenChargeCollateralFees;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let instruction = Self::Instruction {};
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
None,
true,
None,
)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
};
let mut instruction = make_instruction(program_id, &accounts, &instruction);
instruction.accounts.extend(health_check_metas.into_iter());
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![]
}
}

View File

@ -83,6 +83,7 @@ export class Bank implements BankForHealth {
public zeroUtilRate: I80F48;
public platformLiquidationFee: I80F48;
public collectedLiquidationFees: I80F48;
public collectedCollateralFees: I80F48;
static from(
publicKey: PublicKey,
@ -148,6 +149,8 @@ export class Bank implements BankForHealth {
zeroUtilRate: I80F48Dto;
platformLiquidationFee: I80F48Dto;
collectedLiquidationFees: I80F48Dto;
collectedCollateralFees: I80F48Dto;
collateralFeePerDay: number;
},
): Bank {
return new Bank(
@ -213,6 +216,8 @@ export class Bank implements BankForHealth {
obj.platformLiquidationFee,
obj.collectedLiquidationFees,
obj.disableAssetLiquidation == 0,
obj.collectedCollateralFees,
obj.collateralFeePerDay,
);
}
@ -279,6 +284,8 @@ export class Bank implements BankForHealth {
platformLiquidationFee: I80F48Dto,
collectedLiquidationFees: I80F48Dto,
public allowAssetLiquidation: boolean,
collectedCollateralFees: I80F48Dto,
public collateralFeePerDay: number,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.oracleConfig = {
@ -311,6 +318,7 @@ export class Bank implements BankForHealth {
this.zeroUtilRate = I80F48.from(zeroUtilRate);
this.platformLiquidationFee = I80F48.from(platformLiquidationFee);
this.collectedLiquidationFees = I80F48.from(collectedLiquidationFees);
this.collectedCollateralFees = I80F48.from(collectedCollateralFees);
this._price = undefined;
this._uiPrice = undefined;
this._oracleLastUpdatedSlot = undefined;

View File

@ -50,6 +50,7 @@ export class Group {
fastListingIntervalStart: BN;
fastListingsInInterval: number;
allowedFastListingsPerInterval: number;
collateralFeeInterval: BN;
},
): Group {
return new Group(
@ -74,6 +75,7 @@ export class Group {
obj.fastListingIntervalStart,
obj.fastListingsInInterval,
obj.allowedFastListingsPerInterval,
obj.collateralFeeInterval,
[], // addressLookupTablesList
new Map(), // banksMapByName
new Map(), // banksMapByMint
@ -113,6 +115,7 @@ export class Group {
public fastListingIntervalStart: BN,
public fastListingsInInterval: number,
public allowedFastListingsPerInterval: number,
public collateralFeeInterval: BN,
public addressLookupTablesList: AddressLookupTableAccount[],
public banksMapByName: Map<string, Bank[]>,
public banksMapByMint: Map<string, Bank[]>,

View File

@ -304,6 +304,7 @@ export class MangoClient {
feesMngoTokenIndex?: TokenIndex,
feesExpiryInterval?: BN,
allowedFastListingsPerInterval?: number,
collateralFeeInterval?: BN,
): Promise<MangoSignatureStatus> {
const ix = await this.program.methods
.groupEdit(
@ -319,6 +320,7 @@ export class MangoClient {
feesMngoTokenIndex ?? null,
feesExpiryInterval ?? null,
allowedFastListingsPerInterval ?? null,
collateralFeeInterval ?? null,
)
.accounts({
group: group.publicKey,
@ -462,6 +464,7 @@ export class MangoClient {
params.zeroUtilRate,
params.platformLiquidationFee,
params.disableAssetLiquidation,
params.collateralFeePerDay,
)
.accounts({
group: group.publicKey,
@ -550,6 +553,7 @@ export class MangoClient {
params.zeroUtilRate,
params.platformLiquidationFee,
params.disableAssetLiquidation,
params.collateralFeePerDay,
)
.accounts({
group: group.publicKey,

View File

@ -31,6 +31,7 @@ export interface TokenRegisterParams {
zeroUtilRate: number;
platformLiquidationFee: number;
disableAssetLiquidation: boolean;
collateralFeePerDay: number;
}
export const DefaultTokenRegisterParams: TokenRegisterParams = {
@ -72,6 +73,7 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
zeroUtilRate: 0.0,
platformLiquidationFee: 0.0,
disableAssetLiquidation: false,
collateralFeePerDay: 0.0,
};
export interface TokenEditParams {
@ -114,6 +116,7 @@ export interface TokenEditParams {
zeroUtilRate: number | null;
platformLiquidationFee: number | null;
disableAssetLiquidation: boolean | null;
collateralFeePerDay: number | null;
}
export const NullTokenEditParams: TokenEditParams = {
@ -156,6 +159,7 @@ export const NullTokenEditParams: TokenEditParams = {
zeroUtilRate: null,
platformLiquidationFee: null,
disableAssetLiquidation: null,
collateralFeePerDay: null,
};
export interface PerpEditParams {

View File

@ -277,6 +277,12 @@ export type MangoV4 = {
"type": {
"option": "u16"
}
},
{
"name": "collateralFeeIntervalOpt",
"type": {
"option": "u64"
}
}
]
},
@ -635,6 +641,10 @@ export type MangoV4 = {
{
"name": "disableAssetLiquidation",
"type": "bool"
},
{
"name": "collateralFeePerDay",
"type": "f32"
}
]
},
@ -1051,6 +1061,12 @@ export type MangoV4 = {
"type": {
"option": "bool"
}
},
{
"name": "collateralFeePerDayOpt",
"type": {
"option": "f32"
}
}
]
},
@ -5963,6 +5979,25 @@ export type MangoV4 = {
}
]
},
{
"name": "tokenChargeCollateralFees",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": []
},
{
"name": "altSet",
"accounts": [
@ -7531,12 +7566,30 @@ export type MangoV4 = {
"defined": "I80F48"
}
},
{
"name": "collectedCollateralFees",
"docs": [
"Collateral fees that have been collected (in native tokens)",
"",
"See also collected_fees_native and fees_withdrawn."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "collateralFeePerDay",
"docs": [
"The daily collateral fees rate for fully utilized collateral."
],
"type": "f32"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1920
1900
]
}
}
@ -7664,12 +7717,28 @@ export type MangoV4 = {
],
"type": "u16"
},
{
"name": "padding2",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "collateralFeeInterval",
"docs": [
"Intervals in which collateral fee is applied"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1812
1800
]
}
}
@ -7791,12 +7860,27 @@ export type MangoV4 = {
],
"type": "u64"
},
{
"name": "temporaryDelegate",
"type": "publicKey"
},
{
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"docs": [
"Time at which the last collateral fee was charged"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
200
152
]
}
},
@ -9566,12 +9650,16 @@ export type MangoV4 = {
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
160
152
]
}
}
@ -13699,6 +13787,36 @@ export type MangoV4 = {
"index": false
}
]
},
{
"name": "TokenCollateralFeeLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "assetUsageFraction",
"type": "i128",
"index": false
},
{
"name": "fee",
"type": "i128",
"index": false
}
]
}
],
"errors": [
@ -14334,6 +14452,12 @@ export const IDL: MangoV4 = {
"type": {
"option": "u16"
}
},
{
"name": "collateralFeeIntervalOpt",
"type": {
"option": "u64"
}
}
]
},
@ -14692,6 +14816,10 @@ export const IDL: MangoV4 = {
{
"name": "disableAssetLiquidation",
"type": "bool"
},
{
"name": "collateralFeePerDay",
"type": "f32"
}
]
},
@ -15108,6 +15236,12 @@ export const IDL: MangoV4 = {
"type": {
"option": "bool"
}
},
{
"name": "collateralFeePerDayOpt",
"type": {
"option": "f32"
}
}
]
},
@ -20020,6 +20154,25 @@ export const IDL: MangoV4 = {
}
]
},
{
"name": "tokenChargeCollateralFees",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": []
},
{
"name": "altSet",
"accounts": [
@ -21588,12 +21741,30 @@ export const IDL: MangoV4 = {
"defined": "I80F48"
}
},
{
"name": "collectedCollateralFees",
"docs": [
"Collateral fees that have been collected (in native tokens)",
"",
"See also collected_fees_native and fees_withdrawn."
],
"type": {
"defined": "I80F48"
}
},
{
"name": "collateralFeePerDay",
"docs": [
"The daily collateral fees rate for fully utilized collateral."
],
"type": "f32"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1920
1900
]
}
}
@ -21721,12 +21892,28 @@ export const IDL: MangoV4 = {
],
"type": "u16"
},
{
"name": "padding2",
"type": {
"array": [
"u8",
4
]
}
},
{
"name": "collateralFeeInterval",
"docs": [
"Intervals in which collateral fee is applied"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
1812
1800
]
}
}
@ -21848,12 +22035,27 @@ export const IDL: MangoV4 = {
],
"type": "u64"
},
{
"name": "temporaryDelegate",
"type": "publicKey"
},
{
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"docs": [
"Time at which the last collateral fee was charged"
],
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
200
152
]
}
},
@ -23623,12 +23825,16 @@ export const IDL: MangoV4 = {
"name": "temporaryDelegateExpiry",
"type": "u64"
},
{
"name": "lastCollateralFeeCharge",
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
160
152
]
}
}
@ -27756,6 +27962,36 @@ export const IDL: MangoV4 = {
"index": false
}
]
},
{
"name": "TokenCollateralFeeLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "assetUsageFraction",
"type": "i128",
"index": false
},
{
"name": "fee",
"type": "i128",
"index": false
}
]
}
],
"errors": [