The withdraw() function allows the creator to claim all raised funds without verifying that the fundraising goal was
actually met. While the contract defines a goal field and enforces it during campaign creation, no check is performed at
withdrawal time. This allows creators to steal funds from failed campaigns where the goal was not reached, violating the
core contract promise that funds are only released on successful campaigns.
Likelihood:
Creators have immediate economic incentive to call withdraw() as soon as contributions arrive, and the function succeeds
on the first call regardless of whether the goal is met — there is no technical barrier or time delay preventing this
exploitation
The vulnerability activates the moment any contribution is received, since the fund account has positive lamports and
the function transfers all of them without checking the goal value
Impact:
All contributor funds in campaigns that fail to meet their goal are stolen by the creator — if 300 SOL is raised toward
a 1000 SOL goal, the creator successfully withdraws all 300 SOL that should be refundable
Contributors cannot recover their funds through the refund mechanism because the fund account has been emptied, causing
refund() calls to fail with InsufficientFunds errors
The entire crowdfunding contract promise is violated — funds are supposed to be locked until the goal is met, but
instead they can be claimed immediately by the creator
This test creates a campaign with a 1000 SOL goal but only receives 300 SOL in contributions (goal not met). The creator
then immediately calls withdraw(), which succeeds and transfers all 300 SOL to the creator's wallet. This proves the
vulnerability: the contract allows withdrawal without checking if the goal was actually achieved.
What the fix does:
The require! macro validates that the total amount raised (amount) is greater than or equal to the campaign goal
(fund.goal). If this condition is false, the function immediately reverts with an error, preventing any lamport transfers.
This creates an atomic gate that only permits withdrawal on successful campaigns.
# H-01. Creators Can Withdraw Funds Without Meeting Campaign Goals **Severity:** High\ **Category:** Fund Management / Economic Security Violation ## Description The `withdraw` function in the RustFund contract allows creators to prematurely withdraw funds without verifying if the campaign goal was successfully met. ## Vulnerability Details In the current RustFund implementation (`lib.rs`), the `withdraw` instruction lacks logic to verify that the campaign's `amount_raised` is equal to or greater than the `goal`. Consequently, creators can freely withdraw user-contributed funds even when fundraising objectives haven't been met, undermining the core economic guarantees of the platform. **Vulnerable Component:** - File: `lib.rs` - Function: `withdraw` - Struct: `Fund` ## Impact - Creators can prematurely drain user-contributed funds. - Contributors permanently lose the ability to receive refunds if the creator withdraws early. - Severely damages user trust and undermines the economic integrity of the RustFund platform. ## Proof of Concept (PoC) ```js // Create fund with 5 SOL goal await program.methods .fundCreate(FUND_NAME, "Test fund", new anchor.BN(5 * LAMPORTS_PER_SOL)) .accounts({ fund, creator: creator.publicKey, systemProgram: SystemProgram.programId, }) .signers([creator]) .rpc(); // Contribute only 2 SOL (below goal) await program.methods .contribute(new anchor.BN(2 * LAMPORTS_PER_SOL)) .accounts({ fund, contributor: contributor.publicKey, contribution, systemProgram: SystemProgram.programId, }) .signers([contributor]) .rpc(); // Set deadline to past await program.methods .setDeadline(new anchor.BN(Math.floor(Date.now() / 1000) - 86400)) .accounts({ fund, creator: creator.publicKey }) .signers([creator]) .rpc(); // Attempt withdrawal (should fail but succeeds) await program.methods .withdraw() .accounts({ fund, creator: creator.publicKey, systemProgram: SystemProgram.programId, }) .signers([creator]) .rpc(); /* OUTPUT: Fund goal: 5 SOL Contributed amount: 2 SOL Withdrawal succeeded despite not meeting goal Fund balance after withdrawal: 0.00089088 SOL (rent only) */ ``` ## Recommendations Add conditional logic to the `withdraw` function to ensure the campaign has reached its fundraising goal before allowing withdrawals: ```diff pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> { let fund = &mut ctx.accounts.fund; + require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotMet); let amount = 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(()) } ``` Also define the new error clearly: ```diff #[error_code] pub enum ErrorCode { // existing errors... + #[msg("Campaign goal not met")] + GoalNotMet, } ```
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.