In a properly functioning crowdfunding smart contract, the withdraw() function must enforce that creators can only extract funds after the campaign successfully meets its funding goal [file:1]. The function should validate multiple success conditions before allowing withdrawal: (1) the fund.amount_raised must be greater than or equal to fund.goal, (2) the campaign deadline must have been reached, and (3) the campaign must be in a success state where the goal was met by the deadline. This ensures contributors' funds are only used for campaigns that achieve their stated objectives, providing the core accountability mechanism that distinguishes crowdfunding from donations. If a campaign fails to meet its goal, contributors should be able to reclaim their funds through the refund mechanism, not have them extracted by the creator.
The withdraw() function contains zero validation of campaign success conditions, lacking any checks that the funding goal was met before allowing the creator to extract all raised funds [file:1]. Lines 90-105 of lib.rs show the function directly transfers fund.amount_raised to the creator with only basic authorization (checking the creator is the signer) and arithmetic safety (overflow protection), but completely missing business logic validation. A creator can call withdraw() immediately after a single contributor sends 0.01 SOL to a campaign with a 100 SOL goal, draining the tiny contribution despite achieving only 0.01% of the stated objective. This transforms the contract from a conditional crowdfunding platform into an unconditional donation system where creators can steal any amount at any time, enabling systematic rugpull attacks where malicious actors create campaigns with impossible goals, collect small contributions from multiple victims, and immediately extract funds without delivering on any promises
Likelihood:
The missing goal validation in withdraw() creates an unconditional theft vector accessible to every campaign creator from the moment any contribution is received [file:1]. The function sits in the critical path for fund extraction and will be discovered immediately by any creator who tests their campaign's functionality or reads the contract code. Unlike CRITICAL-01 (passive bug affecting refunds) or CRITICAL-02 (requires manipulation timing), this vulnerability actively enables theft through a single function call with zero prerequisites beyond having created a campaign and received at least one contribution. The bug manifests deterministically on every withdrawal attempt regardless of campaign state, funding level, or timing.
Malicious actors will inevitably exploit this vulnerability through systematic rugpull operations where creating campaigns with impossible goals and immediately withdrawing any contributions becomes a profitable attack pattern [code_file:12]. The attack requires no technical sophistication (single transaction), has no on-chain penalties (appears as normal withdrawal), creates no detection signatures (looks like successful campaign), and can be repeated indefinitely with different campaign names. Rational economic actors maximizing profit will discover that setting goal = 1000 SOL and withdrawing after raising 1 SOL generates risk-free returns. The absence of any goal validation means every campaign becomes a direct donation box where creators can extract funds at will, making exploitation not just likely but economically guaranteed.
Impact:
Contributors suffer immediate and total loss of funds through direct theft rather than system malfunction [code_file:12]. A creator sets a campaign goal of 100 SOL (appearing ambitious but achievable), receives 5 SOL in contributions from 10 different contributors (5% of goal), then immediately calls withdraw() and steals all 5 SOL without meeting the stated objective. Unlike CRITICAL-01 where funds are trapped due to a bug, or CRITICAL-02 where deadline manipulation creates delays, CRITICAL-03 represents intentional theft enabled by missing validation. The creator can repeat this attack pattern across unlimited campaigns—each taking less than 60 seconds to execute—systematically defrauding users with no recovery mechanism.
The vulnerability completely destroys the fundamental purpose of a crowdfunding protocol by removing the conditional nature of funding [file:1]. Crowdfunding differs from donations specifically because funds are contingent on goal achievement—contributors accept risk that campaigns may fail, but expect funds returned if objectives aren't met. By allowing withdrawals regardless of goal status, the contract becomes a misleading donation system where "goals" are meaningless numbers with no enforcement. This false advertising creates severe legal liability as users are deceived into thinking their contributions are protected by goal-based refund logic when in reality creators have unconditional access. The protocol cannot honestly call itself "crowdfunding" with this vulnerability present.
CRITICAL-03 enables professional rugpull operations at scale where attackers can systematically defraud users across multiple campaigns [code_file:12]. Attack pattern: (1) Create campaign with 100 SOL goal and appealing description, (2) Advertise on social media / crypto forums to attract contributors, (3) Receive 2-5 SOL from victims who believe it's a legitimate crowdfunding campaign, (4) Immediately withdraw all funds with a single transaction, (5) Abandon campaign and create new one under different name, (6) Repeat process indefinitely. Each campaign yields small but risk-free profit, and at scale (10-20 campaigns per day), generates significant illicit income. The vulnerability provides plausible deniability ("I withdrew after a single contribution, didn't know I needed to wait for goal") while actually being intentional theft.
POC RESULT:
Add goal and Success validation
# 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.