Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ on:
env:
SOLANA_CLI_VERSION: 2.1.0
NODE_VERSION: 22.15.0
ANCHOR_CLI_VERSION: 0.31.0
TOOLCHAIN: 1.76.0
ANCHOR_CLI_VERSION: 0.31.1
TOOLCHAIN: 1.85.0

jobs:
program_changed_files:
Expand Down
2 changes: 2 additions & 0 deletions Anchor.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
[toolchain]
anchor_version = "0.31.1"
solana_version = "2.1.0"
package_manager = "yarn"

[features]
Expand Down
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Breaking Changes

## dynamic-fee-sharing [0.1.2] [PR #15](https://github.com/MeteoraAg/dynamic-fee-sharing/pull/15)

### Added

- Add a new endpoint `create_operator_account` and `close_operator_account`that allows vault owner to manage different operator accounts
- Add a new account `Operator`, that would stores `whitelisted_address` as well as their operational permissions
- Add a new endpoint `update_user_share` that allows an operator to update a user's share. This affects the fees the user will be entitled to when the vault is funded. Any fees users earned before the share changed will be preserved.

## dynamic-fee-sharing [0.1.1] [PR #8](https://github.com/MeteoraAg/dynamic-fee-sharing/pull/8)

### Added

- Add new field `fee_vault_type` in `FeeVault` to distinguish between PDA-derived and keypair-derived fee vaults.
- Add new endpoint `fund_by_claiming_fee`, that allow share holder in fee vault to claim fees from whitelisted endpoints of DAMM-v2 or Dynamic Bonding Curve

10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,11 @@

- Program ID: `dfsdo2UqvwfN8DuUVrMRNfQe11VaiNoKcMqLHVvDPzh`


### Development

### Dependencies

- anchor 0.31.0
- solana 2.2.14

### Build

Program
Program

```
anchor build
Expand All @@ -25,4 +19,4 @@ anchor build
```
pnpm install
pnpm test
```
```
23 changes: 23 additions & 0 deletions programs/dynamic-fee-sharing/src/access_control.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use crate::error::FeeVaultError;
use crate::state::operator::{Operator, OperatorPermission};
use crate::state::FeeVault;
use anchor_lang::prelude::*;

pub fn is_valid_operator_role<'info>(
fee_vault_loader: &AccountLoader<'info, FeeVault>,
operator_loader: &AccountLoader<'info, Operator>,
signer: &Pubkey,
permission: OperatorPermission,
) -> Result<()> {
let fee_vault = fee_vault_loader.load()?;
let operator = operator_loader.load()?;

if fee_vault.operator_address.eq(&operator_loader.key())
&& operator.whitelisted_address.eq(signer)
&& operator.is_permission_allow(permission)
{
Ok(())
} else {
err!(FeeVaultError::InvalidPermission)
}
}
2 changes: 2 additions & 0 deletions programs/dynamic-fee-sharing/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ use anchor_lang::Discriminator;

pub const MAX_USER: usize = 5;
pub const PRECISION_SCALE: u8 = 64;
pub const MAX_OPERATION: u8 = 1;

pub mod seeds {
pub const FEE_VAULT_PREFIX: &[u8] = b"fee_vault";
pub const FEE_VAULT_AUTHORITY_PREFIX: &[u8] = b"fee_vault_authority";
pub const TOKEN_VAULT_PREFIX: &[u8] = b"token_vault";
pub const OPERATOR_PREFIX: &[u8] = b"operator";
}

// (program_id, instruction, index_of_token_vault_account)
Expand Down
6 changes: 6 additions & 0 deletions programs/dynamic-fee-sharing/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,10 @@ pub enum FeeVaultError {

#[msg("Invalid action")]
InvalidAction,

#[msg("Invalid permission")]
InvalidPermission,

#[msg("Operator already exists")]
OperatorAlreadyExists,
}
7 changes: 7 additions & 0 deletions programs/dynamic-fee-sharing/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ pub struct EvtClaimFee {
pub index: u8,
pub claimed_fee: u64,
}

#[event]
pub struct EvtUpdateUserShare {
pub fee_vault: Pubkey,
pub index: u8,
pub share: u32,
}
4 changes: 4 additions & 0 deletions programs/dynamic-fee-sharing/src/instructions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ pub mod ix_initialize_fee_vault_pda;
pub use ix_initialize_fee_vault_pda::*;
pub mod ix_fund_by_claiming_fee;
pub use ix_fund_by_claiming_fee::*;
pub mod operator;
pub use operator::*;
pub mod owner;
pub use owner::*;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use crate::event::EvtUpdateUserShare;
use crate::state::{FeeVault, Operator};
use anchor_lang::prelude::*;

