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.
goal = 100 SOL
deadline = future_timestamp
contribute(10 SOL)
contribute(15 SOL)
fund.amount_raised == 25 SOL
withdraw()
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(())
}