Rust Fund

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

withdraw() lacks deadline and goal checks, allowing immediate fund drainage by campaign creator

Missing Access-Control Checks in withdraw() Allow Creator to Drain Funds Before Deadline or Goal Completion (Rug Pull)

ชื่อเรื่อง

withdraw() lacks deadline and goal checks, allowing immediate fund drainage by campaign creator

Impact: High

Likelihood: High

ขอบเขต

src/lib.rs — withdraw instruction handler and FundWithdraw accounts struct

คำอธิบาย

Normal behavior:

RustFund is a crowdfunding platform where contributors send SOL to a Fund account trusting that funds will only be released to the creator once the campaign has legitimately concluded (i.e., after the deadline has passed and/or the funding goal has been met). This is the core trust assumption that distinguishes a crowdfunding contract from a simple wallet.

The issue:

The withdraw instruction transfers the entire fund.amount_raised balance to the creator with no validation of fund.deadline and no validation of fund.goal:Root + Impact

คำอธิบาย

  • อธิบายพฤติกรรมปกติในหนึ่งประโยคหรือมากกว่า

    อธิบายปัญหาหรือข้อขัดข้องที่เกิดขึ้นในหนึ่งประโยคหรือมากกว่า


pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// @> no check that Clock::get() has passed fund.deadline
// @> no check that amount_raised >= fund.goal
**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(())
}

The FundWithdraw account struct only enforces has_one = creator (i.e., only the creator can call it) — it never touches deadline or goal at all:Risk

ความเป็นไปได้:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

ผลกระทบ:

  • Impact 1

  • Impact 2

Proof of Concept

#[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>,
#[account(mut)]
pub creator: Signer<'info>,
pub system_program: Program<'info, System>,
}

As a result, the creator can call withdraw() the instant a single lamport of contribution has landed in the fund — seconds after fund_create, with the deadline still far in the future and the goal nowhere near met.

Risk

Likelihood:

Occurs on every campaign where the creator calls withdraw() before the funding period ends, which requires no special conditions, no race, and no privileged state — it is reachable on the very first successful contribute() call.

Occurs regardless of whether set_deadline() was ever called, since deadline == 0 is treated the same as "no restriction" everywhere in withdraw().

Impact:

Total, unrecoverable loss of contributor funds: the creator can withdraw 100% of amount_raised at any time, including immediately after the first contribution, before the goal is reached and before the deadline passes.

Breaks the core trust/escrow guarantee that defines a crowdfunding contract — contributors have no on-chain assurance their funds will only move to the creator upon a successful, completed campaign.

Combined with the separate bug where contribute() never increments contribution.amount, contributors additionally have no way to reclaim funds via refund() after the creator drains the fund, making the loss permanent.

Proof of Concept

// 1. Creator calls fund_create("Trip", "desc", goal=1000 SOL)
// -> fund.deadline = 0, fund.amount_raised = 0
// 2. Attacker/contributor calls contribute(amount=1 SOL)
// -> fund.amount_raised = 1 SOL
// -> system_program::transfer moves 1 SOL from contributor into fund account
// 3. Creator immediately calls withdraw()
// -> ctx.accounts.fund.amount_raised == 1 SOL (0.1% of goal, deadline never set/reached)
// -> No check blocks this call
// -> 1 SOL lamports moved from fund account to creator
// -> Campaign has neither met its goal nor reached any deadline
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 6 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!