RustFund

First Flight #36
Beginner FriendlyRust
100 EXP
View results
Submission Details
Severity: high
Valid

Creator Can Withdraw Funds Without Goal Check in `withdraw`

https://github.com/CodeHawks-Contests/2025-03-rustfund/blob/main/programs/rustfund/src/lib.rs#L90-L105

Summary

The "rustfund" contains a critical vulnerability in the FundWithdraw function, allowing the creator to withdraw all raised funds (fund.amount_raised) at any time without restrictions. There are no checks to ensure the crowdfunding goal (fund.goal) has been met or that the deadline (fund.deadline) has passed before withdrawal is permitted. This unrestricted access enables the creator to drain the fund account prematurely, potentially scamming contributors by withdrawing funds before the campaign concludes or succeeds. This behavior deviates from standard crowdfunding practices, where withdrawals are typically conditional on meeting predefined criteria, undermining the contract’s integrity and contributor trust.


Vulnerability Details

The vulnerability is located in the FundWithdraw function, which facilitates the transfer of all accumulated funds from the fund account to the creator’s account. The function lacks any conditional logic to verify that the crowdfunding campaign has met its goal or reached its deadline, allowing immediate and unconditional withdrawal.

Root Cause

The root cause is the absence of validation checks in FundWithdraw to enforce crowdfunding rules. The function simply transfers the entire fund.amount_raised to the creator without evaluating fund.goal or fund.deadline. Below is the relevant code snippet:

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(())
}

In this snippet:

  • amount is set to fund.amount_raised, representing all funds contributed to the campaign.

  • The SOL transfer occurs directly from the fund account to the creator’s account using low-level lamports manipulation.

  • There are no conditions checking fund.amount_raised >= fund.goal or fund.deadline <= current_time, which are standard in crowdfunding contracts to ensure fairness.

Context in Contract Design

The Fund struct defines goal and deadline fields, suggesting an intent to enforce a crowdfunding model where funds are only accessible to the creator if the campaign succeeds:

#[account]
#[derive(InitSpace)]
pub struct Fund {
#[max_len(200)]
pub name: String,
#[max_len(5000)]
pub description: String,
pub goal: u64, // Intended target amount
pub deadline: u64, // Intended end time
pub creator: Pubkey,
pub amount_raised: u64,
pub dealine_set: bool,
}

However, FundWithdraw ignores these fields, allowing the creator to bypass the intended logic and withdraw funds at any point, even immediately after contributions begin.

Impact

This vulnerability has a profound impact on the contract’s security and reliability:

  • Potential for Scams: The creator can withdraw all funds at any time, even if the goal is far from being met or the deadline hasn’t passed. For example, if a fund has a goal of 100 SOL and only 10 SOL is raised, the creator can still take the 10 SOL, leaving contributors with no recourse until the deadline (and even then, refunds fail due to the first vulnerability).

  • Financial Loss for Contributors: Contributors cannot recover their funds after an early withdrawal, as the fund account would be drained, leaving insufficient lamports for refunds.

Tools Used

  • Manual Code Review

Recommendations

To mitigate this vulnerability, the following steps are recommended:

  1. Add Goal and Deadline Checks in FundWithdraw:
    Modify the FundWithdraw function to restrict withdrawals to cases where the crowdfunding goal is met and, optionally, the deadline has passed. Here’s the corrected code snippet:

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &mut ctx.accounts.fund;
let current_time = Clock::get()?.unix_timestamp.try_into().unwrap();
// Check if goal is met
+ if fund.amount_raised < fund.goal {
+ return Err(ErrorCode::GoalNotReached.into());
+ }
// Optional: Check if deadline has passed (if deadline enforcement is desired)
+ if fund.deadline != 0 && fund.deadline > current_time {
+ return Err(ErrorCode::DeadlineNotReached.into());
+ }
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(())
}
  • fund.amount_raised < fund.goal ensures the campaign has succeeded before funds can be withdrawn.

  • The optional deadline check (fund.deadline > current_time) prevents early withdrawals, aligning with a time-bound campaign model. If the intent is to allow withdrawals anytime after the goal is met, this check can be omitted.

Updates

Appeal created

bube Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

No goal achievement check in `withdraw` function

Support

FAQs

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