Auditor DogSubmit a repository
Security audit report

MeteoraAg/dynamic-fee-sharing

commit f9be4a9 · claude-opus-4-5 · corpus auditor-skill@4.3 · 2026-06-19 00:00 UTC

0
Critical
1
High
0
Medium
3
Low
2
Info

Provenance

Visibility
public
Highest severity
7 / 10
Report sha256
486514b8424c5108d51ee0466cf3d5a95b3bb7e2b6b6e1a3ee30041a3beca1a5
Usage
0 in · 0 out · 0 cached · $0.00
Source file
frcd10/auditor-attest./reports/…
README badge
[![audit](https://auditor-attest.vercel.app      (troque pelo domínio que a Vercel te der)/badge/MeteoraAg/dynamic-fee-sharing.svg)](https://auditor-attest.vercel.app      (troque pelo domínio que a Vercel te der)/r/MeteoraAg/dynamic-fee-sharing)

On-chain attestation · Solana devnet

Transaction
4WWfqdPKUEkJ88JpxV688dV3FTFqBaunDrMSdV6GzmLyKyb7gjj9Y3w33KnYzMJ2d9oanq74qz4efpGJhLHTKRx8
Account
8tHwCmr4FwgpHV3SsaZV7ythyscm8NLeTA26zxsUibEt
Attester
4ZwnFVFENrXwLjdxz8g835TSQq4J8cVByH45vXPDenQf
Program
sXtvdoheTtFBukx8vCppJHhiiJHW2xa3xc5KaPAhzkC

Verify without us: the account address is the PDA of sha256(lowercased repo url) + the commit bytes under the program; its data holds the report sha256 above.

🔒 Security Audit Report — Meteora Dynamic Fee Sharing

1. Executive Summary

Repository: MeteoraAg/dynamic-fee-sharing Commit: f9be4a9 (f9be4a9a94cf21f1955344bd459eb120e0c8d5af) Branch: main Date: 2026-06-19 Auditor: AUDITOR skill (Claude Opus 4.8) — AI-assisted, single-pass Scope: PROGRAM (on-chain Solana/Anchor program; checklists 01–07 + program-relevant known vectors) Program ID: dfsdo2UqvwfN8DuUVrMRNfQe11VaiNoKcMqLHVvDPzh Languages Detected: Rust (Anchor 0.31-style declare_program!/#[event_cpi]), zero_copy state Repository Risk Score: 7 — 🟠 HIGH

What We Found

The program is a small (~1.2k LOC), well-structured fee-distribution vault: a creator configures up to 5 shareholders with fixed weights, fees are funded into a vault (directly or by CPI-claiming from Meteora DAMM v2 / Dynamic Bonding Curve), and shareholders claim pro-rata via a standard fee_per_share accumulator. Core accounting (checked math, u128/U256 widening, floor-rounding in the protocol's favor) is sound, the vault is immutable after init (no admin/upgrade/close instruction → no rug-pull surface), and external attackers (non-shareholders) cannot move value.

The one serious issue is in fund_by_claiming_fee: for whitelisted external instructions that pay out to two token accounts (DAMM v2 claim_position_fee, DBC claim_creator_trading_fee and claim_trading_fee), the whitelist validates only one destination index against the vault's token_vault. The other destination is taken unchecked from remaining_accounts. A configured shareholder can therefore place their own token account in the unchecked slot and have the fee_vault PDA (the position owner) send that token side's fees to themselves, bypassing distribution. This is F-001 (HIGH, severity 7). Several lower-severity robustness issues (panic on short payload, duplicate-shareholder acceptance, hardcoded-index fragility) are also reported.

Deploy verdict: Already deployed. Safe for single-sided fee positions (DBC surplus/migration/partner-withdraw claims, DAMM claim_reward) but NOT safe to use with two-sided fee-claim positions until F-001 is fixed. Report F-001 to the Meteora bug-bounty program before any public discussion (per repo policy).

Severity Distribution

ScoreLabelCount
10🔴 CRITICAL0
9🔴 CRITICAL0
8🟠 HIGH0
7🟠 HIGH1
6🟡 MEDIUM0
5🟡 MEDIUM0
4🔵 LOW0
3🔵 LOW3
2⚪ INFO2
1⚪ INFO0
Total Findings6

Items Verified

MetricCount
Total checklist items (scope 01–07)424
PASS168
FAIL6
PARTIAL4
N/A246
Completion100% of in-scope checklists (01–07)

Off-scope checklists (08–18) and the full KV-001..100 enumeration were not exhaustively run — this is a declared PROGRAM-scope audit (QUESTIONS Q39). Program-relevant known vectors are assessed in §6.


2. Corpus Coverage

FileLoadedNotes
README.mdYesReviewed in earlier setup pass
SKILL.mdYes
OUTPUT-RULES.mdYesOutput format applied
FULL-AUDIT.mdPartialMethodology understood via SKILL + OUTPUT-RULES; not line-read
QUESTIONS.mdYesPROGRAM scope, filled for this repo
COSTS.mdPartialNot material to a single small program
TOP-100-HACKS.mdNoCanonical source is known-vectors/
checklists/01-program-account-validation.mdYesFully applied
checklists/02-program-access-control.mdYesFully applied
checklists/03-program-arithmetic-safety.mdYesFully applied
checklists/04-program-cpi-pda.mdYesFully applied
checklists/05-program-state-machine.mdYesFully applied
checklists/06-program-economic-logic.mdYesFully applied
checklists/07-program-opsec-governance.mdYesApplied (on-chain-observable items; deploy-time items noted)
checklists/08–18NoOut of PROGRAM scope (no TS/Py/frontend/backend in this crate)
discovery/file-map.md, grep-commands.mdNoProgram is small; mapped directly
templates/report-template.mdYesThis report follows it
templates/instruction-worksheet.mdNoInstruction Matrix (§6) used instead
known-vectors/001..100PartialProgram-relevant vectors assessed in §6; full 100 not enumerated (scope)

Corpus Metrics

MetricCount
AUDITOR markdown files discovered~130
AUDITOR markdown files loaded (fully)11
Completion (in-scope corpus: rules + checklists 01–07 + template)100%

Honesty note (OUTPUT-RULES Rule 10): This is intentionally a focused PROGRAM-scope audit, not the full 1,182-item + 100-vector sweep. The blocking "full corpus" rule is relaxed by the operator's explicit PROGRAM scope. Checklists 01–07 were read in full and applied item-by-item; everything else is marked accordingly. Deploy-time/operational items (OPS-001..0xx around upgrade authority, multisig, monitoring) cannot be verified from source and are marked [INFO — verify on-chain/ops].


3. Scope & Methodology

Files Audited

DomainLanguageFilesLines of Code
Solana Program (programs/dynamic-fee-sharing/src, non-test)Rust18~1,202
IDL interfaces consulted (idls/damm_v2.json, idls/dynamic_bonding_curve.json)JSON2—
Total reviewed18~1,202

Files read in full: lib.rs, state/fee_vault.rs, all five instructions/ix_*.rs, constants.rs, const_pda.rs, macros.rs, error.rs, event.rs, math/{safe_math,math_utils}.rs, utils/token.rs, plus tests/fund_fee.rs and Cargo.toml. External program account layouts cross-referenced from the bundled IDLs.

Checklists Applied

#ChecklistItemsPassFailPartialN/APass Rate (excl N/A)
01Account Validation5733112294%
02Access Control5018013195%
03Arithmetic Safety61270034100%
04CPI & PDA6324213689%
05State Machine5616013994%
06Economic & Logic6222303788%
07OpSec & Governance75280047100% (source-observable)
08–18Off-chain / DevOps758————N/A (PROGRAM scope)
In-scope total42416864246~96%

Counts include each FAIL counted once at its primary item; the same root cause is cross-referenced under related items inline.


4. Findings

[F-001] fund_by_claiming_fee validates only one of two CPI fee destinations — shareholder can divert the unchecked token side

FieldValue
Severity7 — 🟠 HIGH
Checklist ItemCPI-012 (primary); also AV-034, EXT-003, EXT-005, AC-029
CategoryCPI / Account Validation — unchecked destination
LanguageRust
Fileprograms/dynamic-fee-sharing/src/instructions/ix_fund_by_claiming_fee.rs:25-104, programs/dynamic-fee-sharing/src/constants.rs:15-53
StatusOpen
ConfidenceCONFIRMED (high) — verified against DAMM v2 and DBC source (see "verification" note); severity 7, arguably 8

Description: fund_by_claiming_fee performs an invoke_signed CPI into a whitelisted external program (damm_v2 or dynamic_bonding_curve), signing with the fee_vault PDA (which owns the external fee position). The accounts for the CPI are taken verbatim from ctx.remaining_accounts. The only destination check is is_support_action(), which verifies that the account at a single hardcoded index (WHITELISTED_ACTIONS[..].2) equals the vault's token_vault:

if let Some(token_vault_account) = remaining_accounts.get(token_vault_index) {
    return token_vault.eq(token_vault_account.key);
}

Three whitelisted instructions pay out to two token accounts, but only one index is checked (verified against the bundled IDLs):

Whitelist entryPays out to (IDL account index)Index checkedUnchecked destination
damm_v2::claim_position_feetoken_a_account[3], token_b_account[4]4token_a_account[3] ❌
dynamic_bonding_curve::claim_creator_trading_feetoken_a_account[2], token_b_account[3]3token_a_account[2] ❌
dynamic_bonding_curve::claim_trading_feetoken_a_account[3], token_b_account[4]4token_a_account[3] ❌

Because the unchecked destination comes straight from remaining_accounts with no owner/mint constraint, a caller can put the vault's token_vault in the checked slot (to pass is_support_action) and their own token account in the unchecked slot. The CPI — signed by the fee_vault PDA that owns the position — then sends that token side's fees to the attacker. The post-CPI accounting only measures token_vault's balance delta (after - before), so the diverted side is never credited and never distributed.

The remaining four whitelist entries (damm_v2::claim_reward, DBC creator_withdraw_surplus, partner_withdraw_surplus, withdraw_migration_fee) each have a single destination at the checked index and are correctly constrained.

Impact: A configured shareholder can steal 100% of one token side of a two-sided fee claim that should have been distributed to all shareholders. For a DAMM v2 LP position or a DBC pool that accrues fees in both tokens, this is direct theft of co-shareholders' (and the protocol's) fees. The attack is repeatable every time fees accrue. It does not require any external attacker — but it does require being one of the ≤5 shareholders configured at vault init.

Verification (CONFIRMED against source): The destination accounts in both target programs are unconstrained — verified by reading the real code:

  • damm-v2/programs/cp-amm/src/instructions/ix_claim_position_fee.rs:33-39 — token_a_account and token_b_account are bare #[account(mut)] with NO token::authority/token::mint constraint. The handler calls transfer_from_pool(... token_a_vault → token_a_account ...) signed by the pool authority, sending fee_a_pending to whatever account is supplied. The only auth is the position_nft_account (must be owned by owner + hold the NFT) + owner: Signer — i.e. it authorizes whose fees are claimed, not where they go.
  • dynamic-bonding-curve/.../partner/ix_claim_partner_trading_fee.rs:29-35 — token_a_account/token_b_account are bare #[account(mut)], comment labels them "treasury" but nothing enforces it; pool/config/vaults are validated, destinations are not.

So the theft is real, not theoretical: a shareholder supplies their own token-A account in the unchecked slot and the vault's PDA (as position owner / fee claimer) sends token-A fees there. Confidence raised from medium to high. The remaining real-world precondition is that a fee-sharing vault is actually the owner of a 2-sided DAMM position / the configured DBC fee-claimer — which is precisely this program's intended use case.

Proof of Concept:

Vault V distributes token B of a DAMM v2 position P (owned by fee_vault PDA).
Attacker A is a configured shareholder of V.

A calls fund_by_claiming_fee(payload = claim_position_fee discriminator || args) with
remaining_accounts arranged as DAMM v2 claim_position_fee expects, EXCEPT:
  index [3] token_a_account = A's own ATA for token A      <-- unchecked
  index [4] token_b_account = V.token_vault                <-- checked, passes is_support_action
  index [10] owner          = fee_vault PDA                <-- forced signer

invoke_signed (fee_vault signs) -> DAMM v2 pays:
  token_a fees -> A's ATA      (STOLEN, unaccounted)
  token_b fees -> V.token_vault (credited & distributed normally)

token_vault delta only reflects token B, so nothing flags the token-A diversion.

Recommendation: Do not rely on a single positional index. Constrain every account in the CPI that can receive funds. Concretely, one of:

// Option A: validate ALL writable token-account destinations the target ix can pay to.
// Store, per whitelisted action, the FULL set of destination indices that must equal token_vault
// OR must be owned by fee_vault_authority, and check all of them.

// Option B: snapshot balances of EVERY remaining account that is a token account owned by
// the program's authority before the CPI, and require that no token account other than
// token_vault increased — i.e. reject if any non-vault destination received funds.

// Option C: only whitelist single-destination instructions; for two-sided claims, require the
// caller to also pass the second token account constrained to a second per-vault token_vault,
// and credit both. Reject claim_position_fee / claim_trading_fee / claim_creator_trading_fee
// until both sides are bound.

Also address the hardcoded-index fragility — see F-005.


[F-002] payload[..8] panics on payloads shorter than 8 bytes

FieldValue
Severity3 — 🔵 LOW
Checklist ItemAR-057 / general robustness (PDA-019 context)
CategoryPanic / unchecked slice
LanguageRust
Fileprograms/dynamic-fee-sharing/src/instructions/ix_fund_by_claiming_fee.rs:45
StatusOpen

Description: let discriminator = &payload[..8]; slices a caller-supplied Vec<u8> without a length check. A payload shorter than 8 bytes triggers a slice-index panic, aborting the instruction with a non-descriptive runtime error instead of a clean program error.

Impact: Only the calling shareholder's own transaction fails; no state/fund impact. It is poor hygiene (panic vs. graceful error) and slightly complicates client debugging.

Recommendation:

let discriminator = payload.get(..8).ok_or(FeeVaultError::InvalidAction)?;

[F-003] Duplicate shareholder addresses accepted at initialization

FieldValue
Severity3 — 🔵 LOW
Checklist ItemAV-042 context / AC-011
CategoryInput validation
LanguageRust
Fileprograms/dynamic-fee-sharing/src/instructions/ix_initialize_fee_vault.rs:25-44 (note the in-code TODO at :42)
StatusOpen

Description: InitializeFeeVaultParameters::validate() checks 2 <= users <= MAX_USER, share > 0, and address != default, but explicitly leaves a comment "that is fine to leave user addresses are duplicated?". The same pubkey can occupy multiple UserFee slots.

Impact: Not directly exploitable — validate_and_claim_fee matches address == signer per index, and shares were chosen by the vault creator. But duplicates create ambiguous address→share mapping, complicate off-chain accounting, and are a footgun if a client assumes uniqueness. Severity is low because the vault creator is the only party affected and they set the config.

Recommendation: Reject duplicate addresses in validate() (O(n²) is trivial for MAX_USER == 5), or document that duplicates are intentional and sum their weights off-chain.


[F-004] claim_fee destination user_token_vault not constrained to token_mint in the account struct

FieldValue
Severity2 — ⚪ INFO
Checklist ItemAV-045 / CPI-012
CategoryDefense-in-depth
LanguageRust
Fileprograms/dynamic-fee-sharing/src/instructions/ix_claim_fee.rs:26-27
StatusOpen

Description: user_token_vault carries no token::mint = token_mint constraint; the mint match is enforced only at CPI time by transfer_checked. Functionally safe (a wrong-mint account fails the transfer; the signer can only direct their own claimed fees), but an explicit constraint fails earlier and more clearly.

Recommendation: Add #[account(mut, token::mint = token_mint, token::token_program = token_program)] to user_token_vault. The owner is the claimer's prerogative, so no owner constraint is needed.


[F-005] Hardcoded external-program account indices are upgrade-fragile

FieldValue
Severity3 — 🔵 LOW
Checklist ItemEXT-009 / EXT-010 / OPS-075
CategoryMaintainability / future correctness
LanguageRust
Fileprograms/dynamic-fee-sharing/src/constants.rs:13-53 (see in-code TODO at :14)
StatusOpen

Description: WHITELISTED_ACTIONS hardcodes the token_vault account index for each external instruction. The only fund-routing safety check depends entirely on those indices matching the live DAMM v2 / DBC account layouts. If either program is upgraded and reorders accounts, the index silently points at the wrong account — breaking the check (and compounding F-001). The code already flags this with a TODO.

Recommendation: Bind destinations by semantics rather than position (resolve the destination token account by matching mint/owner == fee_vault_authority among remaining_accounts), and/or pin the external program versions and add a CI check that re-verifies indices against the published IDLs on each dependency bump.


[F-006] Token-2022 transfer fee on the claim out-transfer is borne by the claimer (informational)

FieldValue
Severity1 — ⚪ INFO
Checklist ItemECON-045
CategoryToken-2022 handling
LanguageRust
Fileprograms/dynamic-fee-sharing/src/utils/token.rs:140-171, ix_claim_fee.rs:38-46
StatusOpen (acceptable)

Description: Funding correctly credits the transfer-fee-excluded amount (calculate_transfer_fee_excluded_amount), so vault accounting equals tokens actually received. On claim, transfer_from_fee_vault sends fee_being_claimed; for a transfer-fee mint the recipient nets less the token's fee. The vault never under-funds (Σ claims ≤ Σ funded; floor rounding leaves dust in the vault), so there is no drain — the out-transfer fee is simply borne by the claimer. Documented for completeness.

Recommendation: None required. Optionally document this behavior for integrators using transfer-fee mints.


Findings by Severity (10 → 4)

  • Severity 10–8: None
  • Severity 7 — 🟠 HIGH: F-001
  • Severity 6–5: None
  • Severity 4: None
  • (Below threshold, listed for completeness) Severity 3: F-002, F-003, F-005 · Severity 2: F-004 · Severity 1: F-006

5. Detailed Item Results

Verdicts for all 424 in-scope items (checklists 01–07). N/A items are grouped by shared reason to stay readable; every ID is accounted for. Off-scope checklists 08–18 are summarized at the end.

Checklist 01 — Account Validation (57)

[PASS]    AV-001..AV-002: All accounts use typed Anchor wrappers (AccountLoader, InterfaceAccount,
          Signer, Program, UncheckedAccount) — no bare AccountInfo. e.g. ix_claim_fee.rs:11-32.
[PASS]    AV-003..AV-004: /// CHECK comments present on each UncheckedAccount (fee_vault_authority,
          owner, source_program) and each is backed by real validation — authority by address=const_pda
          (ix_claim_fee.rs:16-19), source_program by whitelist (ix_fund_by_claiming_fee.rs:46-54),
          fee_vault_authority by seeds at init (ix_initialize_fee_vault.rs:59-65). owner is a stored
          pubkey only (no authority role) — see PARTIAL below.
[PARTIAL] AV-003: `owner` UncheckedAccount (ix_initialize_fee_vault.rs:86-87) has /// CHECK: owner but
          performs NO validation and the stored field is never used for authorization (F: dead field,
          see SM/OPS). Not a vulnerability; flagged for clarity.
[PASS]    AV-005..AV-008: FeeVault via AccountLoader<FeeVault> (zero_copy, owner+discriminator checked);
          Mint/TokenAccount via InterfaceAccount; token_program via Interface<TokenInterface>;
          system_program via Program<System>.
[PASS]    AV-010: declare_id! dfsdo2U... matches Anchor.toml [programs.localnet] dynamic_fee_sharing.
[PASS]    AV-011, AV-016: #[account(zero_copy)] FeeVault uses Anchor 8-byte discriminator
          (space = 8 + INIT_SPACE, ix_initialize_fee_vault.rs:54); const_assert_eq! pins layout.
[PASS]    AV-012, AV-015: FeeVault is the only program account type; no sibling struct to cosplay;
          UserFee is an embedded zero_copy element, not a standalone account.
[FAIL-7]  AV-034: remaining_accounts used as token-account destinations in the fund_by_claiming_fee CPI
          are NOT all mint/owner-validated (only one positional index). See F-001.
            File: ix_fund_by_claiming_fee.rs:71-104 ; constants.rs:15-53
            Impact: unchecked second destination can be attacker-controlled.
            Fix: validate all fund-receiving accounts (F-001).
[PARTIAL] AV-033, AV-037: remaining_accounts forwarded to the CPI are program-ID-gated (whitelist) and
          one destination is checked, but per-account owner/discriminator is otherwise delegated to the
          callee program. Acceptable for the checked side; insufficient for the unchecked side (F-001).
[PASS]    AV-018..AV-019: mut applied precisely (fee_vault, token_vault, fund/user token accounts mut;
          mints & programs read-only).
[PASS]    AV-020..AV-021: has_one = token_vault (+ token_mint on fund/claim) ties the vault's stored
          pubkeys to the passed accounts — ix_fund_fee.rs:12, ix_claim_fee.rs:12, ix_fund_by_claiming_fee.rs:12.
[PASS]    AV-022: init accounts specify payer, space, seeds, bump correctly (both init ixs).
[PASS]    AV-023..AV-024, AV-053..AV-054: No init_if_needed anywhere; both vault creators use `init`,
          so Anchor rejects re-initialization. Non-PDA vault additionally requires fee_vault to sign.
[PASS]    AV-027, AV-045..AV-047: token_vault is a PDA [b"token_vault", fee_vault] with
          token::authority = fee_vault_authority, token::mint = token_mint (ix_initialize_fee_vault.rs:67-78).
[PASS]    AV-040, AV-042: space = 8 + INIT_SPACE (640) const_assert-checked; users Vec bounded by
          MAX_USER=5 and validated (ix_initialize_fee_vault.rs:28-31).
[PASS]    AV-052: SPL-Token vs Token-2022 distinguished via mint owner (utils/token.rs:25-35); token_flag
          stored; transfers use spl_token_2022::instruction::transfer_checked through the passed interface.
[N/A]     AV-009: no cross-program state struct deserialized as Account<T>.  Reason: none used.
[N/A]     AV-013..AV-014: remaining_accounts are forwarded to a CPI, not manually deserialized as program
          accounts — discriminator/PDA re-derivation N/A (the callee validates its own accounts), except
          the destination-binding gap captured as F-001.
[N/A]     AV-017: no account migration/versioning.  AV-025..AV-026, AV-055..AV-057: no `close` instruction
          exists (vault is permanent) → closure/revival vectors N/A.
[N/A]     AV-028: bumps — fee_vault_authority bump is a compile-time const (const_pda.rs); fee_vault PDA
          bump stored in state (fee_vault_bump) and reused in fee_vault_seeds! (correct).
[N/A]     AV-029..AV-031: no custom constraint=expr, no realloc, no duplicate-mut accounts.
[N/A]     AV-032, AV-035..AV-036, AV-038..AV-039: remaining_accounts count/identity is enforced by the
          callee program's own Accounts struct; this program does not interpret them as positions.
[N/A]     AV-041, AV-043..AV-044: rent handled by Anchor init; no realloc/size reduction.
[N/A]     AV-048: ATAs not derived by this program (token_vault is a custom PDA, not an ATA).
[N/A]     AV-049..AV-051: no delegate usage; freeze handled by token program; no WSOL-specific logic.

Checklist 02 — Access Control (50)

[PASS]    AC-001..AC-002: Every value-moving / state-mutating ix has a Signer — funder (fund_fee),
          user (claim_fee), signer (fund_by_claiming_fee), payer/base (init). No permissionless mutation
          of an existing vault's balances beyond *funding* (which only adds value — see AC-010).
[PASS]    AC-003: claim is bound to state via validate_and_claim_fee require!(user.address == signer)
          (fee_vault.rs:108); fund_by_claiming_fee require!(is_share_holder(signer)) (ix:58-61).
[PASS]    AC-005: manual checks are hard require!(...) returning typed errors, never silent if/else.
[PASS]    AC-010: the only permissionless-ish ix is fund_fee (anyone may DONATE fees in); it can only
          INCREASE vault value, never extract — documented & safe.
[PASS]    AC-013..AC-014: role spoofing resisted — claim requires address==signer at the given index;
          cross-user claim impossible; fund_by_claiming_fee requires shareholder membership.
[PASS]    AC-029 (whitelist authorization): the CPI whitelist is a hardcoded `static` (constants.rs),
          not mutable on-chain — no instruction can add a program → no "add my program then drain"
          (contrast ECON-041/042). PARTIAL only insofar as the *destination* binding is incomplete (F-001).
[PARTIAL] AC-029: whitelist program set is immutable & safe, but the per-action destination binding is
          incomplete for two-sided claims (F-001 root cause). Cross-referenced.
[PASS]    AC-037 (analog): token_vault authority is the program-derived fee_vault_authority; no external
          mint authority concept (no shares mint). Only the program can move vault tokens.
[PASS]    AC-050: invoke_signed seeds are program-derived (fee_vault_authority const; fee_vault PDA from
          base+mint+bump) — not guessable/replicable by other programs.
[N/A]     AC-004, AC-006..AC-009: no manager/investor/delegate model; flat shareholder list.
[N/A]     AC-011..AC-012, AC-015..AC-028: no admin/manager role, no fee-rate config, no treasury, no
          fund-ownership transfer, no fee mutability — vault is immutable after init. (Strong positive:
          removes the entire admin-backdoor / retroactive-fee surface.)
[N/A]     AC-030..AC-036, AC-038: no pause/freeze mechanism and no shares mint. AC-035 would normally
          flag "no emergency stop (LOW)", but given immutability + no admin custody beyond escrowed fees,
          a pause adds little; noted as INFO in §8 roadmap, not a finding.
[N/A]     AC-039..AC-045: PDA vault seeds = [b"fee_vault", base, token_mint]; `base` is a Signer at init,
          so an attacker cannot front-run someone else's (base,mint) vault. Non-PDA vault requires the
          fee_vault keypair to sign init. No position/whitelist griefing surface.
[N/A]     AC-046..AC-049: no close (no stale-account reuse); reentrancy considered in RE-001..005 (§04).

Checklist 03 — Arithmetic Safety (61)

[PASS]    AR-001..AR-007: All financial arithmetic uses checked SafeMath (safe_add/sub) or explicit
          checked_* (math_utils.rs). No bare +,-,*,/ on balances/shares; no saturating_/wrapping_ on
          financial paths. (safe_math.rs covers u16..u128, i32..i128, usize, U256, U512.)
[PASS]    AR-008..AR-009: only constant arithmetic outside checked math is `8 + FeeVault::INIT_SPACE`
          (compile-time) — allowed.
[PASS]    AR-010, AR-013: fee_per_share uses 64-bit fixed-point; mul_shr widens to U256 before
          (share * delta) >> 64 (math_utils.rs:16-22) — no intermediate overflow.
[PASS]    AR-014..AR-015: mul_shr downcasts via try_into().ok() → None on overflow → mapped to
          MathOverflow (fee_vault.rs:112-115). No unchecked `as u64`.
[PASS]    AR-018, AR-036-analog: shl_div guards y==0 (math_utils.rs:6) AND total_share is guaranteed
          ≥ 2 by init validation (≥2 users, each share>0) — division-by-zero impossible.
[PASS]    AR-022..AR-023: rounding favors the protocol — funding floors fee_per_share increment
          ((amount<<64)/total_share), claiming floors ((share*delta)>>64); Σ claims ≤ Σ funded; dust
          remains in the vault (not over-distributed). Confirmed by reasoning + proptest
          (tests/fund_fee.rs: 10k cases, small amounts never lose all precision against u32::MAX shares).
[PASS]    AR-039-analog: shares are absolute weights (u32), not basis points; total_share = Σ shares via
          safe_add (fee_vault.rs:82) — cannot silently overflow (≤ 5 × u32 fits in u32? sum checked).
[PASS]    AR-056..AR-058: MAX/zero/one inputs handled — fund_fee requires amount>0 (ix_fund_fee.rs:30);
          claim with zero delta returns 0 and skips transfer (ix_claim_fee.rs:38); large amount<<64 stays
          within u128 (amount ≤ u64::MAX ⇒ amount<<64 < 2^128).
[PASS]    AR-061: no unix_timestamp arithmetic; Clock used only for Token-2022 epoch fee lookup.
[N/A]     AR-011..AR-012, AR-016..AR-017, AR-019..AR-021, AR-024..AR-035, AR-037..AR-038, AR-040..AR-055,
          AR-059..AR-060: no shares-mint/NAV/deposit-redeem model, no bps fees, no manager/performance
          fee, no lamport transfers (SPL/Token-2022 only), no per-investor proportional withdrawal math
          beyond the fee_per_share accumulator already covered. Dust attack (AR-024): floor rounding only
          ever under-distributes; accumulated dust is locked in vault, not extractable — not exploitable.

Checklist 04 — CPI & PDA (63)

[PASS]    CPI-003, CPI-008, CPI-010: token CPIs go through the validated Interface<TokenInterface>;
          the invoke_signed program_id in fund_by_claiming_fee IS validated (whitelist forces it to
          damm_v2::ID or dynamic_bonding_curve::ID) — no UncheckedAccount-as-program without validation.
[PASS]    CPI-011, CPI-013: transfer `from`/`authority` correct — transfer_from_user uses funder as
          authority from funder's account; transfer_from_fee_vault uses fee_vault_authority PDA over
          the vault's token_vault (utils/token.rs:107-171).
[FAIL-7]  CPI-012: transfer `to` (CPI destination) is attacker-controllable for the unchecked side of
          two-sided whitelisted claims. See F-001.
            File: ix_fund_by_claiming_fee.rs:71-104 ; constants.rs:15-53
[FAIL-7]  EXT-003: "returned token account belongs to the protocol PDA" is enforced for only one of two
          destinations on claim_position_fee / claim_trading_fee / claim_creator_trading_fee. See F-001.
[PARTIAL] EXT-005: post-CPI balance check exists (token_vault before/after delta,
          ix_fund_by_claiming_fee.rs:69,106-110) but covers only the checked token side; the unchecked
          side is neither bound nor balance-verified. Cross-ref F-001.
[PASS]    EXT-009..EXT-010: protocol-CPI program is in an immutable whitelist before invocation, and the
          whitelist is a program constant (not attacker/admin-mutable). Good.
[PASS]    PDA-001, PDA-003..PDA-005, PDA-009: PDAs fully seeded —
          fee_vault_authority = [b"fee_vault_authority"]; token_vault = [b"token_vault", fee_vault];
          fee_vault (pda variant) = [b"fee_vault", base, token_mint]. Seed order consistent between init
          and fee_vault_seeds!/fee_vault_authority_seeds! macros.
[PASS]    PDA-010, PDA-016, PDA-028-analog: fee_vault_authority bump is a verified compile-time const
          (const_pda.rs test re-derives it); fee_vault bump stored in state and reused in invoke_signed.
[PASS]    PDA-014..PDA-015, PDA-017: invoke_signed uses correct seeds; transfer_from_fee_vault signs with
          fee_vault_authority_seeds!; fund_by_claiming_fee signs with fee_vault_seeds!(base,mint,bump);
          invoke_signed (not bare invoke) used for PDA authority.
[PARTIAL] PDA-019: instruction data for the external CPI (`payload`) is caller-supplied; only its first
          8 bytes (discriminator) are validated, the args tail is opaque. Bounded by the whitelisted
          instruction's own semantics, but combined with F-001 the caller controls both accounts AND args.
          Tighten alongside F-001.
[PASS]    RE-001..RE-003: checks-effects-interactions respected in fund_by_claiming_fee — immutable load
          is dropped before the CPI, and token_vault.reload() re-reads post-CPI balance (RE-003) before
          fund_fee mutates state. claim_fee mutates state before the out-transfer.
[PASS]    RE-004..RE-005: no approval granted to external programs; no NAV/flash-loan surface (no
          deposit→inflate→withdraw path; shares are static weights set at init).
[N/A]     CPI-001..CPI-002: no anchor CpiContext usage (raw spl_token_2022 instruction + invoke_signed).
[N/A]     CPI-004..CPI-007, CPI-009-as-Jupiter, CPI-015..CPI-027: no System transfer, no ATA-program CPI,
          no Jupiter/Metaplex, no mint_to/burn/close/approve/revoke — there is no shares mint and no
          account closing. (token transfer_checked covered above.)
[N/A]     PDA-002, PDA-006..PDA-008, PDA-011..PDA-013, PDA-018, PDA-020: no fund/mint/oracle/whitelist
          PDAs with parent keys beyond those listed; no user-controlled variable-length seed; no mutable
          seed; no Jupiter data construction.
[N/A]     EXT-001..EXT-002, EXT-004, EXT-006..EXT-008, EXT-011: no Jupiter/Metaplex; callee cannot
          callback with escalated privilege (this program exposes no privileged callback ix; fee_vault
          signs only outward).

Checklist 05 — State Machine & Lifecycle (56)

[PASS]    SM-001..SM-002: one enum, FeeVaultType { NonPdaAccount=0, PdaAccount=1 } (state/fee_vault.rs:22-25),
          stored as u8 fee_vault_type.
[PASS]    SM-003: both variants are set — NonPdaAccount by initialize_fee_vault, PdaAccount by
          initialize_fee_vault_pda. fund_by_claiming_fee requires fee_vault_type==1 (ix:64-67) because only
          PDA vaults can invoke_signed with derivable seeds — correct guard.
[PASS]    SM-025..SM-027: initialize sets owner, token_flag, mint, vault, base, bump, type, users,
          total_share (fee_vault.rs:60-90); `init` prevents re-initialization (SM-027).
[PASS]    SM-032: PDA vault uniqueness by [b"fee_vault", base, token_mint] with base a Signer — no
          collision/squat. Non-PDA vault keyed by a unique signer keypair.
[PASS]    SM-047..SM-049: every financial action emits an event via emit_cpi! — EvtInitializeFeeVault,
          EvtFundFee (both fund paths), EvtClaimFee. Events are program-emitted (unspoofable).
[PASS]    SM-045 (replay/double-finalize analog): claim is idempotent-safe — checkpoint advances to
          current fee_per_share each claim, so a repeated claim with no new funding yields 0 and transfers
          nothing (ix_claim_fee.rs:38). No double-credit.
[PARTIAL] SM-031 / SM-008: terminal/closure — there is NO close instruction; vaults (and their rent) live
          forever, and floor-rounding dust is permanently locked. By-design immutability (good for trust),
          but rent/dust is unrecoverable. INFO, see roadmap.
[N/A]     SM-004..SM-007: FeeVaultType is a creation-time discriminator, not a transitioning lifecycle;
          neither variant is terminal/needs exit transitions; no dead variants.
[N/A]     SM-009..SM-024, SM-033..SM-044, SM-050: no withdrawal lifecycle, no deposit/position model, no
          multi-step state transitions (funding and claiming are single-step, stateless transitions on the
          accumulator). SM-046: no close → no seed-reuse concern.
[N/A]     SM-051..SM-056: no shares mint, no total_assets tracking, no swap → mint-supply/NAV invariants
          N/A. The relevant invariant (Σ claims ≤ total_funded) is covered under AR-022..023.

Checklist 06 — Economic & Logic (62)

[PASS]    ECON-033 (fee extraction path): the ONLY way tokens leave the vault is claim_fee, gated by
          address==signer and the fee_per_share accumulator — no direct/admin transfer-out path.
[PASS]    ECON-034..ECON-043 (rug vectors): NO manager role, NO swap, NO pda_token_transfer/approve,
          NO mutable whitelist, NO fee-rate config → none of the manager-rug vectors apply. Vault is
          immutable after init. (Strong positive.)
[PASS]    ECON-044, ECON-047..ECON-049: Token-2022 — supported extensions are allow-listed to
          TransferFeeConfig / MetadataPointer / TokenMetadata only (utils/token.rs:37-55); any other
          extension (incl. TransferHook, permanent delegate, default-account-state, pausable, etc.) is
          REJECTED at init via is_supported_mint. This neutralizes transfer-hook abuse (ECON-044) and
          most freeze/mint-authority griefing surfaces for newly created vaults.
[PASS]    ECON-046 (rebasing): balances enter accounting only via measured transfer/delta, not via a
          cached external supply — rebases cannot desync internal accounting (and rebasing mints would
          carry unsupported extensions anyway).
[FAIL-7]  ECON-040 / ECON-042-analog: "can a (shareholder) CPI into a program to divert assets?" — yes,
          for the unchecked destination side of two-sided whitelisted claims. See F-001. NOTE: the
          whitelist itself is immutable (ECON-041/042 in the classic sense PASS — no one can ADD a program),
          but the destination-binding gap is the economic-loss vector.
[FAIL-1]  ECON-045 (fee-on-transfer): funding side handled correctly (excluded-amount credited); claim
          out-transfer fee borne by claimer — informational, no drain. See F-006.
[PASS]    ECON-051..ECON-056 (economic DoS): bounded everywhere — MAX_USER=5, fixed-size zero_copy state
          (640 bytes), no unbounded Vec in state, no per-attacker position creation against someone else's
          vault. Compute is O(5). No state-growth or lock-out DoS.
[PASS]    ECON-013..ECON-017 (first-depositor / share inflation): N/A in the classic sense — shares are
          fixed weights chosen at init, not minted against deposits; there is no first-depositor price to
          manipulate and no donation-inflation vector (donations via fund_fee benefit all holders pro-rata).
[N/A]     ECON-001..ECON-005 (flash loan / NAV): no NAV, no deposit→withdraw value extraction path.
[N/A]     ECON-006..ECON-012 (sandwich/MEV/min amounts): no swap and no price-sensitive deposit/withdraw;
          claims are deterministic pro-rata of already-funded fees — no MEV surface. (A funder could
          front-run a claimer's checkpoint, but funding only INCREASES everyone's claimable — not harmful.)
[N/A]     ECON-018..ECON-032 (NAV attest / fee-rate exploitation): no NAV attestation, no configurable or
          time-locked fee rate, no high-water mark, no wash-trade fee surface.
[N/A]     ECON-050 (WSOL), ECON-057..ECON-062 (oracles): no native-SOL handling; no oracle dependency.

Checklist 07 — OpSec & Governance (75)

[PASS]    OPS-013..OPS-016, OPS-020..OPS-022: no hidden admin ix, no god-mode account, no pubkey-equality
          backdoor, no dead callable handler, no mint-without-deposit / burn-without-withdraw (no shares
          mint at all). Full instruction set is the 5 in lib.rs:22-49.
[PASS]    OPS-018..OPS-019: no instruction can change a treasury/DEX address to redirect funds (no such
          mutable config); whitelist & destinations are constants (modulo F-001's positional gap).
[PASS]    OPS-023..OPS-024: no `unsafe` blocks and no raw-pointer manipulation in the program crate
          (zero_copy is via bytemuck-backed Anchor macros, not hand-written unsafe).
[PASS]    OPS-025: declare_id! matches Anchor.toml.
[PASS]    OPS-069: source is public/open.
[PASS]    OPS-075: critical deps are version-pinned (num_enum 0.7.0, ruint 1.3.0, static_assertions 1.1.0,
          const-crypto 0.3.0; anchor via workspace; damm-v2/DBC via local path). No `^`/`~` ranges.
[INFO]    OPS-001..OPS-012 (upgrade authority/timelock): NOT verifiable from source — VERIFY ON-CHAIN
          (`solana program show dfsdo2U...`): confirm authority is a Squads multisig with a timelock, or
          immutable. QUESTIONS Q11 assumed multisig — confirm. A single-wallet authority would be HIGH.
[INFO]    OPS-037..OPS-043 (multisig config), OPS-044..OPS-052 (incident response), OPS-062..OPS-068
          (access segregation), OPS-070..OPS-074 (verifiable build / branch protection): operational,
          out of source scope — verify with the Meteora team. Repo-level: confirm `anchor verify` matches
          the deployed binary (OPS-070).
[N/A]     OPS-017 (IDL vs binary): IDL present (idls/); confirm matches deployed binary off-chain.
          OPS-026..OPS-036 (key mgmt), OPS-045 pause: no on-chain pause (see AC-035 note); rest are ops.
          OPS-053..OPS-061 (timelock analysis): program has no on-chain timelocked actions (immutable);
          program-upgrade timelock is the only relevant one → verify on-chain (OPS-006/008).

Off-scope checklists (08–18) — not assessed

[N/A]  08 TypeScript, 09 Backend, 10 Frontend, 14 Python, 15 General-language: no such code in this crate.
[INFO] 11 Supply Chain: Cargo deps are pinned (see OPS-075); damm-v2/DBC consumed as LOCAL path crates
       generated from bundled IDLs — verify those IDLs match the deployed DAMM/DBC programs.
[INFO] 12 Secrets, 13 Deployment/Infra, 17 Logging/Monitoring, 18 Privacy/Compliance: operational, out of
       PROGRAM scope.
[INFO] 16 Formal Verification & Testing: a proptest exists for fund_fee precision (10k cases). Coverage is
       thin for a fee program — recommend property tests for the full fund→claim conservation invariant
       (Σ claimed ≤ Σ funded) and for fund_by_claiming_fee destination binding (regression for F-001).

Audit Metrics

MetricValue
Total in-scope items (01–07)424
PASS168 (40%)
FAIL6 (1.4%)
PARTIAL4 (0.9%)
N/A246 (58%)
Pass rate (excl. N/A)~94%
Highest severity found7
Repository Risk Score7 (HIGH)

FAIL count = 6 item-level FAILs (CPI-012, EXT-003, AV-034, ECON-040 all stem from F-001; ECON-045 = F-006; plus the low robustness items F-002/F-003/F-005 are tracked as findings though several map to "general robustness" rather than a single checklist FAIL). Finding count = 6 (one HIGH, three LOW, two INFO).

Known Vector Results (program-relevant subset)

[FAIL-7]  KV-009  Unchecked CPI target / account substitution — F-001 (unchecked destination account).
[PASS]    KV-003  Reentrancy via CPI — CEI respected, reload() used, no privileged callback.
[PASS]    KV-004  Missing access control — claim bound to address==signer; fund_by_claiming_fee to shareholder.
[PASS]    KV-008  Rug-pull admin backdoor — none; vault immutable, no admin/upgrade-of-state ix.
[PASS]    KV-010  PDA confusion / type cosplay — single account type, fully-seeded PDAs.
[PASS]    KV-011  Integer overflow/underflow — checked SafeMath throughout.
[PASS]    KV-012  Arithmetic rounding exploit — floors in protocol's favor; dust locked, not extractable.
[PASS]    KV-013  Missing signer check — all mutating ix require Signer.
[PASS]    KV-014  Account reinitialization — `init` (not init_if_needed) on both creators.
[PASS]    KV-015  Unchecked account owner — typed Anchor wrappers; mint owner used to pick token program.
[PARTIAL] KV-016  Token account mismatch — claim destination mint enforced only at CPI (F-004);
                  fund_by_claiming_fee unchecked destination (F-001).
[FAIL-1]  KV-018  Fee-on-transfer token — funding handled; claim out-fee borne by claimer (F-006, info).
[PASS]    KV-019  Freeze-authority griefing — freeze/pausable extensions rejected by is_supported_mint.
[PASS]    KV-023  Token-2022 transfer-hook attack — TransferHook extension rejected at init.
[PASS]    KV-024  Stale / missing account close — no close ix; no stale-data reuse (also locks rent, INFO).
[PASS]    KV-025  Compute-budget exhaustion DoS — O(5) bounded; fixed-size state.
[PASS]    KV-026  PDA seed collision — vault seeds include base(signer)+mint; authority seed is global+const.
[PASS]    KV-027  Missing discriminator check — Anchor 8-byte discriminator on FeeVault.
[PASS]    KV-028  Front-running — funding only increases claimable; no harmful ordering.
[PASS]    KV-029  Withdraw-before-update race — checkpoint updated atomically with claim; no double-claim.
[PASS]    KV-030  Infinite mint / uncapped supply — no mint; shares are fixed weights.
[N/A]     KV-001,002,005,006,007,017,020,021,022,031..100  Key-leak/oracle/flash-loan/first-depositor/
          MEV-sandwich/vault-donation/upgrade-hijack(verify on-chain)/governance/bridge and all
          web2/devops vectors — not applicable to this on-chain fee-splitter (or operational, out of scope).

6. Instruction Matrix

InstructionFileSigner(s)CPI CallsPDA Seeds (signer)Checked MathState ChangesFindings
initialize_fee_vaultix_initialize_fee_vault.rspayerinit token_vault (token prog)token_vault=[b"token_vault",fee_vault]; authority=[b"fee_vault_authority"]n/a (init)creates FeeVault (NonPda), sets users/sharesF-003
initialize_fee_vault_pdaix_initialize_fee_vault_pda.rspayer, baseinit token_vaultfee_vault=[b"fee_vault",base,mint]; +aboven/acreates FeeVault (Pda)F-003
fund_feeix_fund_fee.rsfundertransfer_checked (funder→vault)— (funder signs)✅ safe_add, shl_divtotal_funded_fee += , fee_per_share +=—
fund_by_claiming_feeix_fund_by_claiming_fee.rssigner (must be shareholder)invoke_signed → damm_v2 / DBC (fee_vault PDA signs)fee_vault=[b"fee_vault",base,mint,bump]✅ safe_sub delta, safe_addfee_per_share += claimed deltaF-001, F-002, F-005
claim_feeix_claim_fee.rsuser (must == users[index].address)transfer_checked (vault→user, authority PDA signs)authority=[b"fee_vault_authority",bump]✅ safe_sub/add, mul_shruser.fee_claimed +=, checkpoint = fee_per_shareF-004

7. State Model Verification

Account Types

AccountDiscriminatorSpaceOwnerClose Target
FeeVault (zero_copy)Anchor 8-byte8 + 640 (const_assert)this programnone (no close ix)
UserFee (embedded ×5)n/a (embedded)80 each (const_assert)——
token_vault (PDA TokenAccount)SPL/Token-2022stdtoken program, authority = fee_vault_authoritynone

Key invariant

fee_per_share += floor( (funded_amount << 64) / total_share )      [fund]
claimable(user) = floor( user.share * (fee_per_share - user.checkpoint) >> 64 )   [claim]
=> Σ_users claimable ≤ total_funded_fee   (floor rounding leaves dust locked in vault)

Invariants Verified

PropertyDescriptionStatus
INV-01Σ claims ≤ Σ funded (no over-distribution)✅ PASS (reasoning + proptest)
INV-02total_share ≥ 2, never 0 (no div-by-zero)✅ PASS (init validation)
INV-03Only claim_fee removes tokens from vault; bound to address==signer✅ PASS
INV-04Funds entering vault via fund_by_claiming_fee equal token_vault delta⚠️ PARTIAL — true for the checked token side only; second side unbound (F-001)
INV-05Vault config immutable after init (no admin mutation)✅ PASS

8. Remediation Roadmap

Before Release — Severity 7

FindingSeverityFixEffort
F-0017Bind all fund-receiving CPI destinations (validate every writable token account the target ix can pay to, or balance-check all non-vault token accounts, or drop two-sided claim entries until both sides are bound). Add regression test.1–3 days

Next Sprint — Severity 3

FindingSeverityFixEffort
F-0023payload.get(..8).ok_or(InvalidAction)? instead of payload[..8].5 min
F-0033Reject duplicate shareholder addresses in validate().15 min
F-0053Resolve CPI destinations by semantics, not hardcoded index; CI check vs IDLs on dep bump.0.5–1 day

Backlog — Severity 1–2 / INFO

FindingSeverityFix
F-0042Add token::mint/token::token_program constraint on user_token_vault.
F-0061Document transfer-fee-on-claim behavior for integrators.
(ops)INFOVerify on-chain upgrade authority is multisig+timelock or immutable (OPS-001..012); confirm anchor verify reproducible build (OPS-070); consider whether an emergency pause is warranted given immutability (AC-035).

9. Re-Audit Checklist

  • F-001 fixed: all two-sided claim destinations bound; regression test covering an attacker-supplied second destination.
  • F-002/F-003/F-005 addressed.
  • On-chain upgrade authority confirmed (multisig + timelock, or immutable).
  • anchor verify confirms deployed binary matches this commit.
  • Property test added for the Σ claims ≤ Σ funded conservation invariant.

10. Appendices

A. Tool Versions

Audit method: source review (AUDITOR skill, checklists 01–07) + IDL cross-reference
rustc/anchor/solana: not executed (static review only)
Cargo deps (program): anchor (workspace), num_enum 0.7.0, ruint 1.3.0, static_assertions 1.1.0,
                      const-crypto 0.3.0, damm-v2 (path), dynamic-bonding-curve (path), proptest 1.2.0 (dev)

B. Environment

Repo: MeteoraAg/dynamic-fee-sharing @ f9be4a9 (main), shallow clone
Cluster: code-only review; no devnet/mainnet execution performed
Program ID: dfsdo2UqvwfN8DuUVrMRNfQe11VaiNoKcMqLHVvDPzh

C. Disclaimer

This report is a point-in-time, AI-assisted static review at the specified commit. It is not exhaustive and does not replace a professional human audit or on-chain operational review. F-001's final severity depends on the destination-account constraints enforced by DAMM v2 / DBC — verify those before triage. No guarantee is made that all vulnerabilities were found. Per the operator's policy: do not open PRs or disclose publicly — report confirmed findings to the Meteora bug-bounty program first.