Skip to content

token-fundraiser: add pinocchio example - #698

Open
skyyycodes wants to merge 1 commit into
solana-foundation:mainfrom
skyyycodes:token-fundraiser-pinocchio
Open

token-fundraiser: add pinocchio example#698
skyyycodes wants to merge 1 commit into
solana-foundation:mainfrom
skyyycodes:token-fundraiser-pinocchio

Conversation

@skyyycodes

@skyyycodes skyyycodes commented Aug 26, 2026

Copy link
Copy Markdown

Adds tokens/token-fundraiser/pinocchio, so the fundraiser example exists in Pinocchio as well as Anchor. The README asks for missing framework variants, and this was one of the token examples that only had anchor/.

What it does

A maker opens a campaign with a target and a deadline. Contributors deposit SPL tokens into a vault owned by the fundraiser PDA, each capped at 10% of the target. The maker collects once the target is met; contributors refund themselves if the deadline passes without it.

Four instructions — initialize, contribute, check_contributions, refund — behaviorally matching tokens/token-fundraiser/anchor.

Conventions followed

Modeled on tokens/escrow/pinocchio: client-derived PDA bumps passed in instruction data and re-checked on-chain with create_program_address, and associated-token-account derivation checks on every account that funds move into or out of. Tests are mocha-via-tsx over @solana/kit + litesvm 1.x, per AGENTS.md. The crate is a root workspace member.

Deliberate differences from the Anchor version

  • transferChecked instead of transfer, so the token program enforces the mint and decimals rather than trusting the caller's accounts.
  • checked_pow for the minimum-target check. Mint decimals are a u8 that SPL Token does not cap, the minimum target is 3^decimals, and 3^41 overflows u64. With overflow-checks = true in the release profile, an unchecked pow aborts the instruction with ProgramFailedToComplete instead of returning an error. There's a test that opens a campaign against a decimals=41 mint and asserts the clean InvalidAmount.
  • Elapsed days compared as i64 rather than cast to u16, avoiding a truncating cast on the deadline comparison.
  • check_contributions closes the drained vault as well as the fundraiser account, so the vault's rent isn't stranded under a closed PDA.

Testing

21 litesvm cases covering both lifecycles (funded → payout, expired → refund) plus the negative paths: deadline boundary, per-contributor cap (single and cumulative), target gates in both directions, substituted vault, redirected refund destination, and non-maker payout attempt.

Each negative test asserts the specific ProgramError::Custom code rather than just "it failed", so a test can't pass for the wrong reason.

Validated by mutation testing: 13 invariants were each deliberately broken one at a time (deadline gate, vault ATA check, contributor ATA checks, per-contributor cap, target gates, maker identity check, checked_pow, …) and every one was caught by the test that should catch it — no silent survivors.

Locally verified: cargo fmt --check, cargo clippy -- -D warnings, workspace-membership gate, repo-wide prettier --check, tsc --noEmit, pnpm build, pnpm build-and-test, and cargo test — all green, including from a clean checkout containing only the committed files.

Note for reviewers

contribute creates the contributor PDA with a plain CreateAccount, matching tokens/escrow/pinocchio. Anchor's init_if_needed additionally handles the case where someone pre-funds the PDA address with lamports to grief it. I kept the existing Pinocchio idiom in this repo rather than diverging, but happy to add the transfer/allocate/assign fallback if you'd prefer parity there.

Ports the Anchor fundraiser to Pinocchio, so the example exists in both
flavors. A maker opens a campaign with a target and a deadline; contributors
deposit SPL tokens into a vault owned by the fundraiser PDA, each capped at
10% of the target; the maker collects once the target is met, and contributors
refund themselves if the deadline passes without it.

Follows the conventions in tokens/escrow/pinocchio: client-derived PDA bumps
passed in instruction data and re-checked with create_program_address, and
associated-token-account derivation checks on every account that funds move
into or out of.

Notable differences from the Anchor version, all deliberate:

- transferChecked instead of transfer, so the token program enforces the mint
  and decimals rather than trusting the caller's accounts.
- The minimum-target check uses checked_pow. Mint decimals are a u8 that SPL
  Token does not cap, the minimum target is 3^decimals, and 3^41 overflows
  u64; with overflow-checks on in the release profile an unchecked pow aborts
  the instruction (ProgramFailedToComplete) instead of returning an error.
  Covered by a test that opens a campaign against a decimals=41 mint.
