Root:
Impact:
Contributors may not have enough time to contribute or request refunds.
Campaign rules and trust guarantees are violated.
Funds can be misappropriated before the fundraising period ends.
Normal behavior:
A campaign creator should only be able to withdraw funds after the campaign deadline has passed. This ensures that all contributors have the full campaign duration to participate, and that refunds can be claimed if the campaign fails.
Issue:
In the current withdraw function, there is no check on the campaign deadline. As a result, a creator can call withdraw immediately after receiving contributions, even if the campaign is still active. This allows creators to prematurely access funds while contributors may not have had a chance to participate or request refunds.
Likelihood: HIGH
Reason 1 – Every time a campaign creator calls the withdraw instruction, the program allows fund transfer without verifying the campaign deadline.
Reason 2 – This occurs regardless of campaign progress, contributions received, or the remaining time until the deadline, enabling repeated premature withdrawals.
Impact:HIGH
Impact 1: Contributors may lose funds because creators can withdraw before the campaign ends, preventing refunds from being claimed.
Impact 2: The platform’s trust and integrity are compromised, as campaign rules and timelines are not enforced.
Campaign Setup:
The campaign is created with a deadline 1 day in the future and a small amount raised (2 SOL out of 10 SOL goal).
Withdraw Call:
The creator calls withdraw immediately after receiving contributions.
Bug Triggered:
There is no check for fund.deadline in the withdraw function.
The function transfers all raised funds to the creator even though the campaign is still active.
Result:
Contributors cannot participate further or request refunds.
Funds are prematurely misappropriated, breaking campaign rules and trust
Code Fix Example:
require!(fund.deadline != 0 && now >= fund.deadline, ErrorCode::DeadlineNotReached);
Ensures that creators cannot withdraw before the campaign ends.
fund.amount_raised = 0;
Prevents multiple withdrawals, maintaining fund integrity.
Safe arithmetic ensures no overflows or underflows during transfers.
## 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.