Skip to content

Feat/update share, add user, remove user#16

Open
bangyro wants to merge 35 commits intorelease_0.1.2from
feat/update_share
Open

Feat/update share, add user, remove user#16
bangyro wants to merge 35 commits intorelease_0.1.2from
feat/update_share

Conversation

@bangyro
Copy link
Copy Markdown
Collaborator

@bangyro bangyro commented Feb 3, 2026

No description provided.

@bangyro bangyro marked this pull request as ready for review February 4, 2026 05:01
@bangyro bangyro changed the title Feat/update share Feat/update share and remove user Feb 16, 2026
Comment on lines +26 to +27
#[msg("Invalid number of users")]
InvalidNumberOfUsers,
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)

Comment thread programs/dynamic-fee-sharing/src/utils/access_control.rs Outdated
Comment thread programs/dynamic-fee-sharing/src/utils/access_control.rs
Comment thread programs/dynamic-fee-sharing/src/instructions/admin/ix_remove_user.rs Outdated
Comment thread programs/dynamic-fee-sharing/src/instructions/owner/ix_update_operator.rs Outdated
Comment thread programs/dynamic-fee-sharing/src/state/fee_vault.rs Outdated
Comment thread programs/dynamic-fee-sharing/src/state/fee_vault.rs Outdated
Comment thread programs/dynamic-fee-sharing/src/state/fee_vault.rs Outdated
Comment thread programs/dynamic-fee-sharing/src/state/fee_vault.rs Outdated
Comment thread programs/dynamic-fee-sharing/src/state/fee_vault.rs Outdated
Comment thread programs/dynamic-fee-sharing/src/state/fee_vault.rs Outdated
@codewithgun
Copy link
Copy Markdown

codewithgun commented Feb 26, 2026

  1. Can we rename admin folder to operator? admin and owner folder naming feel the same to me.
  2. Currently, once user being removed, there's no way to add back.

@bangyro bangyro force-pushed the feat/update_share branch 2 times, most recently from b743f3a to d98b8bf Compare February 27, 2026 04:57
@bangyro
Copy link
Copy Markdown
Collaborator Author

bangyro commented Feb 27, 2026

@codewithgun I've renamed it to operator and added ix_add_user to add user to a fee_vault

Comment thread programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs Outdated
use crate::{error::FeeVaultError, state::FeeVault};
use anchor_lang::prelude::*;

pub fn verify_is_admin<'info>(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not just enforce:
fee_vault.operator.eq(signer),

and change function to verify_is_operator?

FeeVaultError::InvalidUserAddress
);
require!(
share > 0 && share != user.share,
Copy link
Copy Markdown
Contributor

@andrewsource147 andrewsource147 Mar 4, 2026

Choose a reason for hiding this comment

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

I dont think we need the check share != user.share or even share > 0, otherwise it make operations harder:

  • Operator could launch in background service, and they may not track if share == user.share, we could return OK(()) if share is still the same
  • Sometimes, they just want to set share to zero, but dont want to remove users out of the fee_vault


#[event_cpi]
#[derive(Accounts)]
pub struct RemoveUserCtx<'info> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What happen if operator remove user, then add back again, then remove again? is there any issues for user to claim?

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.

@andrewsource147 Should work fine.

The first time the user is removed from a FeeVault and they have unclaimed_fee, we will create the UserUnclaimedFee PDA to store their unclaimed_fee.

If they get re-added and removed. We will add any unclaimed_fee into the PDA. If it's still unclaimed.


#[account(zero_copy)]
#[derive(InitSpace, Debug, Default)]
pub struct UserUnclaimedFee {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

could you move it to other file? and should add user and fee_vault pubkey in account, so it is easier for user or fee_vault to filter unclaimedFee account

pub fee_per_share: u128,
pub base: Pubkey,
pub padding: [u128; 4],
pub operator: Pubkey,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

when fee_vault.initialize(), we should set operator == owner. If owner want to change operator to new wallet, they could do later

@andrewsource147
Copy link
Copy Markdown
Contributor

I dont see you are implementing fee_vault immutable. Note that:

  • All existed fee vault must be immutable, it meant no-one could update or add/remove user shares
  • If owner want to create fee_vault mutable, they need to specify a flag to indicate it is mutable

)?;
}