- Elapsed days are compared as i64 rather than cast to u16, avoiding a
  truncating cast on the deadline comparison.
- check_contributions closes the drained vault as well as the fundraiser
  account, so the vault's rent is not stranded under a closed PDA.

Tests: 21 litesvm cases over @solana/kit covering both lifecycles plus the
negative paths (deadline boundary, per-contributor cap, target gates,
substituted vault, redirected refund destination, non-maker payout).
Verified by mutation testing: each of 13 deliberately broken invariants is
caught by the test that should catch it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TYjn7QPXQ65mhYBM2fZqzo
@skyyycodes
skyyycodes requested a review from dev-jodee as a code owner August 26, 2026 17:49
@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a Pinocchio implementation and test suite for the token fundraiser example, registers its Rust crate in the workspace, and links it from the repository README.

  • Implements campaign initialization, capped contributions, successful payout, and expired-campaign refunds.
  • Adds explicit PDA, ATA, signer, mint, deadline, and target validation around token custody.
  • Adds TypeScript/litesvm coverage and project-specific build and test configuration.

Confidence Score: 4/5

The large-target contribution failure should be fixed before merging because the program currently accepts campaigns that can never receive a contribution.

Initialization accepts the full u64 target range, but contribution processing computes 10% through a checked multiplication by 10, causing every contribution to fail for targets above u64::MAX / 10 even though the intended cap remains representable.

Files Needing Attention: tokens/token-fundraiser/pinocchio/program/src/instructions/mod.rs and tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs

Important Files Changed

Filename Overview
tokens/token-fundraiser/pinocchio/program/src/instructions/initialize.rs Creates the fundraiser PDA and vault and stores campaign terms, but permits large targets that later make contribution-cap calculation overflow.
tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs Validates contribution authority, timing, PDAs, ATAs, and cumulative limits before atomically transferring and recording funds.
tokens/token-fundraiser/pinocchio/program/src/instructions/check_contributions.rs Validates maker and custody relationships, pays out a successful campaign, and closes both vault and fundraiser accounts.
tokens/token-fundraiser/pinocchio/program/src/instructions/refund.rs Validates failed-campaign refund eligibility, returns the contributor's recorded amount, updates campaign state, and closes the contributor record.
tokens/token-fundraiser/pinocchio/program/src/instructions/mod.rs Provides shared decoding and account-validation helpers; its multiply-before-divide cap calculation makes some valid campaigns unusable.
tokens/token-fundraiser/pinocchio/program/src/state.rs Defines fixed-width campaign and contributor account layouts with matching serialization and deserialization.
tokens/token-fundraiser/pinocchio/tests/test.ts Adds broad lifecycle and negative-path coverage, though it does not cover targets whose cap calculation overflows.

Reviews (1): Last reviewed commit: "token-fundraiser: add pinocchio example" | Re-trigger Greptile

Comment on lines +82 to +87
pub(crate) fn max_contribution(amount_to_raise: u64) -> Result<u64, ProgramError> {
amount_to_raise
.checked_mul(crate::constants::MAX_CONTRIBUTION_PERCENTAGE)
.map(|scaled| scaled / crate::constants::PERCENTAGE_SCALER)
.ok_or_else(|| FundraiserError::ArithmeticOverflow.into())
}

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.

P1 Contribution cap overflows

When a campaign target exceeds u64::MAX / 10, max_contribution overflows while multiplying before dividing, causing every contribution to fail with ArithmeticOverflow even though the intended 10% cap is representable.

Suggested change
pub(crate) fn max_contribution(amount_to_raise: u64) -> Result<u64, ProgramError> {
amount_to_raise
.checked_mul(crate::constants::MAX_CONTRIBUTION_PERCENTAGE)
.map(|scaled| scaled / crate::constants::PERCENTAGE_SCALER)
.ok_or_else(|| FundraiserError::ArithmeticOverflow.into())
}
pub(crate) fn max_contribution(amount_to_raise: u64) -> Result<u64, ProgramError> {
Ok(amount_to_raise
/ (crate::constants::PERCENTAGE_SCALER / crate::constants::MAX_CONTRIBUTION_PERCENTAGE))
}

Knowledge Base Used: Token escrow, swaps, and fundraising

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.

1 participant