#[event_cpi]
#[derive(Accounts)]
pub struct UpdateUserShareCtx<'info> {
#[account(mut)]
pub fee_vault: AccountLoader<'info, FeeVault>,

pub operator: AccountLoader<'info, Operator>,

pub signer: Signer<'info>,
}

pub fn handle_update_user_share(
ctx: Context<UpdateUserShareCtx>,
index: u8,
share: u32,
) -> Result<()> {
let mut fee_vault = ctx.accounts.fee_vault.load_mut()?;

fee_vault.validate_and_update_share(index, share)?;

emit_cpi!(EvtUpdateUserShare {
fee_vault: ctx.accounts.fee_vault.key(),
index,
share,
});

Ok(())
}
2 changes: 2 additions & 0 deletions programs/dynamic-fee-sharing/src/instructions/operator/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod ix_update_user_share;
pub use ix_update_user_share::*;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use crate::state::{FeeVault, Operator};
use anchor_lang::prelude::*;

#[event_cpi]
#[derive(Accounts)]
pub struct CloseOperatorAccountCtx<'info> {
#[account(mut, has_one = owner)]
pub fee_vault: AccountLoader<'info, FeeVault>,

#[account(
mut,
close = rent_receiver
)]
pub operator: AccountLoader<'info, Operator>,

pub owner: Signer<'info>,

/// CHECK: Account to receive closed account rental SOL
#[account(mut)]
pub rent_receiver: UncheckedAccount<'info>,
}

pub fn handle_close_operator_account(ctx: Context<CloseOperatorAccountCtx>) -> Result<()> {
let mut fee_vault = ctx.accounts.fee_vault.load_mut()?;
fee_vault.operator_address = Pubkey::default();

Ok(())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::{
constants::{seeds::OPERATOR_PREFIX, MAX_OPERATION},
error::FeeVaultError,
state::{FeeVault, Operator},
};
use anchor_lang::prelude::*;

#[event_cpi]
#[derive(Accounts)]
pub struct CreateOperatorAccountCtx<'info> {
#[account(mut, has_one = owner)]
pub fee_vault: AccountLoader<'info, FeeVault>,

#[account(
init,
payer = owner,
seeds = [
OPERATOR_PREFIX.as_ref(),
whitelisted_address.key().as_ref(),
],
bump,
space = 8 + Operator::INIT_SPACE
)]
pub operator: AccountLoader<'info, Operator>,

/// CHECK: can be any address
pub whitelisted_address: UncheckedAccount<'info>,

#[account(mut)]
pub owner: Signer<'info>,

pub system_program: Program<'info, System>,
}

pub fn handle_create_operator_account(
ctx: Context<CreateOperatorAccountCtx>,
permission: u128,
) -> Result<()> {
// validate permission, only support 1 operations for now
require!(
permission > 0 && permission < 1 << MAX_OPERATION,
FeeVaultError::InvalidPermission
);

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

require!(
fee_vault.operator_address == Pubkey::default(),
FeeVaultError::OperatorAlreadyExists
);

let mut operator = ctx.accounts.operator.load_init()?;
operator.initialize(ctx.accounts.whitelisted_address.key(), permission);

fee_vault.operator_address = ctx.accounts.operator.key();

Ok(())
}
4 changes: 4 additions & 0 deletions programs/dynamic-fee-sharing/src/instructions/owner/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod ix_create_operator_account;
pub use ix_create_operator_account::*;
pub mod ix_close_operator_account;
pub use ix_close_operator_account::*;
23 changes: 23 additions & 0 deletions programs/dynamic-fee-sharing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ pub mod constants;
pub mod error;
pub mod instructions;
pub use instructions::*;
pub mod access_control;
pub mod const_pda;
pub mod event;
pub mod math;
pub mod state;
pub mod utils;
pub use access_control::*;
use state::OperatorPermission;

