fix(token-fundraiser): scale minimums by 10^decimals and gate target/refund on current_amount - #9
Open
SwineCoder101 wants to merge 2 commits into
Open
fix(token-fundraiser): scale minimums by 10^decimals and gate target/refund on current_amount#9SwineCoder101 wants to merge 2 commits into
SwineCoder101 wants to merge 2 commits into
Conversation
…lt balance gates target/refund initialize accepts targets below 3 whole tokens (3^decimals instead of 3*10^decimals) and contribute accepts sub-token amounts (1^decimals is always 1). check_contributions and refund read the raw vault balance, so a plain SPL transfer into the vault flips a campaign to target-met, letting the maker sweep it and locking contributors out of refunds.
…refund on current_amount initialize required 3^decimals base units and contribute required 1^decimals (always 1), so neither minimum enforced anything; both now use checked 10^decimals math and return DecimalsOverflow instead of panicking on exotic mints. check_contributions and refund compared the raw vault balance against the target, so a direct SPL transfer into the vault let the maker withdraw early and blocked every contributor's refund; both now read the program-tracked current_amount. check_contributions still sweeps the full vault balance on a genuine success.
SwineCoder101
force-pushed
the
fix/token-fundraiser-decimals-math
branch
from
August 27, 2026 12:23
1b19b49 to
27261e4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug: decimal minimums are never enforced and the raw vault balance gates target-met / refunds
Three logic bugs in the
fundraiserprogram. The "at least 3 tokens" and "at least 1 token" minimums are computed with the wrong exponent base, so they enforce nothing (and the first one panics for exotic mints). Separately,check_contributionsandrefunddecide whether the target was met fromvault.amountinstead of the program's owncurrent_amountcounter, so anyone can flip a campaign to "target met" with a plain SPL transfer into the vault. That lets the maker sweep a vault that never reached its target through the program, bypasses the per-contributor 10% cap, and locks every contributor out ofrefunduntil the maker withdraws.Affected
anchor/programs/fundraiser/src/instructions/initialize.rsanchor/programs/fundraiser/src/instructions/contribute.rsanchor/programs/fundraiser/src/instructions/checker.rsanchor/programs/fundraiser/src/instructions/refund.rsanchor/programs/fundraiser/src/error.rs(newDecimalsOverflowvariant, appended)anchor/readme.MD(code snippets mirrored the buggy checks)There is no
native/orpinocchio/variant of this example.Functionality
initialize(amount, duration)creates a campaign and must reject a target below 3 whole tokens ofmint_to_raise(MIN_AMOUNT_TO_RAISE = 3).contribute(amount)must reject contributions below 1 whole token, cap each contributor at 10% of the target, and add the amount tofundraiser.current_amount.check_contributionslets the maker withdraw once the target has been reached;refundlets a contributor take their contribution back after the deadline if the target was not reached. Both must judge "target reached" by what was actually contributed through the program.The bug
initialize.rs:44-47(pre-fix) checksamount >= MIN_AMOUNT_TO_RAISE.pow(decimals), i.e.3^decimals. For a 6-decimal mint that is 729 base units (0.000729 tokens), for 9 decimals 19,683, for 0 decimals just 1 — never "3 tokens". Withoverflow-checkson,3u64.pow(decimals)also panics fordecimals >= 41.contribute.rs:60-63(pre-fix) checksamount >= 1_u64.pow(decimals), which is always1. Onlyamount == 0is rejected; any dust amount passes.checker.rs:53-56andrefund.rs:65-68(pre-fix) compareself.vault.amountagainstamount_to_raise.fundraiser.current_amountis maintained bycontribute/refundbut never read. The vault is an ordinary ATA, so anyone canspl-token transferinto it without touching the program:check_contributionsthen passes and the maker sweeps the whole vault even though contributors never reached the target through the program (and the 10% per-contributor cap is meaningless, since the "missing" amount can be dropped in directly).refundfails withTargetMetfor every contributor after the deadline, locking their funds until the maker chooses to withdraw.Reproduce
cd tokens/token-fundraiser/anchor pnpm install --frozen-lockfile anchor build --ignore-keys pnpm mocha --import=tsx -t 1000000 tests/minimums-and-accounting.test.tsAgainst the unmodified program,
tests/minimums-and-accounting.test.tsfails all four targeted tests:Tests 1-3 are transactions that must be rejected but are accepted (a 1,000-base-unit target on a 6-decimal mint; a 500-base-unit contribution; a maker withdrawal after a direct 29-token transfer padded the vault to a 30-token target with only 1 token contributed). Test 4 is the contributor's post-deadline refund on the same padded setup, which is wrongly rejected with
TargetMet.Fix
initialize.rs: the minimum isMIN_AMOUNT_TO_RAISE * 10^decimals, computed withchecked_pow/checked_mul. Overflow (a mint with too many decimals to express a whole token inu64) returns the newDecimalsOverflowerror instead of panicking.contribute.rs: the minimum contribution is10^decimals, computed withchecked_powand the same overflow error.checker.rs/refund.rs: the target-met checks readfundraiser.current_amount.check_contributionsstill transfersvault.amount, so once the target is genuinely reached the maker also receives any tokens that were sent to the vault directly; the fundraiser account is closed in the same instruction, so nothing is left stranded.refundstill transfers exactlycontributor_account.amount, so a padded vault can never be drained by contributors.error.rs:DecimalsOverflowis appended, so existing error codes are unchanged.readme.MD: the four code snippets and two prose lines that reproduced the buggy checks now match the program.Existing fixtures (30-token targets, 1-token contributions, a 3-token target in
checker-mint-binding.test.ts) already satisfy the corrected minimums, so no test data needed adjusting. The untouchedcargo fmtpass on the edited files also normalized their import layout.Verification
cd tokens/token-fundraiser/anchor pnpm install --frozen-lockfile anchor build --ignore-keys pnpm mocha --import=tsx -t 1000000 tests/minimums-and-accounting.test.ts tests/litesvm.test.ts tests/checker-mint-binding.test.tsThe full
Anchor.tomltest script (anchor test --validator legacyin CI, which also runs the validator-basedtests/fundraiser.ts) was run against a freshsolana-test-validator --resetwith the fixed.sopreloaded at thedeclare_id!address:cargo clippy -p fundraiser -- -D warnings -A clippy::diverging_sub_expression # same flags as CIpasses (the allowed lint is a known false positive from Anchor 1.0's
#[program]expansion and fires identically on the unmodified code).