Rust Fund

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

Creator Can Withdraw Funds Without Meeting Goal or Deadline

RustFund is a decentralized crowdfunding protocol. The README states that creators can withdraw funds only once their campaign succeeds (deadline reached AND goal met). However, the withdraw() function performs no such check. A creator can drain the fund at any moment, regardless of whether the campaign deadline has passed or the goal has been reached. Contributors who deposited SOL in good faith have no protection.

The root cause is the absence of any success-condition validation in lines 90-105. The function transfers the full amount_raised after only verifying that msg.sender is the fund creator (has_one = creator). No timestamp comparison against fund.deadline and no amount comparison against fund.goal is performed.

// Line 90-105: No deadline or goal check
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised; // @> reads full amount
**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: Always. As soon as any contributor deposits SOL, amount_raised becomes positive and withdraw becomes profitable for the creator. No external condition, rare state, or multi-step chain is required.

Impact: Direct theft of all contributed funds. Every contributor who deposits SOL can have their entire payment taken by the creator before the campaign concludes, regardless of outcome.

Proof of Concept

The test deploys RustFund on a local Solana validator, creates a campaign with a 1 SOL goal, contributes only 0.5 SOL (goal not met, deadline not set), then calls withdraw. The creator balance increases by the full 0.5 SOL deposited amount, proving no success condition exists.

// 1. createFund(name="poc1", goal=1 SOL)
// 2. contribute(0.5 SOL) // goal NOT met
// 3. withdraw() // succeeds without deadline/goal check

On-chain evidence (Solana localnet):

  • Creator balance: 9,962,404,040 -> 10,462,399,040 lamports (+0.5 SOL)

  • Fund was never required to reach its 1 SOL goal

  • No deadline was ever set

Recommended Mitigation

Add three require statements at the top of withdraw():

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
+ // 1. Creator must have set a deadline
+ require!(ctx.accounts.fund.deadline != 0, ErrorCode::DeadlineNotSet);
+ // 2. Deadline must have passed
+ require!(ctx.accounts.fund.deadline <= Clock::get()?.unix_timestamp as u64, ErrorCode::DeadlineNotReached);
+ // 3. Campaign goal must have been met
+ require!(ctx.accounts.fund.amount_raised >= ctx.accounts.fund.goal, ErrorCode::GoalNotMet);
let amount = ctx.accounts.fund.amount_raised;
// ... transfer logic unchanged
}

Note that a new DeadlineNotSet error variant must be added to the ErrorCode enum. This three-part guard enforces the campaign lifecycle on-chain: create -> contribute -> deadline pass -> goal check -> withdraw, matching the documented behavior.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day 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!