pub mod tests;
declare_id!("dfsdo2UqvwfN8DuUVrMRNfQe11VaiNoKcMqLHVvDPzh");
Expand Down Expand Up @@ -47,4 +50,24 @@ pub mod dynamic_fee_sharing {
pub fn claim_fee(ctx: Context<ClaimFeeCtx>, index: u8) -> Result<()> {
instructions::handle_claim_fee(ctx, index)
}

#[access_control(is_valid_operator_role(&ctx.accounts.fee_vault, &ctx.accounts.operator, ctx.accounts.signer.key, OperatorPermission::UpdateUserShare))]
pub fn update_user_share(
ctx: Context<UpdateUserShareCtx>,
index: u8,
share: u32,
) -> Result<()> {
instructions::handle_update_user_share(ctx, index, share)
}

pub fn create_operator_account(
ctx: Context<CreateOperatorAccountCtx>,
permission: u128,
) -> Result<()> {
instructions::handle_create_operator_account(ctx, permission)
}

pub fn close_operator_account(_ctx: Context<CloseOperatorAccountCtx>) -> Result<()> {
Ok(())
}
}
47 changes: 43 additions & 4 deletions programs/dynamic-fee-sharing/src/state/fee_vault.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ pub struct FeeVault {
pub total_funded_fee: u64,
pub fee_per_share: u128,
pub base: Pubkey,
pub padding: [u128; 4],
pub operator_address: Pubkey,
pub padding: [u128; 2],
pub users: [UserFee; MAX_USER],
}
const_assert_eq!(FeeVault::INIT_SPACE, 640);
Expand All @@ -51,7 +52,8 @@ pub struct UserFee {
pub share: u32,
pub padding_0: [u8; 4],
pub fee_claimed: u64,
pub padding: [u8; 16], // padding for future use
pub pending_fee: u64,
pub padding: [u8; 8], // padding for future use
pub fee_per_share_checkpoint: u128,
}
const_assert_eq!(UserFee::INIT_SPACE, 80);
Expand Down Expand Up @@ -107,13 +109,16 @@ impl FeeVault {
.ok_or_else(|| FeeVaultError::InvalidUserIndex)?;
require!(user.address.eq(signer), FeeVaultError::InvalidUserAddress);

let reward_per_share_delta = self.fee_per_share.safe_sub(user.fee_per_share_checkpoint)?;
let fee_per_share_delta = self.fee_per_share.safe_sub(user.fee_per_share_checkpoint)?;

let fee_being_claimed = mul_shr(user.share.into(), reward_per_share_delta, PRECISION_SCALE)
let current_fee: u64 = mul_shr(user.share.into(), fee_per_share_delta, PRECISION_SCALE)
.ok_or_else(|| FeeVaultError::MathOverflow)?
.try_into()
.map_err(|_| FeeVaultError::MathOverflow)?;

let fee_being_claimed = user.pending_fee.safe_add(current_fee)?;

user.pending_fee = 0;
user.fee_per_share_checkpoint = self.fee_per_share;
user.fee_claimed = user.fee_claimed.safe_add(fee_being_claimed)?;

Expand All @@ -125,4 +130,38 @@ impl FeeVault {
.iter()
.any(|share_holder| share_holder.address.eq(signer))
}

pub fn validate_and_update_share(&mut self, index: u8, share: u32) -> Result<()> {
require!(
index < self.users.len() as u8,
FeeVaultError::InvalidUserIndex
);
require!(share > 0, FeeVaultError::InvalidFeeVaultParameters);

// when updating user share, we need to update the pending fee for all users
// based on the current fee per share to preserve the fee distribution up to that point
let mut total_share = 0;
for (i, user) in self.users.iter_mut().enumerate() {
let fee_per_share_delta = self.fee_per_share.safe_sub(user.fee_per_share_checkpoint)?;
let pending_fee = mul_shr(user.share.into(), fee_per_share_delta, PRECISION_SCALE)
.ok_or_else(|| FeeVaultError::MathOverflow)?
.try_into()
.map_err(|_| FeeVaultError::MathOverflow)?;

user.pending_fee = user.pending_fee.safe_add(pending_fee)?;
user.fee_per_share_checkpoint = self.fee_per_share;

if i == index as usize {
require!(
share != user.share,
FeeVaultError::InvalidFeeVaultParameters
);
user.share = share;
}
total_share = total_share.safe_add(user.share)?;
}
self.total_share = total_share;

Ok(())
}
}
2 changes: 2 additions & 0 deletions programs/dynamic-fee-sharing/src/state/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub mod fee_vault;
pub use fee_vault::*;
pub mod operator;
pub use operator::*;
Loading
Loading