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

implements entry/redemption fees #55

Merged
merged 7 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
19 changes: 19 additions & 0 deletions programs/accountant/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use anchor_lang::prelude::*;

#[event]
pub struct PerformanceFeeUpdatedEvent {
pub accountant_key: Pubkey,
pub performance_fee: u64,
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be useful to also include timestamp to
PerformanceFeeUpdatedEvent, EntryFeeUpdatedEvent, RedemptionFeeUpdatedEvent
so that we can keep track of the fee changing history easily.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we have a timestamp in the transaction data?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, No need to strictly use Clock sysvar timestamp.


#[event]
pub struct EntryFeeUpdatedEvent {
pub accountant_key: Pubkey,
pub entry_fee: u64,
}

#[event]
pub struct RedemptionFeeUpdatedEvent {
pub accountant_key: Pubkey,
pub redemption_fee: u64,
}
52 changes: 48 additions & 4 deletions programs/accountant/src/instructions/set_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use access_control::{
state::{UserRole, Role}
};

use crate::events::{EntryFeeUpdatedEvent, PerformanceFeeUpdatedEvent, RedemptionFeeUpdatedEvent};
use crate::utils::unchecked_accountant::UncheckedAccountant;

#[derive(Accounts)]
Expand All @@ -30,12 +31,55 @@ pub struct SetFee<'info> {
pub access_control: Program<'info, AccessControl>
}

pub fn handle_set_fee(
pub fn handle_set_performance_fee(
ctx: Context<SetFee>,
fee: u64,
) -> Result<()> {
let accountant = &mut ctx.accounts.accountant.from_unchecked()?;

accountant.set_fee(fee)?;
accountant.save_changes(&mut &mut ctx.accounts.accountant.try_borrow_mut_data()?[8..])
}
accountant.set_performance_fee(fee)?;
accountant.save_changes(&mut &mut ctx.accounts.accountant.try_borrow_mut_data()?[8..])?;

emit!(PerformanceFeeUpdatedEvent {
accountant_key: ctx.accounts.accountant.key(),
performance_fee: fee,
});

Ok(())
}


pub fn handle_set_entry_fee(
ctx: Context<SetFee>,
fee: u64,
) -> Result<()> {
let accountant = &mut ctx.accounts.accountant.from_unchecked()?;

accountant.set_entry_fee(fee)?;
accountant.save_changes(&mut &mut ctx.accounts.accountant.try_borrow_mut_data()?[8..])?;

emit!(EntryFeeUpdatedEvent {
accountant_key: ctx.accounts.accountant.key(),
entry_fee: fee,
});

Ok(())
}

pub fn handle_set_redemption_fee(
ctx: Context<SetFee>,
fee: u64,
) -> Result<()> {
let accountant = &mut ctx.accounts.accountant.from_unchecked()?;

accountant.set_redemption_fee(fee)?;
accountant.save_changes(&mut &mut ctx.accounts.accountant.try_borrow_mut_data()?[8..])?;

emit!(RedemptionFeeUpdatedEvent {
accountant_key: ctx.accounts.accountant.key(),
redemption_fee: fee,
});

Ok(())
}

13 changes: 11 additions & 2 deletions programs/accountant/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use anchor_lang::prelude::*;

pub mod constants;
pub mod error;
pub mod events;
pub mod instructions;
pub mod state;
pub mod utils;
Expand Down Expand Up @@ -45,7 +46,15 @@ pub mod accountant {
handle_distribute(ctx)
}

