Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Creator can withdraw funds at any time bypassing funding goal and deadline constraints

Root + Impact

Description

The crowdfunding flow stores a funding goal and optional deadline, implying that contributed funds should remain locked until the fundraising process reaches its intended completion conditions.

However, the withdraw() instruction allows the fund creator to withdraw all contributed funds immediately after any contribution is received. The function only verifies that the caller is the creator and does not enforce any deadline or funding-goal requirements before transferring funds.

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(())
}
#[derive(Accounts)]
pub struct FundWithdraw<'info> {
@> #[account(
@> mut,
@> seeds = [fund.name.as_bytes(), creator.key().as_ref()],
@> bump,
@> has_one = creator
@> )]
pub fund: Account<'info, Fund>,
}

Risk

Likelihood:

  • Creator has unrestricted access to withdraw() after any deposit

  • No state gating exists (no goal, no deadline, no finalized flag)

  • Function is publicly callable at any time

Impact:

  • Immediate theft of user deposits by fund creator

  • Breaks crowdfunding invariants (goal-based pooling becomes meaningless)

  • Users have no protection even if goal is not reached

  • Undermines entire protocol trust model

Proof of Concept

A fund creator can withdraw all contributed funds before the fundraising process has completed.

// Fund configuration
goal = 100 SOL
deadline = future_timestamp
// Contributors deposit funds
contribute(10 SOL)
contribute(15 SOL)
fund.amount_raised == 25 SOL
// Creator immediately withdraws
withdraw()
// Result
creator receives 25 SOL
fund balance is drained
goal was not reached
deadline was not reached

Recommended Mitigation

The withdraw() instruction should enforce the fundraising completion conditions before transferring funds to the creator.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
+ require!(
+ ctx.accounts.fund.amount_raised >= ctx.accounts.fund.goal,
+ ErrorCode::GoalNotReached
+ );
+
+ if ctx.accounts.fund.deadline != 0 {
+ require!(
+ Clock::get()?.unix_timestamp as u64 >= ctx.accounts.fund.deadline,
+ ErrorCode::DeadlineNotReached
+ );
+ }
+
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(())
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 12 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!