Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b84bbbb
chore: add toolchain
bangyro Feb 2, 2026
a8a46db
feat: update user share logic
bangyro Feb 2, 2026
3ef93aa
feat: create/close operator account on a fee vault level
bangyro Feb 2, 2026
3be39f2
feat: update changelog
bangyro Feb 3, 2026
2561964
feat: change from operator_account to address
bangyro Feb 11, 2026
3cf2cd7
feat: mutable fee_vault and remove user feature
bangyro Feb 16, 2026
e63211b
feat: update tooling and config
bangyro Feb 16, 2026
e9fffc7
feat: add fail case
bangyro Feb 16, 2026
bf5760a
docs: update changelog
bangyro Feb 16, 2026
0f963eb
fix: validate mutable_flag param
bangyro Feb 17, 2026
ab2aeb9
feat: add more test
bangyro Feb 17, 2026
1a05433
feat: refactor baseKp
bangyro Feb 17, 2026
92ee278
feat: add more test assertions
bangyro Feb 17, 2026
1f70893
docs: document mutable_flag field
bangyro Feb 17, 2026
a3cc414
feat: address comments
bangyro Feb 26, 2026
9b24620
feat: address comments and change update user share behaviour
bangyro Feb 26, 2026
681d89c
feat: add user and add update operator event
bangyro Feb 27, 2026
c8dabc7
fix: only create removed_user_token_vault when unclaimed_fee > 0
bangyro Feb 27, 2026
5861fb5
fix: only allow update operator when fee_vault is mutable
bangyro Mar 2, 2026
f526a47
feat: address comments
bangyro Mar 2, 2026
7b8a733
feat: address comments
bangyro Mar 2, 2026
f1483ef
feat: update changelog
bangyro Mar 2, 2026
ca85f20
fix: missing await
bangyro Mar 2, 2026
a93020a
fix: run cargo fmt
bangyro Mar 2, 2026
8033c8d
docs: add comment to clarify
bangyro Mar 3, 2026
7ef755e
feat: address comments
bangyro Mar 3, 2026
c399252
feat: add owner validation
bangyro Mar 3, 2026
2cd65fe
feat: re-add mutable_flag
bangyro Mar 4, 2026
4e40252
feat: address comments
bangyro Mar 4, 2026
23ce64e
feat: operator reclaims rent from user_unclaimed_fee
bangyro Mar 6, 2026
b73dc9e
feat: increase max user to 100
bangyro Mar 7, 2026
e7b4498
feat: remove user from slow and shift left users. minor fixes
bangyro Mar 9, 2026
365449a
fix: cargo clippy warnings
bangyro Mar 12, 2026
87bed87
fix: pr comments
bangyro Mar 12, 2026
30c0fa6
fix: prevent total_share from becoming 0
bangyro Mar 14, 2026
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ on:
- release_*

env:
SOLANA_CLI_VERSION: 2.1.0
SOLANA_CLI_VERSION: 2.3.13
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
Comment thread
bangyro marked this conversation as resolved.

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.3.13"
package_manager = "yarn"

[features]
Expand Down
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,26 @@ 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 field `mutable_flag` to `FeeVault` to indicate its mutability
- Add a new field `operator` to `FeeVault`. The `operator` defaults to the owner when the `FeeVault` is initialized. The `operator` and can perform operator instructions on a mutable `FeeVault`
- Add a new owner endpoint `update_operator` for vault owner to update the operator field
- Add a new operator endpoint `add_user` to add a user to a `FeeVault`
- Add a new operator endpoint `remove_user` which removes a user and transfers any unclaimed fee into an account for the removed user to claim
- Add a new endpoint `claim_unclaimed_fee` where a user who have been removed from the `FeeVault` can claim any unclaimed fees
- Add a new operator endpoint `update_user_share` 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
- Increase the `MAX_USER` limit from 5 to 100

## Changed

- Prevent initializing a `FeeVault` with duplicate user address. This change affects both `initialize_fee_vault` and `initialize_fee_vault_pda` endpoints.

## 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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ incremental = false
codegen-units = 1

[workspace.dependencies]
anchor-lang = {version = "0.31.1", features = ["event-cpi"]}
anchor-lang = {version = "0.31.1", features = ["event-cpi", "init-if-needed"]}
anchor-spl = "0.31.1"
bytemuck = { version = "1.20.0", features = ["derive", "min_const_generics"] }
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
```
```
5 changes: 4 additions & 1 deletion programs/dynamic-fee-sharing/src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use anchor_lang::prelude::Pubkey;
use anchor_lang::Discriminator;

pub const MAX_USER: usize = 5;
pub const MIN_USER: usize = 2;
pub const MAX_STATIC_USER: usize = 5;
pub const MAX_USER: usize = 100; // 5 static users + 95 dynamic users; this is an arbitrary limit
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

leaving it at 100 for now. Since I don't know if there is a demand for more. We can increase the limit.

pub const PRECISION_SCALE: u8 = 64;

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 USER_UNCLAIMED_FEE_PREFIX: &[u8] = b"user_unclaimed_fee";
}

