feat(token-fundraiser): add teardown for failed campaigns - #731
feat(token-fundraiser): add teardown for failed campaigns#731devtechedge wants to merge 2 commits into
Conversation
A campaign that expires under target left the vault and fundraiser accounts behind with their rent unrecovered, and any tokens deposited directly into the vault stayed locked. Adds a maker-callable teardown instruction that runs once the duration has elapsed, requires every recorded contribution to be refunded first, sweeps the remaining vault balance to the maker, and closes the vault and fundraiser accounts. Fixes solana-foundation#725
|
| authority: self.fundraiser.to_account_info(), | ||
| }; | ||
|
|
||
| close_account(CpiContext::new_with_signer(cpi_program, close_accounts, &signer_seeds), None)?; |
There was a problem hiding this comment.
anchor_spl 1.0.2 defines close_account with only a CpiContext argument, but this call also passes None. This prevents the fundraiser program from compiling, so the new teardown instruction cannot run. Remove the second argument, consistent with the repository's other close_account calls.
| close_account(CpiContext::new_with_signer(cpi_program, close_accounts, &signer_seeds), None)?; | |
| close_account(CpiContext::new_with_signer(cpi_program, close_accounts, &signer_seeds))?; |
| it('Teardown is rejected while contributions are outstanding', async () => { | ||
| const vault = getAssociatedTokenAddressSync(mint, fundraiser, true); | ||
|
|
||
| await expectAnchorError( | ||
| program.methods | ||
| .teardown() | ||
| .accountsPartial({ | ||
| maker: maker.publicKey, | ||
| mintToRaise: mint, | ||
| fundraiser, | ||
| vault, | ||
| makerAta: makerATA, | ||
| tokenProgram: TOKEN_PROGRAM_ID, | ||
| systemProgram: anchor.web3.SystemProgram.programId, | ||
| associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, | ||
| }) | ||
| .signers([maker]) | ||
| .rpc(), | ||
| 'UnrefundedContributions', | ||
| ); | ||
| }); |
There was a problem hiding this comment.
The teardown tests run only after the shared clock has reached the deadline, so none directly exercises the new FundraiserNotEnded guard. Add a pre-deadline teardown attempt; otherwise, removing or weakening that guard could go unnoticed because the existing rejection test would still fail on UnrefundedContributions.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
What and why
A failed campaign (one that expires under target) had no teardown path:
refundreturns each recorded contribution and closes the contributor record, but the vault and the fundraiser account stayed behind with their rent unrecovered, and any tokens deposited directly into the vault stayed locked.This adds a maker-callable
teardowninstruction. It is allowed once the duration has elapsed, requires every recorded contribution to be refunded first (current_amount == 0), sweeps the remaining vault balance to the maker, and closes the vault and the fundraiser account.The refund-first gate matters: without it, sweeping
vault.amountstraight to the maker would also capture tokens that still belong to contributors who have not refunded yet. Once every record is refunded, the only balance left in the vault is stray direct deposits, which the maker can safely collect.One note on the issue's parity ask: #708 was closed unmerged, so only the anchor implementation exists on main. When a pinocchio port of this example lands, the same instruction should be added there to keep the flavors in sync.
Fixes #725
Testing
Two cases added to
tests/litesvm.test.ts:UnrefundedContributionswhile a recorded contribution is still un-refunded (runs after the deadline warp, before the refund)I could not run
anchor testlocally (no SBF toolchain on this machine); the added LiteSVM tests run through the example'sanchor testin CI.AI disclosure
Check exactly one. See CONTRIBUTING.md.