Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Severity: high
Valid

Missing Goal Validation in Withdraw Allows Failed Campaign Extraction

Root + Impact

Root Cause: The withdraw function does not verify that the campaign successfully met its funding goal before allowing fund extraction.

Impact: Creators can withdraw all contributed funds even when the campaign failed to reach its goal, stealing funds that should be refundable to contributors.

Description

Normal behavior: In crowdfunding, creators should only receive funds if the campaign meets its goal. If the goal is not met, contributors should be able to reclaim their contributions through refunds.

Issue: The withdraw function transfers amount_raised to the creator without checking if amount_raised >= goal. Failed campaigns can still have funds extracted.
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// @> Root cause: No check that fund.amount_raised >= fund.goal
// @> Creator withdraws even if campaign failed to meet goal
**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(())
}

Risk

Likelihood:HIGH

  • Reason 1 :Every withdraw call succeeds regardless of campaign success or failure status

  • Reason 2 :No prerequisite prevents creators from extracting funds from failed campaigns

Impact:HIGH

  • Impact 1:Contributors lose funds to failed campaigns that should allow refunds

  • Impact 2:Crowdfunding goal mechanism becomes meaningless; creators always get paid regardless of performance

Proof of Concept

// Campaign with 100 SOL goal only raised 5 SOL - FAILED
fund.goal = 100_000_000_000; // 100 SOL goal
fund.amount_raised = 5_000_000_000; // Only 5 SOL raised (5%)
fund.deadline = Clock::get()?.unix_timestamp as u64 - 1000; // Past
// Creator withdraws from FAILED campaign
withdraw(ctx)?; // Succeeds! No goal check
// Creator steals 5 SOL that should be refundable to contributors
// Contributors cannot refund - funds already extracted

A campaign needed 100 SOL but only raised 5 SOL - a clear failure. According to crowdfunding rules, contributors should be able to refund. However, the creator calls withdraw and successfully extracts all 5 SOL because there's no goal validation. Contributors lose their funds to a failed campaign.

Recommended Mitigation

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
+ let fund = &mut ctx.accounts.fund;
+ let clock = Clock::get()?;
+
+ require!(
+ fund.deadline != 0 && clock.unix_timestamp as u64 >= fund.deadline,
+ ErrorCode::DeadlineNotReached
+ );
+ require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotMet);
- let amount = ctx.accounts.fund.amount_raised;
+ let amount = fund.amount_raised;
+ fund.amount_raised = 0;
// ... transfer logic
}

Add validation requiring amount_raised >= goal before withdrawal. This ensures only successful campaigns allow creator withdrawal. Failed campaigns keep funds available for contributor refunds. Also reset amount_raised = 0 after withdrawal to prevent accounting issues.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 9 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-02] H-01. Creators Can Withdraw Funds Without Meeting Campaign Goals

# 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, } ```

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!