withdraw checks neither the funding goal nor the deadline. Impact: a creator drains every contribution at any moment, while contributors cannot refund because the campaign is still runningThe README states that creators withdraw "once their campaign succeeds", and the program stores a goal and a deadline on every fund for exactly that purpose. A withdrawal should therefore require that the goal has been reached and the campaign is over.
withdraw performs no such check. It reads amount_raised and moves that many lamports to the creator, with nothing gating when this may happen.
The goal field is written once at line 16 and never read anywhere else in the program, so it can never influence any decision. The deadline field is enforced elsewhere, in contribute at line 29 and refund at line 69, which shows the check was understood and simply omitted here. The only guard on the instruction is has_one = creator, which constrains who calls it, not when.
Likelihood:
The creator can do this in any block from the first contribution onward, with no precondition, no timing and no special tooling.
It needs only the one signature the creator already holds, so every campaign on the platform is exposed by default.
Impact:
Every lamport contributed is taken by the creator before the campaign concludes, which is the exact rug pull the goal and deadline fields exist to prevent.
Contributors have no recourse: refund rejects them with DeadlineNotReached while the campaign is still live, and by the time the deadline arrives the account is already empty.
The attacker is the campaign creator, who needs nothing beyond their own signature. The victims are contributors, whose SOL is taken before the campaign ends. Run with anchor test.
Output:
Two further tests close the obvious objections. With a deadline a full year in the future and the goal unmet, the same call drained the identical 1,999,995,008 lamports, because withdraw never reads fund.deadline. A contributor trying to rescue their SOL afterwards is rejected with DeadlineNotReached, the campaign being formally still live, so neither side has a recovery path.
I also confirmed the existing guard works: a stranger calling withdraw on someone else's fund is rejected by has_one = creator. This is about when a withdrawal is allowed, not who may call it.
Gate the withdrawal on the two conditions the program already stores, and reset the counter so the funds cannot be claimed twice:
Add a GoalNotReached variant to ErrorCode. A campaign that misses its goal should leave the lamports in the fund so contributors can refund instead.
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.