pub fn set_fee(ctx: Context<SetFee>, fee: u64) -> Result<()> {
handle_set_fee(ctx, fee)
pub fn set_performance_fee(ctx: Context<SetFee>, fee: u64) -> Result<()> {
handle_set_performance_fee(ctx, fee)
}

pub fn set_redemption_fee(ctx: Context<SetFee>, fee: u64) -> Result<()> {
handle_set_redemption_fee(ctx, fee)
}

pub fn set_entry_fee(ctx: Context<SetFee>, fee: u64) -> Result<()> {
handle_set_entry_fee(ctx, fee)
}
}
12 changes: 9 additions & 3 deletions programs/accountant/src/state/base_accountant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ pub trait Accountant {
fn init(&mut self, index: u64, bump: u8) -> Result<()>;

fn report(&self, profit: u64, loss: u64) -> Result<(u64,u64)>;
fn enter(&self, amount: u64) -> Result<u64>;
fn redeem(&self, amount: u64) -> Result<u64>;

fn distribute(&mut self, accounts: &Distribute) -> Result<()>;
fn set_fee_recipient(&mut self, recipient: Pubkey) -> Result<()>;
fn set_fee(&mut self, fee: u64) -> Result<()>;

fn set_performance_fee(&mut self, fee: u64) -> Result<()>;
fn set_redemption_fee(&mut self, fee: u64) -> Result<()>;
fn set_entry_fee(&mut self, fee: u64) -> Result<()>;

fn entry_fee(&self) -> u64;
fn redemption_fee(&self) -> u64;
fn performance_fee(&self) -> u64;
fn fee_recipient(&self) -> Pubkey;

fn seeds(&self) -> [&[u8]; 2];
fn save_changes(&self, writer: &mut dyn std::io::Write) -> Result<()>;
Expand Down
32 changes: 26 additions & 6 deletions programs/accountant/src/state/generic_accountant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ pub struct GenericAccountant {
pub index_buffer: [u8; 8],
pub bump: [u8; 1],

pub entry_fee: u64,
pub redemption_fee: u64,
pub performance_fee: u64,
pub fee_recipient: Pubkey,
}

impl Accountant for GenericAccountant {
Expand All @@ -36,6 +37,16 @@ impl Accountant for GenericAccountant {
Ok((total_fees, total_refunds))
}

fn enter(&self, amount: u64) -> Result<u64> {
let fee = self.entry_fee * amount / FEE_BPS;
Ok(fee)
}

fn redeem(&self, amount: u64) -> Result<u64> {
let fee = self.redemption_fee * amount / FEE_BPS;
Ok(fee)
}

fn distribute(&mut self, accounts: &Distribute) -> Result<()> {
let total = accounts.token_account.amount;

Expand All @@ -53,22 +64,31 @@ impl Accountant for GenericAccountant {
)
}

fn set_fee(&mut self, fee: u64) -> Result<()> {
fn set_performance_fee(&mut self, fee: u64) -> Result<()> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for
set_performance_fee, set_redemption_fee, set_entry_fee fn
we can consider adding a condition that fee arg be less or equal to FEE_BPS constant value, just to ensure that the fee won't be over 100%

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense, will add validation

self.performance_fee = fee;
Ok(())
}

fn set_fee_recipient(&mut self, recipient: Pubkey) -> Result<()> {
self.fee_recipient = recipient;
fn set_redemption_fee(&mut self, fee: u64) -> Result<()> {
self.redemption_fee = fee;
Ok(())
}

fn set_entry_fee(&mut self, fee: u64) -> Result<()> {
self.entry_fee = fee;
Ok(())
}

fn performance_fee(&self) -> u64 {
self.performance_fee
}

fn fee_recipient(&self) -> Pubkey {
self.fee_recipient
fn entry_fee(&self) -> u64 {
self.entry_fee
}

fn redemption_fee(&self) -> u64 {
self.redemption_fee
}

fn save_changes(&self, writer: &mut dyn std::io::Write) -> Result<()> {
Expand Down
25 changes: 1 addition & 24 deletions programs/accountant/src/utils/unchecked_accountant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,4 @@ impl<'a> UncheckedAccountant for UncheckedAccount<'a> {
Ok(())
}

}

// pub fn from_unchecked(strategy_acc: &UncheckedAccount) -> Result<Box<dyn Accountant>> {
// let strategy_data = strategy_acc.try_borrow_data()?;
// let discriminator = get_discriminator(strategy_acc)?;

// match discriminator {
// GenericAccountant::DISCRIMINATOR => {
// let strategy = GenericAccountant::try_from_slice(&strategy_data[8..])
// .map_err(|_| ErrorCode::InvalidData)?;
// Ok(Box::new(strategy))
// }
// _ => {
// msg!("Invalid discriminator");
// Err(ErrorCode::InvalidDiscriminator.into())
// }
// }
// }

// fn get_discriminator(acc_info: &UncheckedAccount) -> Result<[u8; 8]> {
// let data = acc_info.try_borrow_data()?;
// let discriminator = data[0..8].try_into().map_err(|_| ErrorCode::InvalidData)?;
// Ok(discriminator)
// }
}
33 changes: 30 additions & 3 deletions programs/tokenized_vault/src/instructions/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,24 @@ use crate::constants::{SHARES_SEED, UNDERLYING_SEED, WHITELISTED_SEED};

use crate::events::VaultDepositEvent;
use crate::state::Vault;
use crate::utils::{token, vault};
use crate::utils::{accountant, token, vault};

#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut)]
pub vault: AccountLoader<'info, Vault>,

/// CHECK:
#[account(mut, address = vault.load()?.accountant)]
pub accountant: UncheckedAccount<'info>,

#[account(
mut,
associated_token::mint = shares_mint,
Copy link

@codinghistorian codinghistorian Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The accountant requires a token account based on the share_mint. In my opinion, it would be nice if the InitTokenAccount context of the accountant program used a more generic naming convention for the mint account, rather than underlying_mint (which, in the context of this repository, refers to the underlying_asset that users can invest with). At the very least, the current naming caused some confusion for me.

associated_token::authority = accountant,
)]
pub accountant_recipient: Box<InterfaceAccount<'info, TokenAccount>>,

#[account(mut)]
pub user_token_account: InterfaceAccount<'info, TokenAccount>,

Expand Down Expand Up @@ -67,15 +78,18 @@ pub struct Deposit<'info> {
}