emit_cpi!(EvtClaimUnclaimedFee {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we should emit event only if fee_being_claimed > 0

pub fn handle_add_user(ctx: Context<AddUserCtx>, share: u32) -> Result<()> {
let mut fee_vault = ctx.accounts.fee_vault.load_mut()?;
let user = ctx.accounts.user.key();
fee_vault.validate_and_add_user(&user, share)?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we extend fee_vault account, instead of just finding empty slot in fee_vault? we can allow more than max_user = 5

Comment on lines +70 to +73
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);
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

pub const MIN_USER: usize = 2;
pub const MAX_USER: usize = 5;
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.

@bangyro
Copy link
Copy Markdown
Collaborator Author

bangyro commented Mar 12, 2026

/bot review

@metoeraag-bot
Copy link
Copy Markdown

metoeraag-bot Bot commented Mar 12, 2026

Bot Agent progress: /bot review

Progress:

  • Repo checks (4 ok, 1 failed)
  • Context build
  • Codex review (1 pass + first look)
  • Buddy review (agreed after 3/10)
  • Post review

Copy link
Copy Markdown

@metoeraag-bot metoeraag-bot Bot left a comment

Choose a reason for hiding this comment

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

Summary

⚠️ Limited-confidence review: check failed: cargo clippy --workspace --all-targets -- -D warnings.

Remaining issues are that the documented owner+operator admin model isn't reflected in access control, zero-share users violate the share invariant, the owner cannot set themselves back as operator, claim_unclaimed_fee can be permanently blocked by operator rotation, remove_user can send reclaimed rent to an arbitrary unchecked receiver, and is_share_holder incorrectly matches Pubkey::default() for empty slots.

Inline comments

See inline comments on the diff.

Claude (API) review

Rounds: 3 (min=1, max=10).
First look: Codex + Claude (Claude first-look failed; fell back to Codex).
Agreement: yes (no new findings/questions).

Debate transcript

First look comparison

Codex (first look):

Summary:
The new mutable/operator and dynamic-user flow mostly hangs together, but I found a few concrete risks around admin permissions, state invariants, and client-facing account layout.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/utils/access_control.rs:15: This access check only accepts fee_vault.operator, but the changelog says the vault owner and the operator should both be able to run admin instructions. After update_operator delegates to another key, the owner will be rejected on `add
  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:112: initialize_fee_vault still enforces share > 0, but update_share here (and add_user below) no longer preserve that invariant. Allowing share == 0 creates members that still count toward MIN_USER/MAX_USER and pass `is_share_hold
  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:33: The extra UserFee entries are stored past FeeVault::INIT_SPACE, outside the declared FeeVault account layout. That means Anchor/IDL decoders still only expose the first 5 users; the tests had to add a custom raw-byte parser for the ta

Codex (first look fallback):

Summary:
Adds dynamic user management and unclaimed-fee handling, but there are two correctness gaps: owner admin access is not actually honored after operator rotation, and zero-share add/update paths can leave the vault in an invalid fee-sharing state.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/utils/access_control.rs:15: This access check only accepts fee_vault.operator, but the new behavior described for this release says the vault owner and the operator should both be able to run admin instructions. After update_operator rotates the operator away from
  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:112: Initialization enforces share > 0, but this path (and add_user below) do not. That lets an operator create a vault whose remaining members all have zero share: e.g. zero out two users here, then remove the last positive-share user, or a

Comparison notes:

Review synthesis

  • No major disagreement: both reviews agree the top two correctness issues are:
    1. Admin auth regressionaccess_control only permits operator, not owner || operator, so the owner loses admin rights after update_operator.
    2. Broken share invariantadd_user / update_share allow share == 0, which can leave the vault in an invalid fee-sharing state.
  • Different emphasis: Review A additionally calls out a client/account-layout risk: extra UserFee entries are written beyond FeeVault::INIT_SPACE, so Anchor/IDL decoding only exposes the first 5 users.
  • Different framing: Review B focuses more on the zero-share issue as a concrete invalid-state scenario and mentions unclaimed-fee handling, but doesn’t raise the layout/IDL concern.

Most important follow-up action:
Block merge until admin authorization is fixed to allow owner or operator, with regression tests for operator rotation (then immediately enforce share > 0 on add/update).

Round 1

Codex (primary):

Summary:
The new mutable/operator and dynamic-user flow mostly hangs together, but I found a few concrete risks around admin permissions, state invariants, and client-facing account layout.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/utils/access_control.rs:15: This access check only accepts fee_vault.operator, but the changelog says the vault owner and the operator should both be able to run admin instructions. After update_operator delegates to another key, the owner will be rejected on `add
  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:112: initialize_fee_vault still enforces share > 0, but update_share here (and add_user below) no longer preserve that invariant. Allowing share == 0 creates members that still count toward MIN_USER/MAX_USER and pass `is_share_hold
  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:33: The extra UserFee entries are stored past FeeVault::INIT_SPACE, outside the declared FeeVault account layout. That means Anchor/IDL decoders still only expose the first 5 users; the tests had to add a custom raw-byte parser for the ta

Claude (API):

Critique of Primary Review

The primary review correctly identifies the two most critical issues (admin auth regression and zero-share invariant gap) and the IDL/layout concern. However:

Missed Issues

  1. claim_unclaimed_fee is fragile across operator rotation. The has_one = operator constraint on ClaimUnclaimedFeeCtx means if the operator changes between when a user is removed and when they try to claim, the claim will fail permanently. This is a funds-at-risk issue — the removed user's unclaimed fees become unclaimable. The primary review didn't flag this.

  2. Owner cannot reclaim operator role. update_operator rejects operator == owner, so the owner can never set themselves back as operator after delegating. This is a usability/governance issue the primary review didn't mention.

Potential False Positive

The IDL/layout concern (line 33) is valid as a DX observation but overstated as a risk — the code explicitly handles this with DynamicFeeVaultLoader and the tests use a raw parser. It's an inherent limitation of Anchor's fixed-layout IDL when dealing with variable-length accounts, not a bug.

Overall

The primary review's top two findings are solid and should block merge. The claim_unclaimed_fee operator-rotation issue is arguably more severe (permanent fund lock) and should be added to the blocking set.

Codex (response):

  • New findings applied: 2/3
  • False positives addressed: 1/1
  • Open questions kept for author: 2/2

Round 2

Codex (primary):

Summary:
Dropping the dynamic-layout/IDL comment: the variable-length tail is intentional. Remaining issues are that the documented owner+operator admin model isn't reflected in access control, zero-share users violate the share invariant, the owner cannot set themselves back as operator, and claim_unclaimed_fee can be permanently blocked by operator rotation.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/utils/access_control.rs:15: This helper only accepts fee_vault.operator, but the CHANGELOG says the vault owner and the operator should both be able to run admin instructions. After delegation, the owner will be rejected on add_user / remove_user / `update_user_
  • programs/dynamic-fee-sharing/src/instructions/owner/ix_update_operator.rs:25: The operator != owner check prevents the vault owner from setting themselves back as operator to revoke a delegation. Consider removing this constraint and only rejecting no-op updates where the new operator already equals the current one
  • programs/dynamic-fee-sharing/src/instructions/ix_claim_unclaimed_fee.rs:12: 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
  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:112: initialize_fee_vault still enforces share > 0, but update_share here (and add_user below) no longer preserve that invariant. Allowing share == 0 creates members that still occupy MIN_USER/MAX_USER slots and satisfy `is_share_h

Claude (API):

Critique of Primary Review (Round 2)

The primary review now covers the four most important issues well:

  1. Admin auth regression (operator-only, not owner+operator)
  2. Zero-share invariant gap
  3. Owner cannot reclaim operator role
  4. claim_unclaimed_fee fragile across operator rotation

Additional Findings

  1. Unconstrained rent_receiver in remove_user — The primary review didn't flag that rent_receiver in RemoveUserCtx is a fully unchecked mutable account receiving lamports from the shrink operation. While the operator is semi-trusted, this is an unnecessary attack surface.

  2. is_share_holder matches Pubkey::default() — Empty user slots have address == Pubkey::default(). The is_share_holder predicate doesn't exclude these, so it would incorrectly return true for Pubkey::default(). This is used in fund_by_claiming_fee's authorization check. While Pubkey::default() can't sign a transaction, the predicate is semantically incorrect and should be hardened.

No False Positives to Retract

All four primary review findings remain valid and well-justified.

Overall Assessment

The primary review is solid after incorporating the prior critique's findings. The two new findings above are lower severity but worth addressing before merge.

Codex (response):

  • New findings applied: 2/3
  • Open questions kept for author: 2/2

Round 3

Codex (primary):

Summary:
Remaining issues are that the documented owner+operator admin model isn't reflected in access control, zero-share users violate the share invariant, the owner cannot set themselves back as operator, claim_unclaimed_fee can be permanently blocked by operator rotation, remove_user can send reclaimed rent to an arbitrary unchecked receiver, and is_share_holder incorrectly matches Pubkey::default() for empty slots.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/utils/access_control.rs:15: This helper only accepts fee_vault.operator, but the CHANGELOG says the vault owner and the operator should both be able to run admin instructions. After delegation, the owner will be rejected on add_user / remove_user / `update_user_
  • programs/dynamic-fee-sharing/src/instructions/owner/ix_update_operator.rs:25: The operator != owner check prevents the vault owner from setting themselves back as operator to revoke a delegation. Consider removing this constraint and only rejecting no-op updates where the new operator already equals the current one
  • programs/dynamic-fee-sharing/src/instructions/ix_claim_unclaimed_fee.rs:12: 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
  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:112: initialize_fee_vault still enforces share > 0, but update_share here (and add_user below) no longer preserve that invariant. Allowing share == 0 creates members that still occupy MIN_USER/MAX_USER slots and satisfy `is_share_h
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:32: rent_receiver is an unchecked, unconstrained mutable account, and the excess lamports from shrink_dynamic_user are sent there. Since the actual authority is already signer, consider constraining rent_receiver to signer.key() (or e
  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:75: is_share_holder only checks address equality, so an empty slot with address == Pubkey::default() makes is_share_holder(&Pubkey::default()) return true. Even if current callers mostly mitigate this, the predicate should exclude defau

Claude (API):

Critique of Primary Review (Round 3)

The primary review now comprehensively covers all six significant issues identified across prior rounds:

  1. Admin auth regression (access_control only checks operator, not owner)
  2. Owner cannot reclaim operator role (operator != owner check)
  3. claim_unclaimed_fee fragile across operator rotation (has_one = operator)
  4. Zero-share invariant gap (add_user / update_share allow share == 0)
  5. Unconstrained rent_receiver in remove_user
  6. is_share_holder matches Pubkey::default()

All findings are well-justified with correct line references and actionable fixes. The two prior-round questions (owner-as-admin intent and zero-share on add) remain appropriately open for the author. No new issues were identified that the primary review missed, and no false positives need retraction.

The review is solid and ready to block merge on items 1–4 (security/correctness) with items 5–6 as recommended improvements.

Codex (response):

Agreement reached. No further changes.

Synthesis

Review synthesis

  • No major disagreement: both reviews agree the top two correctness issues are:
    1. Admin auth regressionaccess_control only permits operator, not owner || operator, so the owner loses admin rights after update_operator.
    2. Broken share invariantadd_user / update_share allow share == 0, which can leave the vault in an invalid fee-sharing state.
  • Different emphasis: Review A additionally calls out a client/account-layout risk: extra UserFee entries are written beyond FeeVault::INIT_SPACE, so Anchor/IDL decoding only exposes the first 5 users.
  • Different framing: Review B focuses more on the zero-share issue as a concrete invalid-state scenario and mentions unclaimed-fee handling, but doesn’t raise the layout/IDL concern.

Most important follow-up action:
Block merge until admin authorization is fixed to allow owner or operator, with regression tests for operator rotation (then immediately enforce share > 0 on add/update).

Repo checks

Repo Checks

  • LLM checks planner: added package install step before running JS commands.

  • rustup component add --toolchain 1.85.0-x86_64-unknown-linux-gnu clippy: ok

info: component 'clippy' for target 'x86_64-unknown-linux-gnu' is up to date
  • cargo fetch --locked: ok
  • npm ci: ok
added 203 packages, and audited 204 packages in 9s

31 packages are looking for funding
  run `npm fund` for details

10 vulnerabilities (1 low, 3 moderate, 6 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated glob@7.2.0: Glob versions prior to v9 are no longer supported
  • cargo check --workspace: ok
Checking dynamic-fee-sharing v0.1.2 (/data/workdir/MeteoraAg/dynamic-fee-sharing/programs/dynamic-fee-sharing)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.55s
  • cargo clippy --workspace --all-targets -- -D warnings: failed
             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: the following explicit lifetimes could be elided: 'info
  --> programs/dynamic-fee-sharing/src/utils/token.rs:86:31
   |
86 | pub fn get_epoch_transfer_fee<'info>(
   |                               ^^^^^
87 |     token_mint: &InterfaceAccount<'info, Mint>,
   |                                   ^^^^^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
help: elide the lifetimes
   |
86 ~ pub fn get_epoch_transfer_fee(
87 ~     token_mint: &InterfaceAccount<'_, Mint>,
   |

error: using `clone` on type `TransferFee` which implements the `Copy` trait
   --> programs/dynamic-fee-sharing/src/utils/token.rs:101:24
    |
101 |         return Ok(Some(transfer_fee_config.get_epoch_fee(epoch).clone()));
    |                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try dereferencing it: `*transfer_fee_config.get_epoch_fee(epoch)`
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy
    = note: `-D clippy::clone-on-copy` implied by `-D warnings`
    = help: to override `-D warnings` add `#[allow(clippy::clone_on_copy)]`

error: importing legacy numeric constants
 --> programs/dynamic-fee-sharing/src/tests/fund_fee.rs:1:5
  |
1 | use std::u32;
  |     ^^^^^^^^
  |
  = help: remove this import
  = note: then `u32::<CONST>` will resolve to the respective associated constant
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#legacy_numeric_constants
  = note: `-D clippy::legacy-numeric-constants` implied by `-D warnings`
  = help: to override `-D warnings` add `#[allow(clippy::legacy_numeric_constants)]`

error: could not compile `dynamic-fee-sharing` (lib) due to 28 previous errors
warning: build failed, waiting for other jobs to finish...
error: could not compile `dynamic-fee-sharing` (lib test) due to 41 previous errors

Comment thread programs/dynamic-fee-sharing/src/utils/access_control.rs
Comment thread programs/dynamic-fee-sharing/src/instructions/owner/ix_update_operator.rs Outdated
#[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 programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs
Comment thread programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs
@bangyro
Copy link
Copy Markdown
Collaborator Author

bangyro commented Mar 12, 2026

/bot review

@metoeraag-bot
Copy link
Copy Markdown

metoeraag-bot Bot commented Mar 12, 2026

Bot Agent progress: /bot review

Progress:

  • Repo checks (4 ok, 2 failed)
  • Context build
  • Codex review (1 pass + first look)
  • Buddy review (max rounds 10/10)
  • Post review

Copy link
Copy Markdown

@metoeraag-bot metoeraag-bot Bot left a comment

Choose a reason for hiding this comment

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

Summary

⚠️ Limited-confidence review: check failed: cargo fmt --check.

One correctness concern remains: removing the last non-zero-share user can leave the vault with MIN_USER users but total_share == 0, which can break later fee-funding paths that divide by total share.

Inline comments

See inline comments on the diff.

Claude (API) review

Rounds: 10 (min=1, max=10).
First look: Codex + Claude (Claude first-look failed; fell back to Codex).
Agreement: no (see notes below).

Debate transcript

First look comparison

Codex (first look):

Summary:
The new mutable/operator flow looks solid overall, but I found two correctness issues in the remove-user/unclaimed-fee path that can leave a vault unusable or break repeat remove/claim cycles.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:194: This updates total_share without re-validating that the vault still has any positive share left. Because update_share allows setting a user's share to 0, you can reach a state like [0, 0, X] and then remove the last non-zero-share use
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:52: data_is_empty() is too weak as the reuse check for this PDA. After claim_unclaimed_fee, Anchor's close path typically leaves a closed account with allocated data/discriminator state rather than data_len == 0, so a remove -> claim ->

Codex (first look fallback):

Summary:
Dynamic-user support looks mostly coherent, but the new unclaimed-fee flow is coupled too tightly to the mutable operator field, which can reroute rent refunds and make removed-user claims depend on later admin configuration.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/instructions/ix_claim_unclaimed_fee.rs:26: Closing user_unclaimed_fee to the current fee_vault.operator means already-issued claims depend on later admin state. If the owner rotates operator after remove_user, the rent refund for this PDA is redirected to a different add
  • programs/dynamic-fee-sharing/src/instructions/owner/ix_update_operator.rs:24: update_operator only rejects Pubkey::default() and the current operator, but the operator must later sign add_user / remove_user / update_user_share, and is also used as the close target in claim_unclaimed_fee. Setting it to a P

Comparison notes:

Review synthesis

  • No direct disagreement: both reviews say the feature is broadly solid but identify correctness risks in the new remove-user / unclaimed-fee flow.
  • Review A emphasizes state-machine safety: total_share can end up in an invalid/brickable state after zeroing shares, and data_is_empty() is too weak to support safe PDA reuse after close.
  • Review B emphasizes authority coupling: claim_unclaimed_fee depends on the current operator, so later operator changes can reroute rent refunds; update_operator validation is also too permissive for a signer/close-target role.
  • Difference in scope: A focuses on repeated lifecycle behavior (remove -> claim -> re-add), while B focuses on admin mutability and who receives funds.

Most important follow-up action:
Refactor the unclaimed-fee account lifecycle/authority model end-to-end: make the close recipient stable/explicit, harden PDA reuse checks, and add invariants/tests for zero-share and remove -> claim -> re-add paths.

Round 1

Codex (primary):

Summary:
The new mutable/operator flow looks solid overall, but I found two correctness issues in the remove-user/unclaimed-fee path that can leave a vault unusable or break repeat remove/claim cycles.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:194: This updates total_share without re-validating that the vault still has any positive share left. Because update_share allows setting a user's share to 0, you can reach a state like [0, 0, X] and then remove the last non-zero-share use
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:52: data_is_empty() is too weak as the reuse check for this PDA. After claim_unclaimed_fee, Anchor's close path typically leaves a closed account with allocated data/discriminator state rather than data_len == 0, so a remove -> claim ->

Claude (API):

(no additional notes)

Codex (response):

  • Open questions kept for author: 1/1

Round 2

Codex (primary):

Summary:
The new mutable/operator flow looks solid overall, but I found two correctness issues in the remove-user/unclaimed-fee path that can leave a vault unusable or break repeat remove/claim cycles.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:194: This updates total_share without re-validating that the vault still has any positive share left. Because update_share allows setting a user's share to 0, you can reach a state like [0, 0, X] and then remove the last non-zero-share use
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:52: data_is_empty() is too weak as the reuse check for this PDA. After claim_unclaimed_fee, Anchor's close path typically leaves a closed account with allocated data/discriminator state rather than data_len == 0, so a remove -> claim ->

Claude (API):

(no additional notes)

Codex (response):

  • Open questions kept for author: 1/1

Round 3

Codex (primary):

Summary:
The new mutable/operator flow looks solid overall, but I found two correctness issues in the remove-user/unclaimed-fee path that can leave a vault unusable or break repeat remove/claim cycles.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:194: This updates total_share without re-validating that the vault still has any positive share left. Because update_share allows setting a user's share to 0, you can reach a state like [0, 0, X] and then remove the last non-zero-share use
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:52: data_is_empty() is too weak as the reuse check for this PDA. After claim_unclaimed_fee, Anchor's close path typically leaves a closed account with allocated data/discriminator state rather than data_len == 0, so a remove -> claim ->

Claude (API):

(no additional notes)

Codex (response):

  • Open questions kept for author: 1/1

Round 4

Codex (primary):

Summary:
The new mutable/operator flow looks solid overall, but I found two correctness issues in the remove-user/unclaimed-fee path that can leave a vault unusable or break repeat remove/claim cycles.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:194: This updates total_share without re-validating that the vault still has any positive share left. Because update_share allows setting a user's share to 0, you can reach a state like [0, 0, X] and then remove the last non-zero-share use
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:52: data_is_empty() is too weak as the reuse check for this PDA. After claim_unclaimed_fee, Anchor's close path typically leaves a closed account with allocated data/discriminator state rather than data_len == 0, so a remove -> claim ->

Claude (API):

(no additional notes)

Codex (response):

  • Open questions kept for author: 1/1

Round 5

Codex (primary):

Summary:
No actionable critic feedback changed the original conclusions: the new mutable/operator flow looks solid overall, but I still found two correctness issues in the remove-user/unclaimed-fee path that can leave a vault unusable or break repeat remove/claim cycles.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:194: This updates total_share without re-validating that the vault still has any positive share left. Because update_share allows setting a user's share to 0, you can reach a state like [0, 0, X] and then remove the last non-zero-share use
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:52: data_is_empty() is too weak as the reuse check for this PDA. After claim_unclaimed_fee, Anchor's close path typically leaves a closed account with allocated data/discriminator state rather than data_len == 0, so a remove -> claim ->

Claude (API):

(no additional notes)

Codex (response):

  • Open questions kept for author: 1/1

Round 6

Codex (primary):

Summary:
The critic feedback did not provide any actionable false-positive callouts or new findings, so the original review stands: the mutable/operator flow looks solid overall, but there are still two correctness issues in the remove-user/unclaimed-fee path that can leave a vault unusable or break repeat remove/claim cycles.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:194: This updates total_share without re-validating that the vault still has any positive share left. Because update_share allows setting a user's share to 0, you can reach a state like [0, 0, X] and then remove the last non-zero-share use
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:52: data_is_empty() is too weak as the reuse check for this PDA. After claim_unclaimed_fee, Anchor's close path typically leaves a closed account with allocated data/discriminator state rather than data_len == 0, so a remove -> claim ->

Claude (API):

(no additional notes)

Codex (response):

  • Open questions kept for author: 1/1

Round 7

Codex (primary):

Summary:
No actionable critic feedback was available to revise the review: there were no false-positive callouts and no new findings to add, so the original two correctness concerns remain unchanged.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:194: This updates total_share without re-validating that the vault still has any positive share left. Because update_share allows setting a user's share to 0, you can reach a state like [0, 0, X] and then remove the last non-zero-share use
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:52: data_is_empty() is too weak as the reuse check for this PDA. After claim_unclaimed_fee, Anchor's close path typically leaves a closed account with allocated data/discriminator state rather than data_len == 0, so a remove -> claim ->

Claude (API):

(no additional notes)

Codex (response):

  • Open questions kept for author: 1/1

Round 8

Codex (primary):

Summary:
Critic feedback did not identify any false positives or new findings to revise the review (the only note was that critic output parsing failed), so the original two correctness concerns remain unchanged.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:194: This updates total_share without re-validating that the vault still has any positive share left. Because update_share allows setting a user's share to 0, you can reach a state like [0, 0, X] and then remove the last non-zero-share use
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:52: data_is_empty() is too weak as the reuse check for this PDA. After claim_unclaimed_fee, Anchor's close path typically leaves a closed account with allocated data/discriminator state rather than data_len == 0, so a remove -> claim ->

Claude (API):

(no additional notes)

Codex (response):

  • Open questions kept for author: 1/1

Round 9

Codex (primary):

Summary:
Critic feedback did not identify any false positives or actionable new findings to revise the review, so the original two correctness concerns remain unchanged.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:194: This updates total_share without re-validating that the vault still has any positive share left. Because update_share allows setting a user's share to 0, you can reach a state like [0, 0, X] and then remove the last non-zero-share use
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:52: data_is_empty() is too weak as the reuse check for this PDA. After claim_unclaimed_fee, Anchor's close path typically leaves a closed account with allocated data/discriminator state rather than data_len == 0, so a remove -> claim ->

Claude (API):

(no additional notes)

Codex (response):

  • Open questions kept for author: 1/1

Round 10

Codex (primary):

Summary:
The critic feedback did not identify any actionable false positives or new findings to revise the review (it appears the critic output parse failed), so the original two correctness concerns remain unchanged.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:194: This updates total_share without re-validating that the vault still has any positive share left. Because update_share allows setting a user's share to 0, you can reach a state like [0, 0, X] and then remove the last non-zero-share use
  • programs/dynamic-fee-sharing/src/instructions/operator/ix_remove_user.rs:52: data_is_empty() is too weak as the reuse check for this PDA. After claim_unclaimed_fee, Anchor's close path typically leaves a closed account with allocated data/discriminator state rather than data_len == 0, so a remove -> claim ->

Claude (API):

(no additional notes)

Codex (response):

  • Open questions kept for author: 1/1
Synthesis

Review synthesis

  • No direct disagreement: both reviews say the feature is broadly solid but identify correctness risks in the new remove-user / unclaimed-fee flow.
  • Review A emphasizes state-machine safety: total_share can end up in an invalid/brickable state after zeroing shares, and data_is_empty() is too weak to support safe PDA reuse after close.
  • Review B emphasizes authority coupling: claim_unclaimed_fee depends on the current operator, so later operator changes can reroute rent refunds; update_operator validation is also too permissive for a signer/close-target role.
  • Difference in scope: A focuses on repeated lifecycle behavior (remove -> claim -> re-add), while B focuses on admin mutability and who receives funds.

Most important follow-up action:
Refactor the unclaimed-fee account lifecycle/authority model end-to-end: make the close recipient stable/explicit, harden PDA reuse checks, and add invariants/tests for zero-share and remove -> claim -> re-add paths.

Repo checks

Repo Checks

  • rustup component add --toolchain 1.85.0-x86_64-unknown-linux-gnu rustfmt: ok
info: downloading component 'rustfmt'
info: installing component 'rustfmt'
  • rustup component add --toolchain 1.85.0-x86_64-unknown-linux-gnu clippy: ok
info: downloading component 'clippy'
info: installing component 'clippy'
  • cargo fetch --locked: ok
  • cargo check --workspace: ok
Checking dynamic-fee-sharing v0.1.2 (/data/workdir/MeteoraAg/dynamic-fee-sharing/programs/dynamic-fee-sharing)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.12s
  • cargo fmt --check: failed
Diff in /data/workdir/MeteoraAg/dynamic-fee-sharing/programs/dynamic-fee-sharing/src/instructions/ix_claim_fee.rs:3:
 
 use crate::const_pda;
 use crate::event::EvtClaimFee;
-use crate::state::FeeVault;
 use crate::state::DynamicFeeVaultLoader;
+use crate::state::FeeVault;
 use crate::utils::token::transfer_from_fee_vault;
 
 #[event_cpi]
  • cargo clippy --workspace --all-targets -- -D warnings: failed
er_fee = epoch_transfer_fee
   |  ____________________________^
67 | |             .calculate_fee(transfer_fee_included_amount)
68 | |             .ok_or_else(|| FeeVaultError::MathOverflow)?;
   | |_______________________________________________________^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations
help: use `ok_or` instead
   |
68 |             .ok_or(FeeVaultError::MathOverflow)?;
   |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: unnecessary closure used to substitute value for `Option::None`
  --> programs/dynamic-fee-sharing/src/utils/token.rs:69:44
   |
69 |           let transfer_fee_excluded_amount = transfer_fee_included_amount
   |  ____________________________________________^
70 | |             .checked_sub(transfer_fee)
71 | |             .ok_or_else(|| FeeVaultError::MathOverflow)?;
   | |_______________________________________________________^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations
help: use `ok_or` instead
   |
71 |             .ok_or(FeeVaultError::MathOverflow)?;
   |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: importing legacy numeric constants
 --> programs/dynamic-fee-sharing/src/tests/fund_fee.rs:1:5
  |
1 | use std::u32;
  |     ^^^^^^^^
  |
  = help: remove this import
  = note: then `u32::<CONST>` will resolve to the respective associated constant
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#legacy_numeric_constants
  = note: `-D clippy::legacy-numeric-constants` implied by `-D warnings`
  = help: to override `-D warnings` add `#[allow(clippy::legacy_numeric_constants)]`

error: could not compile `dynamic-fee-sharing` (lib) due to 7 previous errors
warning: build failed, waiting for other jobs to finish...
error: could not compile `dynamic-fee-sharing` (lib test) due to 20 previous errors

Comment thread programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs
@bangyro
Copy link
Copy Markdown
Collaborator Author

bangyro commented Mar 14, 2026

/bot review

@metoeraag-bot
Copy link
Copy Markdown

metoeraag-bot Bot commented Mar 14, 2026

Bot Agent progress: /bot review

Progress:

  • Repo checks (4 ok, 1 failed)
  • Context build
  • Codex review (1 pass + first look)
  • Buddy review (agreed after 3/10)
  • Post review

Copy link
Copy Markdown

@metoeraag-bot metoeraag-bot Bot left a comment

Choose a reason for hiding this comment

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

Summary

⚠️ Limited-confidence review: check failed: cargo clippy --workspace --all-targets -- -D warnings.

The dynamic user/operator changes still look mostly coherent, but four review points remain well supported: two off-curve signer footguns (user initialization/add-user validation and operator updates), one removed-user fee-lock in claim_unclaimed_fee, and one dynamic-user test-helper regression.

Inline comments

See inline comments on the diff.

Claude (API) review

Rounds: 3 (min=1, max=10).
First look: Codex + Claude (Claude first-look failed; fell back to Codex).
Agreement: yes (no new findings/questions).

Debate transcript

First look comparison

Codex (first look):

Summary:
The dynamic user/operator feature set looks coherent overall, but I found two correctness footguns around accepting pubkeys that can never satisfy the later signer checks.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/instructions/ix_initialize_fee_vault.rs:39: This validation still accepts off-curve/PDA user addresses. That's risky because both claim_fee and claim_unclaimed_fee require user: Signer<'info>, so an off-curve user can never claim and its share of the vault becomes permanently s
  • programs/dynamic-fee-sharing/src/instructions/owner/ix_update_operator.rs:11: operator is later enforced via signer: Signer<'info> on add_user / update_user_share / remove_user, but this setter currently accepts any unchecked pubkey. If the owner sets a PDA/off-curve address here, no one will ever be able t

Codex (first look fallback):

Summary:
The dynamic-user changes look mostly coherent, but the removed-user claim path is now unnecessarily tied to the current operator, which can lock already-earned fees, and one shared test helper still assumes only the fixed users array exists.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/instructions/ix_claim_unclaimed_fee.rs:26: Closing user_unclaimed_fee to operator (together with fee_vault.has_one = operator) makes a removed user's claim depend on whatever the current operator pubkey is, even though the operator never authorizes this instruction. If the own
  • tests/common/index.ts:430: This helper still inspects getFeeVault(...).users, which only contains the fixed header slots. After a user is added via realloc, updateUserShare() can succeed on-chain but this assertion will see undefined because the dynamic tail is

Comparison notes:

Review synthesis

Top differences in emphasis

  1. Codex focuses on input validation at write time: preventing off-curve/PDA user or operator pubkeys that can never satisfy later Signer checks.
  2. Buddy focuses on claim-path authorization at read time: a removed user’s claim_unclaimed_fee is incorrectly coupled to the current operator, which can lock already-earned fees.
  3. Buddy also flags a test gap/regression (tests/common/index.ts still assumes only fixed users), while Codex is concentrated purely on on-chain correctness.

Most important follow-up action

  • Fix all fee-locking paths first: decouple removed-user claims from the current operator and enforce that stored user/operator keys are signer-capable (not off-curve/PDAs), then add tests covering dynamic users and removed-user claims.

Round 1

Codex (primary):

Summary:
The dynamic user/operator feature set looks coherent overall, but I found two correctness footguns around accepting pubkeys that can never satisfy the later signer checks.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/instructions/ix_initialize_fee_vault.rs:39: This validation still accepts off-curve/PDA user addresses. That's risky because both claim_fee and claim_unclaimed_fee require user: Signer<'info>, so an off-curve user can never claim and its share of the vault becomes permanently s
  • programs/dynamic-fee-sharing/src/instructions/owner/ix_update_operator.rs:11: operator is later enforced via signer: Signer<'info> on add_user / update_user_share / remove_user, but this setter currently accepts any unchecked pubkey. If the owner sets a PDA/off-curve address here, no one will ever be able t

Claude (API):

(no additional notes)

Codex (response):

  • Open questions kept for author: 1/1

Round 2

Codex (primary):

Summary:
The dynamic user/operator feature set looks coherent overall, and I still see two correctness footguns around accepting pubkeys that can never satisfy the later signer checks.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/instructions/ix_initialize_fee_vault.rs:39: This validation still accepts off-curve/PDA user addresses. That's risky because both claim_fee and claim_unclaimed_fee require user: Signer<'info>, so an off-curve user can never claim and its share of the vault becomes permanently s
  • programs/dynamic-fee-sharing/src/instructions/owner/ix_update_operator.rs:11: operator is later enforced via signer: Signer<'info> on add_user / update_user_share / remove_user, but this setter currently accepts any unchecked pubkey. If the owner sets a PDA/off-curve address here, no one will ever be able t

Claude (API):

Delta assessment

The primary review correctly identifies the off-curve/PDA pubkey acceptance issue for initialize_fee_vault and update_operator. However, it misses three significant issues:

  1. Fee-locking via has_one = operator — The claim_unclaimed_fee instruction's has_one = operator constraint means a removed user's ability to claim depends on the current operator. If the operator changes post-removal, the claim becomes impossible. This is the highest-severity issue in the PR.

  2. Test helper regression for dynamic usersupdateUserShare() in tests/common/index.ts searches only getFeeVault(...).users (5 fixed slots). This silently breaks for dynamic users (index ≥ 5). The getUserFees() helper exists and handles both arrays.

  3. Minor: off-by-one / semantics on MIN_USER — The remove guard uses get_user_count() > MIN_USER with MIN_USER = 2, meaning you can never go below 3 users. This may be intentional but contradicts the naming.

The primary review's two findings are valid and worth keeping — accepting PDA/off-curve keys that can never sign is a real footgun. No false positives identified.

Codex (response):

  • New findings applied: 3/3
  • Open questions kept for author: 2/2

Round 3

Codex (primary):

Summary:
The dynamic user/operator feature set still looks coherent overall, but I now see five review points: two off-curve signer footguns, one removed-user fee-lock in claim_unclaimed_fee, one dynamic-user test-helper regression, and a minor MIN_USER invariant/clarity concern.

Inline comment highlights:

  • programs/dynamic-fee-sharing/src/instructions/ix_initialize_fee_vault.rs:39: This validation still accepts off-curve/PDA user addresses. That's risky because both claim_fee and claim_unclaimed_fee require user: Signer<'info>, so an off-curve user can never claim and its share of the vault becomes permanently s
  • programs/dynamic-fee-sharing/src/instructions/owner/ix_update_operator.rs:11: operator is later enforced via signer: Signer<'info> on add_user / update_user_share / remove_user, but this setter currently accepts any unchecked pubkey. If the owner sets a PDA/off-curve address here, no one will ever be able t
  • programs/dynamic-fee-sharing/src/instructions/ix_claim_unclaimed_fee.rs:12: The has_one = operator constraint couples a removed user's claim to the current vault operator. If the owner updates the operator after a user is removed, that user can no longer satisfy this account check even though their `user_unclai
  • programs/dynamic-fee-sharing/src/state/dynamic_fee_vault.rs:182: Please double-check the intended invariant here. With MIN_USER = 2 and a guard of get_user_count() > MIN_USER, removals are only allowed while there are at least 3 current users, so the post-removal floor is 2. If that's the intended be
  • tests/common/index.ts:430: getFeeVault(svm, feeVault).users only covers the 5 fixed user slots. When updateUserShare is used for a dynamic user (index >= 5), the .find() here returns undefined and the assertion throws with an unhelpful error, masking the real

Claude (API):

The primary review now covers all five significant issues identified across both independent reviews and prior critique rounds:

  1. Off-curve/PDA user addresses at init (ix_initialize_fee_vault.rs:39) — correctly flagged.
  2. Off-curve/PDA operator address (ix_update_operator.rs:11) — correctly flagged.
  3. Fee-locking via has_one = operator (ix_claim_unclaimed_fee.rs:12) — correctly flagged with clear description of the lock-out scenario.
  4. MIN_USER invariant clarity (dynamic_fee_vault.rs:182) — correctly flagged as needing author confirmation.
  5. Test helper regression for dynamic users (tests/common/index.ts:430) — correctly flagged.

All three new findings from the previous critique round were applied, and both open questions were kept. No false positives were identified in the primary review. The review is comprehensive and accurate.

Codex (response):

Agreement reached. No further changes.

Synthesis

Review synthesis

Top differences in emphasis

  1. Codex focuses on input validation at write time: preventing off-curve/PDA user or operator pubkeys that can never satisfy later Signer checks.
  2. Buddy focuses on claim-path authorization at read time: a removed user’s claim_unclaimed_fee is incorrectly coupled to the current operator, which can lock already-earned fees.
  3. Buddy also flags a test gap/regression (tests/common/index.ts still assumes only fixed users), while Codex is concentrated purely on on-chain correctness.

Most important follow-up action

  • Fix all fee-locking paths first: decouple removed-user claims from the current operator and enforce that stored user/operator keys are signer-capable (not off-curve/PDAs), then add tests covering dynamic users and removed-user claims.
Repo checks

Repo Checks

  • LLM checks planner: added package install step before running JS commands.

  • rustup component add --toolchain 1.85.0-x86_64-unknown-linux-gnu clippy: ok

info: component 'clippy' for target 'x86_64-unknown-linux-gnu' is up to date
  • cargo fetch --locked: ok
  • npm ci: ok
added 203 packages, and audited 204 packages in 8s

31 packages are looking for funding
  run `npm fund` for details

10 vulnerabilities (1 low, 3 moderate, 6 high)

To address issues that do not require attention, run:
  npm audit fix

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated glob@7.2.0: Glob versions prior to v9 are no longer supported
  • cargo check --workspace: ok
Checking dynamic-fee-sharing v0.1.2 (/data/workdir/MeteoraAg/dynamic-fee-sharing/programs/dynamic-fee-sharing)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.65s
  • cargo clippy --workspace --all-targets -- -D warnings: failed
er_fee = epoch_transfer_fee
   |  ____________________________^
67 | |             .calculate_fee(transfer_fee_included_amount)
68 | |             .ok_or_else(|| FeeVaultError::MathOverflow)?;
   | |_______________________________________________________^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations
help: use `ok_or` instead
   |
68 |             .ok_or(FeeVaultError::MathOverflow)?;
   |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: unnecessary closure used to substitute value for `Option::None`
  --> programs/dynamic-fee-sharing/src/utils/token.rs:69:44
   |
69 |           let transfer_fee_excluded_amount = transfer_fee_included_amount
   |  ____________________________________________^
70 | |             .checked_sub(transfer_fee)
71 | |             .ok_or_else(|| FeeVaultError::MathOverflow)?;
   | |_______________________________________________________^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations
help: use `ok_or` instead
   |
71 |             .ok_or(FeeVaultError::MathOverflow)?;
   |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

error: importing legacy numeric constants
 --> programs/dynamic-fee-sharing/src/tests/fund_fee.rs:1:5
  |
1 | use std::u32;
  |     ^^^^^^^^
  |
  = help: remove this import
  = note: then `u32::<CONST>` will resolve to the respective associated constant
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#legacy_numeric_constants
  = note: `-D clippy::legacy-numeric-constants` implied by `-D warnings`
  = help: to override `-D warnings` add `#[allow(clippy::legacy_numeric_constants)]`

error: could not compile `dynamic-fee-sharing` (lib) due to 7 previous errors
warning: build failed, waiting for other jobs to finish...
error: could not compile `dynamic-fee-sharing` (lib test) due to 20 previous errors

@@ -38,8 +39,14 @@ impl InitializeFeeVaultParameters {
self.users[i].address.ne(&Pubkey::default()),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This validation still accepts off-curve/PDA user addresses. That's risky because both claim_fee and claim_unclaimed_fee require user: Signer<'info>, so an off-curve user can never claim and its share of the vault becomes permanently stranded. I'd reject !address.is_on_curve() here, and mirror the same check in DynamicFeeVault::validate_add_user for the new add_user path.

pub fee_vault: AccountLoader<'info, FeeVault>,

/// CHECK: can be any address
pub operator: UncheckedAccount<'info>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

operator is later enforced via signer: Signer<'info> on add_user / update_user_share / remove_user, but this setter currently accepts any unchecked pubkey. If the owner sets a PDA/off-curve address here, no one will ever be able to satisfy those signer checks again and the vault's operator flows are effectively bricked. Please reject non-curve keys (for example with operator.key().is_on_curve()) or require the new operator to co-sign this change.

Comment thread tests/common/index.ts
const res = sendTransactionOrExpectThrowError(svm, tx);
expect(res instanceof TransactionMetadata).to.be.true;

const userFee = getFeeVault(svm, feeVault).users.find((u) =>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

getFeeVault(svm, feeVault).users only covers the 5 fixed user slots. When updateUserShare is used for a dynamic user (index >= 5), the .find() here returns undefined and the assertion throws with an unhelpful error, masking the real test result. Please use getUserFees() instead so the assertion covers both fixed and dynamic users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants