On Solana, an account that a program creates for a user is normally closed when it is no longer needed. Its rent-exempt deposit then goes back to whoever paid it. Here that means the contributor's Contribution record after a refund, and the creator's Fund account once the campaign is settled.
Each contributor pays the rent for an 80-byte Contribution PDA (init_if_needed, payer = contributor, L125-L131). Each creator pays the rent for the 5,273-byte Fund PDA (init, payer = creator, L111). No account in the program has a close constraint, and there is no close instruction.
refund() only sets contribution.amount = 0 (L85).
withdraw() moves only amount_raised (L91) and leaves the rent behind.
Both accounts are owned by the program, so nothing else can debit them. The Contribution record does stay reusable: a later contribution to the same fund reuses it without charging rent again. Only the rent cannot be reclaimed.
Likelihood:
It occurs whenever a user backs or creates a campaign. The contributor pays the Contribution rent on their first contribution to a fund, and the creator pays the Fund rent in fund_create. Both deposits are left behind when the campaign ends, whether it ends in refunds or in a withdrawal.
A user has no instruction to call later to recover them. The PoC shows Anchor rejecting close, close_contribution, close_fund and close_account with InstructionFallbackNotFound (101).
Impact:
Each contributor permanently loses 1,447,680 lamports (about 0.00145 SOL) per fund they back. Each creator permanently loses 37,590,960 lamports (about 0.0376 SOL) per campaign. The amounts are small and are the users' own deposits, so the impact is Low.
refund() leaves a zeroed Contribution account on-chain instead of cleaning up. The Fund rent can come back only in one degenerate case: if amount_raised at the first withdrawal equals the Fund rent exactly, a second withdraw() re-subtracts the stale total and drains the account to 0. With one lamport more or less, it fails (boundary test).
Test 1 follows one contributor and one creator.
It checks the rent figures against the Rent sysvar: Contribution 80 bytes = 1,447,680 lamports; Fund 5,273 bytes = 37,590,960 lamports.
fund_create debits the creator exactly the Fund rent. contribute(1 SOL) debits Alice exactly 1 SOL plus the Contribution rent.
After refund(), the Contribution account still exists with its rent and amount = 0. Alice receives 0 because of the separate contribution.amount bug. The test asserts that the rent is not returned, whatever the recorded amount.
A second 0.5 SOL contribution costs Alice exactly 0.5 SOL, so the record is reused.
withdraw() pays the creator exactly amount_raised and leaves the Fund holding exactly its rent.
Every recovery attempt fails:
a second refund() moves nothing;
a second withdraw() fails with InsufficientFunds;
close, close_contribution, close_fund and close_account fail with InstructionFallbackNotFound (101).
Both deposits remain in the program-owned accounts.
Test 2 sets the limits of the Fund case:
With amount_raised equal to the Fund rent, a second withdraw() drains the Fund to 0 and returns the rent.
With one lamport less, it fails with InsufficientFundsForRent.
With one lamport more, it fails with InsufficientFunds.
Run from the repository root. Everything before the cd line is a one-time setup; save the file above as programs/rustfund/tests/poc_RF-08.rs before running the last line:
Result: test result: ok. 2 passed; 0 failed. Key log lines:
Close the Contribution record to the contributor when it is refunded, and close the Fund to the creator when it is paid out. Apply the second change together with the withdraw gating fix, so that a Fund is closed only after a successful campaign. A failed campaign's Fund also needs a creator-only close instruction that can run once all contributions have been refunded.
## Description neither the `withdraw()` nor `refund()` functions properly close their respective accounts after the funds have been transferred. This results in the rent amount (the SOL required to keep the account allocated on the Solana blockchain) remaining locked in these accounts indefinitely. ## Vulnerability Details In Solana, accounts must maintain a minimum balance to remain "rent-exempt", ensuring they aren't purged from the blockchain. When accounts are no longer needed, best practice is to close them and return this rent to the appropriate party (typically the account creator). The RustFund protocol currently lacks this account cleanup mechanism. - The withdrawal process in `withdraw()` transfers the raised funds from the fund account to the creator - Similarly, the refund process in `refund()` transfers the contribution amount back to the contributor and resets the contribution amount In both functions, the account data is updated, but the accounts themselves remain open, with their rent amount locked. After all funds have been withdrawn or refunded, these accounts serve no further purpose but continue to consume blockchain resources and lock up SOL. ### Proof of Concept 1. Alice creates a fund with a goal of 100 SOL, which requires 0.1 SOL as rent for the fund account. 2. The fund successfully raises 100 SOL from various contributors, each of whom also paid rent for their contribution accounts (approximately 0.05 SOL each). 3. Alice calls `withdraw()` to claim the 100 SOL raised funds, but the fund account itself is not closed. 4. The 0.1 SOL rent for the fund account remains locked in the account indefinitely. 5. Similarly, when contributors request refunds through the `refund()` function, their contribution accounts are updated but not closed. 6. The rent for all contribution accounts (e.g., 10 contributors × 0.05 SOL = 0.5 SOL) remains locked indefinitely. 7. Over time, as more funds are created and more contributions are made, the amount of permanently locked SOL grows. ## Impact - Economic Inefficiency, - Blockchain bloat, - Reduced user returns ## Recommendations Implement proper account closure in both the `withdraw()` and `refund()` functions.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.