Skip to content

fix(tokens/token-fundraiser): bind check_contributions to the recorded mint - #685

Open
moviendome wants to merge 3 commits into
solana-foundation:mainfrom
moviendome:fix/fundraiser-checker-mint-binding
Open

fix(tokens/token-fundraiser): bind check_contributions to the recorded mint#685
moviendome wants to merge 3 commits into
solana-foundation:mainfrom
moviendome:fix/fundraiser-checker-mint-binding

Conversation

@moviendome

Copy link
Copy Markdown
Contributor

Summary

In tokens/token-fundraiser (anchor), the check_contributions instruction
does not bind the mint to the one recorded when the campaign was created.

contribute and refund both constrain the fundraiser account with
has_one = mint_to_raise, so the mint account passed in the transaction has to
equal the mint_to_raise stored at initialize. checker.rs is the one
instruction of the three without that constraint:

#[account(
    mut,
    seeds = [b"fundraiser".as_ref(), maker.key().as_ref()],
    bump = fundraiser.bump,
    close = maker,
)]
pub fundraiser: Account<'info, Fundraiser>,

Its mint_to_raise — and the vault derived from it via
associated_token::mint = mint_to_raise — are therefore whatever the caller
supplies, kept self-consistent with each other but not tied to the campaign.
The vault.amount >= amount_to_raise goal check then measures an unrelated
token account, and because the same instruction carries close = maker, a call
made with a substitute mint also closes the Fundraiser account — the state
refund needs in order to return real contributors' deposits.

Fix

Add the same has_one = mint_to_raise the two sibling instructions already
carry:

#[account(
    mut,
    seeds = [b"fundraiser".as_ref(), maker.key().as_ref()],
    bump = fundraiser.bump,
    has_one = mint_to_raise,
    close = maker,
)]
pub fundraiser: Account<'info, Fundraiser>,

Test

Adds tests/checker-mint-binding.test.ts, a self-contained litesvm spec that:

  1. initializes a campaign against one mint,
  2. funds the fundraiser's ATA for a different mint up to the goal, and
  3. calls check_contributions with that different mint.

It asserts the call is rejected and the campaign account still exists. Without
the constraint the call succeeds and the campaign is closed; with it, Anchor
rejects the mismatch (ConstraintHasOne) before the handler runs.

Verified red→green against the fix.

…d mint

contribute.rs and refund.rs both constrain the fundraiser account with
has_one = mint_to_raise, so the mint passed in the transaction must equal
the one recorded at initialize. checker.rs omits that constraint: its
mint_to_raise, and the vault derived from it, are whatever the caller
supplies. The goal check then runs against an unrelated token, and the
close = maker on the same account destroys the campaign state that every
contributor's refund depends on.

Add the same has_one the two sibling instructions already carry, and a
regression test that funds a vault for a substitute mint to the goal and
asserts check_contributions rejects it and leaves the campaign intact.
@moviendome
moviendome requested a review from dev-jodee as a code owner August 10, 2026 03:11
@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR binds check_contributions to the fundraiser’s recorded mint and adds a LiteSVM regression test for mismatched-mint calls.

  • Adds Anchor’s has_one = mint_to_raise constraint to the fundraiser account validation.
  • Verifies that a substitute mint is rejected and the fundraiser account remains open.
  • Adds the Chai promise assertion dependencies used by the regression test.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
tokens/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs Adds the correctly named and typed mint-binding constraint before handler execution and account closure.
tokens/token-fundraiser/anchor/tests/checker-mint-binding.test.ts Adds a focused regression test that constructs the prior substitute-mint scenario and checks both rejection and fundraiser preservation.
tokens/token-fundraiser/anchor/package.json Adds test-only Chai promise assertion dependencies whose resolved versions are committed in the lockfile.
tokens/token-fundraiser/anchor/pnpm-lock.yaml Records consistent locked resolutions for the newly added test dependencies.

Reviews (3): Last reviewed commit: "test: assert the mint rejection with rej..." | Re-trigger Greptile

await provider.sendAndConfirm(fundTx, [maker]);

// Call the payout instruction with the fake mint and its funded vault.
let rejected = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

try catch for "finding an error", isn't the proper way to do it, you should actually assert that the error is present, else your assertion is too vague

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback!

Added chai-as-promised and the test now asserts the call rejects with the specific AnchorError (ConstraintHasOne) instead of catching anything.

Followed the pattern already used in tokens/token-2022/transfer-hook/counter

Per review: a try/catch that flags any error is too vague. Follow the
token-2022/transfer-hook pattern — assert the call rejects with the
specific AnchorError (ConstraintHasOne) via chai-as-promised.
@moviendome
moviendome requested a review from dev-jodee August 13, 2026 04:06
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.

2 participants