add pinocchio token-fundraiser example#616
Conversation
Greptile SummaryThis PR adds a Pinocchio version of the token fundraiser example. The main changes are:
Confidence Score: 4/5This is close, but the time boundary should be fixed before merging.
tokens/token-fundraiser/pinocchio/program/src/instructions/contribute.rs Important Files Changed
Reviews (2): Last reviewed commit: "fix(token-fundraiser): correct time-wind..." | Re-trigger Greptile |
| let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; | ||
| if fundraiser_state.duration > elapsed_days { | ||
| return Err(FundraiserError::FundraiserEnded.into()); | ||
| } |
There was a problem hiding this comment.
Active Window Rejects Contributions
When a fundraiser has a non-zero duration, elapsed_days starts below duration, so this branch returns FundraiserEnded during the active window. A 7-day fundraiser rejects normal day-0 contributions and only starts accepting them after the window has passed.
| let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; | |
| if fundraiser_state.duration > elapsed_days { | |
| return Err(FundraiserError::FundraiserEnded.into()); | |
| } | |
| let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; | |
| if elapsed_days > fundraiser_state.duration { | |
| return Err(FundraiserError::FundraiserEnded.into()); | |
| } |
| let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; | ||
| if fundraiser_state.duration < elapsed_days { | ||
| return Err(FundraiserError::FundraiserNotEnded.into()); | ||
| } |
There was a problem hiding this comment.
After a failed fundraiser expires, elapsed_days becomes greater than duration, so this branch returns FundraiserNotEnded. Contributors can be blocked from reclaiming funds after the configured fundraising window has actually ended.
| let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; | |
| if fundraiser_state.duration < elapsed_days { | |
| return Err(FundraiserError::FundraiserNotEnded.into()); | |
| } | |
| let elapsed_days = ((current_time - fundraiser_state.time_started) / SECONDS_TO_DAYS) as u16; | |
| if elapsed_days < fundraiser_state.duration { | |
| return Err(FundraiserError::FundraiserNotEnded.into()); | |
| } |
| Transfer { | ||
| from: contributor_ata, | ||
| to: vault, | ||
| authority: contributor, | ||
| amount, | ||
| } | ||
| .invoke()?; |
There was a problem hiding this comment.
Unverified Vault Records Deposits
This transfer records a contribution without checking that vault is the fundraiser PDA's token account for mint_to_raise. A contributor can send tokens to another token account they control, still get a contributor record and current_amount update, then use that record during refund to withdraw tokens from the real vault.
| let vault_amount = TokenAccount::from_account_view(vault)?.amount(); | ||
| if vault_amount >= fundraiser_state.amount_to_raise { | ||
| return Err(FundraiserError::TargetMet.into()); | ||
| } |
There was a problem hiding this comment.
Caller-Supplied Vault Controls Refund Eligibility
Refund eligibility is based on the balance of whichever vault account the caller supplies, but the account is not checked against the fundraiser mint or PDA authority before this decision. An unrelated low-balance token account can make the target look unmet, so the refund path can proceed from state that does not describe the real fundraiser vault.
…count Addresses review feedback: - Flip the contribute/refund day-window comparisons so contributions are accepted during the active window and refunds after it ends. - Record the vault token account in fundraiser state at initialize and verify the caller-supplied vault against it in contribute, check, and refund, preventing a substituted-vault drain.
| if elapsed_days > fundraiser_state.duration { | ||
| return Err(FundraiserError::FundraiserEnded.into()); | ||
| } |
There was a problem hiding this comment.
End Boundary Overlaps When
elapsed_days == duration, this path still accepts a new contribution, while the refund path already treats the fundraiser as ended because it only rejects elapsed_days < duration. A contributor can deposit during the same boundary state where refunds are allowed, which can change the vault balance and current_amount after the refund window has opened. Use the same boundary on both paths so contributions stop when refunds become available.
| if elapsed_days > fundraiser_state.duration { | |
| return Err(FundraiserError::FundraiserEnded.into()); | |
| } | |
| if elapsed_days >= fundraiser_state.duration { | |
| return Err(FundraiserError::FundraiserEnded.into()); | |
| } |
Description
Adds a Pinocchio port of the
tokens/token-fundraiserexample, sitting alongside the existinganchorimplementation. This brings the token-fundraiser example to a second framework and continues the series of Pinocchio token examples in this repo (after transfer-tokens, escrow, and the open metaplex ports).The program lets a maker open a fundraiser targeting a given SPL mint and amount; contributors deposit tokens into a PDA-owned vault until the goal is reached. It demonstrates:
[b"fundraiser", maker]) is the vault's authority and signs disbursements viainvoke_signed.[b"contributor", fundraiser, contributor]) is created on first contribution (init-if-needed) and closed on refund.Clocksysvar for the fundraising window and enforces a per-contributor 10% cap.Instructions
Tests
tests/test.tsruns againstsolana-bankrun: initialize → contribute ×2 → reject an over-cap contribution → reject checking before the target is met → refund, asserting account state and token balances at each step.Notes
tokens/escrow/pinocchioexample.