// (program_id, instruction, index_of_token_vault_account)
Expand Down
16 changes: 14 additions & 2 deletions programs/dynamic-fee-sharing/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ pub enum FeeVaultError {
#[msg("Invalid user address")]
InvalidUserAddress,

#[msg("Exceeded number of users allowed")]
ExceededUser,
#[msg("Invalid number of users")]
InvalidNumberOfUsers,
Comment on lines +26 to +27
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Changing the error name here since. This error is also used when the number of user is less than the minimum amount (2)


#[msg("Invalid fee vault")]
InvalidFeeVault,
Expand All @@ -34,4 +34,16 @@ pub enum FeeVaultError {

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

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

#[msg("Invalid operator address")]
InvalidOperatorAddress,

#[msg("Type cast error")]
TypeCastFailed,

#[msg("Fee vault is not mutable")]
FeeVaultNotMutable,
}
34 changes: 34 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,37 @@ pub struct EvtClaimFee {
pub index: u8,
pub claimed_fee: u64,
}

#[event]
pub struct EvtAddUser {
pub fee_vault: Pubkey,
pub user: Pubkey,
pub share: u32,
}

#[event]
pub struct EvtUpdateUserShare {
pub fee_vault: Pubkey,
pub user: Pubkey,
pub share: u32,
}

#[event]
pub struct EvtRemoveUser {
pub fee_vault: Pubkey,
pub user: Pubkey,
pub unclaimed_fee: u64,
}

#[event]
pub struct EvtClaimUnclaimedFee {
pub fee_vault: Pubkey,
pub user: Pubkey,
pub claimed_fee: u64,
}

#[event]
pub struct EvtUpdateOperator {
pub fee_vault: Pubkey,
pub operator: Pubkey,
}
10 changes: 6 additions & 4 deletions programs/dynamic-fee-sharing/src/instructions/ix_claim_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
use crate::const_pda;
use crate::event::EvtClaimFee;
use crate::state::FeeVault;
use crate::state::DynamicFeeVaultLoader;
use crate::utils::token::transfer_from_fee_vault;

#[event_cpi]
Expand All @@ -23,6 +24,7 @@ pub struct ClaimFeeCtx<'info> {

pub token_mint: Box<InterfaceAccount<'info, Mint>>,

// token account does not need to be owned by user
#[account(mut)]
pub user_token_vault: Box<InterfaceAccount<'info, TokenAccount>>,

Expand All @@ -32,15 +34,15 @@ pub struct ClaimFeeCtx<'info> {
}

pub fn handle_claim_fee(ctx: Context<ClaimFeeCtx>, index: u8) -> Result<()> {
let mut fee_vault = ctx.accounts.fee_vault.load_mut()?;
let fee_being_claimed = fee_vault.validate_and_claim_fee(index, &ctx.accounts.user.key())?;
let mut vault = ctx.accounts.fee_vault.load_content_mut()?;
let fee_being_claimed = vault.claim_fee(index.into(), &ctx.accounts.user.key())?;

if fee_being_claimed > 0 {
transfer_from_fee_vault(
ctx.accounts.fee_vault_authority.to_account_info(),
&ctx.accounts.token_mint,
&ctx.accounts.token_vault,
&ctx.accounts.user_token_vault,
ctx.accounts.token_vault.to_account_info(),
ctx.accounts.user_token_vault.to_account_info(),
&ctx.accounts.token_program,
fee_being_claimed,
)?;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use crate::const_pda;
use crate::constants::seeds::USER_UNCLAIMED_FEE_PREFIX;
use crate::event::EvtClaimUnclaimedFee;
use crate::state::{FeeVault, UserUnclaimedFee};
use crate::utils::token::transfer_from_fee_vault;
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};

#[event_cpi]
#[derive(Accounts)]
pub struct ClaimUnclaimedFeeCtx<'info> {
#[account(has_one = token_mint, has_one = operator, has_one = token_vault)]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

has_one = operator ties this claim to the current fee_vault.operator, even though the operator account here is only used as the close/rent receiver. If the operator rotates after remove_user but before the removed user claims, this constraint will fail and their unclaimed fees become permanently unclaimable. Please decouple the rent receiver from the live operator field.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

user should pass the current operator

Comment thread
bangyro marked this conversation as resolved.
pub fee_vault: AccountLoader<'info, FeeVault>,

/// CHECK: fee vault authority
#[account(address = const_pda::fee_vault_authority::ID)]
pub fee_vault_authority: UncheckedAccount<'info>,

pub token_mint: Box<InterfaceAccount<'info, Mint>>,

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

#[account(
mut,
close = operator,
seeds = [
Comment thread
bangyro marked this conversation as resolved.
USER_UNCLAIMED_FEE_PREFIX,
fee_vault.key().as_ref(),
user.key().as_ref(),
],
bump,
)]
pub user_unclaimed_fee: AccountLoader<'info, UserUnclaimedFee>,

// token account does not need to be owned by user
#[account(mut)]
pub user_token_vault: Box<InterfaceAccount<'info, TokenAccount>>,