pub fn handle_deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
let enter_fee = accountant::enter(&ctx.accounts.accountant, amount)?;
let amount_to_deposit = amount - enter_fee;

vault::validate_deposit(
&ctx.accounts.vault,
ctx.accounts.kyc_verified.to_account_info(),
ctx.accounts.whitelisted.to_account_info(),
false,
amount
amount_to_deposit
)?;

let shares = ctx.accounts.vault.load()?.convert_to_shares(amount);
let mut shares = ctx.accounts.vault.load()?.convert_to_shares(amount_to_deposit);

token::transfer(
ctx.accounts.token_program.to_account_info(),
Expand All @@ -95,6 +109,19 @@ pub fn handle_deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
&ctx.accounts.vault.load()?.seeds_shares(),
)?;

if enter_fee > 0 {
let fee_shares = ctx.accounts.vault.load()?.convert_to_shares(enter_fee);
shares += fee_shares;
token::mint_to(
ctx.accounts.shares_token_program.to_account_info(),
ctx.accounts.shares_mint.to_account_info(),
ctx.accounts.accountant_recipient.to_account_info(),
ctx.accounts.shares_mint.to_account_info(),
fee_shares,
&ctx.accounts.vault.load()?.seeds_shares(),
)?;
}

let mut vault = ctx.accounts.vault.load_mut()?;
vault.handle_deposit(amount, shares);

Expand Down
39 changes: 33 additions & 6 deletions programs/tokenized_vault/src/instructions/direct_deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,32 @@ use access_control::{
use anchor_lang::prelude::*;
use anchor_spl::{
token::Token,
token_interface::{Mint, TokenAccount},
token_interface::{Mint, TokenAccount, TokenInterface},
};
use strategy::program::Strategy;

use crate::constants::{SHARES_SEED, STRATEGY_DATA_SEED, UNDERLYING_SEED, WHITELISTED_SEED};

use crate::events::{VaultDepositEvent, UpdatedCurrentDebtForStrategyEvent};
use crate::state::{Vault, StrategyData};
use crate::utils::{token, vault};
use crate::utils::strategy as strategy_utils;
use crate::utils::{accountant, strategy as strategy_utils, token, vault};

#[derive(Accounts)]
pub struct DirectDeposit<'info> {
#[account(mut)]
pub vault: AccountLoader<'info, Vault>,

/// CHECK:
#[account(mut, address = vault.load()?.accountant)]
pub accountant: UncheckedAccount<'info>,

#[account(
mut,
associated_token::mint = shares_mint,
associated_token::authority = accountant,
)]
pub accountant_recipient: Box<InterfaceAccount<'info, TokenAccount>>,

#[account(mut)]
pub user_token_account: InterfaceAccount<'info, TokenAccount>,

Expand Down Expand Up @@ -86,21 +96,25 @@ pub struct DirectDeposit<'info> {
#[account(mut)]
pub user: Signer<'info>,

pub token_program: Program<'info, Token>,
pub shares_token_program: Program<'info, Token>,
pub token_program: Interface<'info, TokenInterface>,
pub access_control: Program<'info, AccessControl>,
pub strategy_program: Program<'info, Strategy>,
}

pub fn handle_direct_deposit<'info>(ctx: Context<'_, '_, '_, 'info, DirectDeposit<'info>>, amount: u64) -> Result<()> {
let enter_fee = accountant::enter(&ctx.accounts.accountant, amount)?;
let amount_to_deposit = amount - enter_fee;

vault::validate_deposit(
&ctx.accounts.vault,
ctx.accounts.kyc_verified.to_account_info(),
ctx.accounts.whitelisted.to_account_info(),
true,
amount
amount_to_deposit
)?;

let shares = ctx.accounts.vault.load()?.convert_to_shares(amount);
let mut shares = ctx.accounts.vault.load()?.convert_to_shares(amount_to_deposit);

token::transfer(
ctx.accounts.token_program.to_account_info(),
Expand Down Expand Up @@ -135,6 +149,19 @@ pub fn handle_direct_deposit<'info>(ctx: Context<'_, '_, '_, 'info, DirectDeposi
&ctx.accounts.vault.load()?.seeds_shares(),
)?;

if enter_fee > 0 {
let fee_shares = ctx.accounts.vault.load()?.convert_to_shares(enter_fee);
shares += fee_shares;
token::mint_to(
ctx.accounts.shares_token_program.to_account_info(),
ctx.accounts.shares_mint.to_account_info(),
ctx.accounts.accountant_recipient.to_account_info(),
ctx.accounts.shares_mint.to_account_info(),
fee_shares,
&ctx.accounts.vault.load()?.seeds_shares(),
)?;
}

let mut vault = ctx.accounts.vault.load_mut()?;

ctx.accounts.strategy_data.increase_current_debt(amount)?;
Expand Down
Loading
Loading