Skip to content

feat(token-fundraiser): add teardown for failed campaigns - #731

Open
devtechedge wants to merge 2 commits into
solana-foundation:mainfrom
devtechedge:teardown-failed-campaign
Open

feat(token-fundraiser): add teardown for failed campaigns#731
devtechedge wants to merge 2 commits into
solana-foundation:mainfrom
devtechedge:teardown-failed-campaign

Conversation

@devtechedge

Copy link
Copy Markdown

What and why

A failed campaign (one that expires under target) had no teardown path: refund returns 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 teardown instruction. 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.amount straight 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:

  • teardown is rejected with UnrefundedContributions while a recorded contribution is still un-refunded (runs after the deadline warp, before the refund)
  • after the refund drains the vault, a stray direct deposit into the vault is swept to the maker and both the vault and the fundraiser accounts are closed

I could not run anchor test locally (no SBF toolchain on this machine); the added LiteSVM tests run through the example's anchor test in CI.

AI disclosure

Check exactly one. See CONTRIBUTING.md.

  • No AI tooling was used beyond editor autocomplete.
  • AI tooling was used. Tool and extent: drafted the new instruction and tests with Claude; I reviewed the full diff against the existing checker/refund instructions and the design notes in the issue before opening.

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
@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; both previous findings are fully addressed and no new actionable issues were identified.

Findings

  1. P1 Invalid close account call
  2. P2 Expiry guard remains untested

Summary

  • Sweeps stray vault deposits to the maker.
  • Closes the vault and fundraiser accounts to recover rent.
  • Tests active-campaign rejection, outstanding-contribution rejection, token sweeping, and account closure.
  • The invalid close_account invocation identified previously is fixed.
  • The previously missing expiry-guard test is now present.

Reviews (2) · Last reviewed commit: "fundraiser: fix close_account CPI call a..."

authority: self.fundraiser.to_account_info(),
};

close_account(CpiContext::new_with_signer(cpi_program, close_accounts, &signer_seeds), None)?;

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 Invalid close account call

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.

Suggested change
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))?;

Comment on lines +256 to +276
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',
);
});

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.

P2 Expiry guard remains untested

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!

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.

token-fundraiser: no teardown for a failed campaign (vault + fundraiser rent stranded)

1 participant