/// CHECK: fee vault operator, receives rent from closed account
#[account(mut)]
pub operator: UncheckedAccount<'info>,

pub user: Signer<'info>,

pub token_program: Interface<'info, TokenInterface>,
}

// when a user is removed from a fee vault, they may claim any unclaimed fee that they have earned before the removal
pub fn handle_claim_unclaimed_fee(ctx: Context<ClaimUnclaimedFeeCtx>) -> Result<()> {
let user_unclaimed_fee = ctx.accounts.user_unclaimed_fee.load()?;
let fee_being_claimed = user_unclaimed_fee.unclaimed_fee;

if fee_being_claimed > 0 {
transfer_from_fee_vault(
ctx.accounts.fee_vault_authority.to_account_info(),
&ctx.accounts.token_mint,
ctx.accounts.token_vault.to_account_info(),
ctx.accounts.user_token_vault.to_account_info(),
&ctx.accounts.token_program,
fee_being_claimed,
)?;

emit_cpi!(EvtClaimUnclaimedFee {
fee_vault: ctx.accounts.fee_vault.key(),
user: ctx.accounts.user.key(),
claimed_fee: fee_being_claimed,
});
}

Ok(())
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::constants::WHITELISTED_ACTIONS;
use crate::event::EvtFundFee;
use crate::state::FeeVault;
use crate::math::SafeCast;
use crate::state::{DynamicFeeVaultLoader, FeeVault, FeeVaultType};
use crate::{error::FeeVaultError, math::SafeMath};
use anchor_lang::prelude::*;
use anchor_lang::solana_program::{instruction::Instruction, program::invoke_signed};
Expand All @@ -22,11 +23,11 @@ pub struct FundByClaimingFeeCtx<'info> {
pub source_program: UncheckedAccount<'info>,
}

pub fn is_support_action<'info>(
pub fn is_support_action(
source_program: &Pubkey,
discriminator: &[u8],
token_vault: Pubkey,
remaining_accounts: &[AccountInfo<'info>],
remaining_accounts: &[AccountInfo],
) -> bool {
for &(program, disc, token_vault_index) in WHITELISTED_ACTIONS.iter() {
if program.eq(source_program) && disc.eq(discriminator) {
Expand All @@ -53,29 +54,34 @@ pub fn handle_fund_by_claiming_fee(
FeeVaultError::InvalidAction
);

let fee_vault = ctx.accounts.fee_vault.load()?;
let vault = ctx.accounts.fee_vault.load_content_mut()?;

require!(
fee_vault.is_share_holder(ctx.accounts.signer.key),
vault.is_share_holder(ctx.accounts.signer.key),
FeeVaultError::InvalidSigner
);

// support fee vault type is pda account
require!(
fee_vault.fee_vault_type == 1,
vault.fee_vault.fee_vault_type.safe_cast()? == FeeVaultType::PdaAccount,
FeeVaultError::InvalidFeeVault
);

let base = vault.fee_vault.base;
let token_mint = vault.fee_vault.token_mint;
let fee_vault_bump = vault.fee_vault.fee_vault_bump;
drop(vault);
Comment on lines +70 to +73
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

moved up since we have to drop the dynamic_fee_vault before using fee_vault on like 81. We need to use dynamic_fee_vault for the vault.is_share_holder(ctx.accounts.signer.key) above


let before_token_vault_balance = ctx.accounts.token_vault.amount;

let accounts: Vec<AccountMeta> = ctx
.remaining_accounts
.iter()
.map(|acc| {
let is_signer = acc.key == &ctx.accounts.fee_vault.key();
let is_signer = acc.key.eq(&ctx.accounts.fee_vault.key());
AccountMeta {
pubkey: *acc.key,
is_signer: is_signer,
is_signer,
is_writable: acc.is_writable,
}
})
Expand All @@ -87,11 +93,7 @@ pub fn handle_fund_by_claiming_fee(
.map(|acc| AccountInfo { ..acc.clone() })
.collect();
// invoke instruction to amm
let base = fee_vault.base;
let token_mint = fee_vault.token_mint;
let fee_vault_bump = fee_vault.fee_vault_bump;
let signer_seeds = fee_vault_seeds!(base, token_mint, fee_vault_bump);
drop(fee_vault);

invoke_signed(
&Instruction {
Expand Down
6 changes: 3 additions & 3 deletions programs/dynamic-fee-sharing/src/instructions/ix_fund_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ pub fn handle_fund_fee(ctx: Context<FundFeeCtx>, max_amount: u64) -> Result<()>
fee_vault.fund_fee(excluded_transfer_fee_amount)?;

transfer_from_user(
&ctx.accounts.funder,
ctx.accounts.funder.to_account_info(),
&ctx.accounts.token_mint,
&ctx.accounts.fund_token_vault,
&ctx.accounts.token_vault,
ctx.accounts.fund_token_vault.to_account_info(),
ctx.accounts.token_vault.to_account_info(),
&ctx.accounts.token_program,
amount,
)?;
Expand Down
Loading
Loading