Rust Fund

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

Withdraw Allowed Without Checking Campaign Funding Goal

Root + Impact

Root Cause:
The withdraw function does not check whether the campaign has reached its funding goal before transferring SOL to the creator. There is no validation of fund.amount_raised against fund.goal, nor any check that the campaign deadline has passed.

Impact:
Creators can withdraw funds from underfunded campaigns, causing contributors to lose money. This breaks the core trust model of the crowdfunding platform and allows misappropriation of raised funds.

Description

Normal behavior: A campaign creator should only be able to withdraw funds after the campaign has reached its funding goal and, if applicable, after the campaign deadline has passed. This ensures that contributors are only charged for successful campaigns.

Issue: The withdraw instruction transfers the total fund.amount_raised to the creator without verifying that the campaign goal was met. As a result, creators can withdraw funds from campaigns that failed to reach their target, breaking the trust and integrity of the platform.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// @> Root cause: No check that fund.amount_raised >= fund.goal
// @> Root cause: No check that campaign deadline has passed
**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 time a campaign creator calls the withdraw instruction, the program transfers funds without validating whether the campaign reached its funding goal.

  • Reason 2 – This behavior occurs regardless of campaign contributions, size, or deadline, allowing creators to repeatedly withdraw underfunded campaign funds.

Impact:

  • Impact 1: Contributors permanently lose their funds because creators can withdraw from underfunded campaigns.

  • Impact 2: The platform’s trust and integrity are compromised, as the refund mechanism and funding guarantees fail.

Proof of Concept

Scenario:

A campaign is created with a funding goal of 10 SOL and a deadline in the future.

Contributors send 2 SOL in total to the campaign.

// Step 1: Fund the campaign partially
fund.amount_raised = 2_000_000_000; // 2 SOL
// Step 2: Creator calls withdraw
withdraw(ctx);
// Step 3: Funds are transferred without checking goal
// ctx.accounts.creator receives 2 SOL
// fund.amount_raised still > 0, but campaign goal (10 SOL) not met

Explanation:

The withdraw function reads fund.amount_raised and transfers it to the creator.

There is no check that fund.amount_raised >= fund.goal.

The campaign goal of 10 SOL is ignored, so the creator receives funds even though the campaign failed.

Contributors’ deposits are effectively stolen, demonstrating a critical vulnerability.

Result:

Contributors lose their funds permanently.

The crowdfunding platform’s trust and refund mechanism are broken.

Campaign accounting is inaccurate, allowing misrepresentation of success

Recommended Mitigation

Problem:
The withdraw instruction transfers funds to the creator without verifying that the campaign has reached its funding goal or that the deadline has passed. This allows creators to withdraw underfunded campaigns.

Solution:

Add a goal check to ensure that fund.amount_raised >= fund.goal.

Add a deadline check to ensure the campaign has ended before withdrawals.

Prevent double withdrawals by resetting fund.amount_raised or adding a withdrawn flag.

Code fix:

- 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)?;
+ let now = Clock::get()?.unix_timestamp as u64;
+ // Ensure campaign reached its goal
+ require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotReached);
+ // Ensure campaign deadline has passed
+ require!(fund.deadline != 0 && now >= fund.deadline, ErrorCode::DeadlineNotReached);
+
+ let amount = fund.amount_raised;
+ fund.amount_raised = 0; // Prevent double withdrawal
+
+ **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)?;

Explanation:

require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotReached);
Ensures funds are only withdrawable after the campaign succeeds.

require!(fund.deadline != 0 && now >= fund.deadline, ErrorCode::DeadlineNotReached);
Ensures withdrawals cannot occur before the campaign ends.

fund.amount_raised = 0;
Prevents the creator from calling withdraw multiple times.

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!