withdraw() allow a creator to take contributions before the deadline or goalwithdraw() transfers all recorded contributions to the fund creator without checking either the campaign deadline or whether the funding goal was reached. A malicious creator can therefore withdraw a victim's SOL immediately after it is contributed, despite advertising an unmet goal and a future deadline.
The README states that creators can withdraw funds once their campaign succeeds. Campaign success depends on the deadline and funding goal, but withdraw() reads amount_raised and transfers it without inspecting either field:
The has_one = creator and PDA constraints only prove that the caller created the fund. A fund creator is not entitled to contributions until the advertised campaign conditions are satisfied.
An attacker can exploit this as follows:
Create a fund with a 10 SOL goal.
Set its deadline one day in the future.
Wait for a victim to contribute 0.5 SOL.
Call withdraw() immediately. The call succeeds although the campaign is only 5% funded and has not reached its deadline.
Add the following test to the Anchor test suite:
The test passes, proving that the creator receives the contribution before either success condition is satisfied:
Any fund creator can take 100% of all contributed SOL before the campaign succeeds. This causes direct loss of contributor funds. The broken refund bookkeeping described in a separate finding also prevents the victim from recovering the SOL through refund().
Manual source review
Anchor 0.30.1
solana-test-validator 1.18.17
Bun and ts-mocha
Define campaign success explicitly and enforce it before transferring lamports. At minimum, require a nonzero elapsed deadline and amount_raised >= goal:
The program should also record a terminal campaign state or reset the accounting after withdrawal so that the same campaign cannot continue accepting contributions into stale state.
## Description A Malicious creator can withdraw funds before the campaign's deadline. ## Vulnerability Details There is no check in withdraw if the campaign ended before the creator can withdraw funds. ```Rust pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> { let amount = ctx.accounts.fund.amount_raised; **ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.fund.to_account_info().lamports() .checked_sub(amount) .ok_or(ProgramError::InsufficientFunds)?; **ctx.accounts.creator.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.creator.to_account_info().lamports() .checked_add(amount) .ok_or(ErrorCode::CalculationOverflow)?; Ok(()) } ``` ## Impact A Malicious creator can withdraw all the campaign funds before deadline which is against the intended logic of the program. ## Recommendations Add check for if campaign as reached deadline before a creator can withdraw ```Rust pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> { //add this if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() { return Err(ErrorCode::DeadlineNotReached.into()); } //stops here let amount = ctx.accounts.fund.amount_raised; **ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.fund.to_account_info().lamports() .checked_sub(amount) .ok_or(ProgramError::InsufficientFunds)?; **ctx.accounts.creator.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.creator.to_account_info().lamports() .checked_add(amount) .ok_or(ErrorCode::CalculationOverflow)?; Ok(()) } ``` ## POC keep everything in `./tests/rustfund.rs` up on to `Contribute to fund` test, then add the below: ```TypeScript it("Creator withdraws funds when deadline is not reached", async () => { const creatorBalanceBefore = await provider.connection.getBalance(creator.publicKey); const fund = await program.account.fund.fetch(fundPDA); await new Promise(resolve => setTimeout(resolve, 150)); //default 15000 console.log("goal", fund.goal.toNumber()); console.log("fundBalance", await provider.connection.getBalance(fundPDA)); console.log("creatorBalanceBefore", await provider.connection.getBalance(creator.publicKey)); await program.methods .withdraw() .accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .rpc(); const creatorBalanceAfter = await provider.connection.getBalance(creator.publicKey); console.log("creatorBalanceAfter", creatorBalanceAfter); console.log("fundBalanceAfter", await provider.connection.getBalance(fundPDA)); }); ``` this outputs: ```Python goal 1000000000 fundBalance 537590960 creatorBalanceBefore 499999999460946370 creatorBalanceAfter 499999999960941400 fundBalanceAfter 37590960 ✔ Creator withdraws funds when deadline is not reached (398ms) ``` We can notice that the creator withdraws funds from the campaign before the deadline.
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.