Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Severity: high
Valid

withdraw has no goal-met or deadline check — creator can drain funds at any time

Root + Impact

The withdraw function transfers the entire amount_raised to the creator
without verifying that the fundraising goal has been met or that the deadline
has passed. A creator can withdraw any contributed SOL immediately after the
first contribution arrives, breaking the core crowdfunding trust guarantee.

Description

The expected behavior is that a creator can only withdraw after campaign
success — meaning amount_raised >= goal AND the deadline has passed. These
are the two conditions that distinguish a successful campaign from a failed one.
Neither condition is checked in the current implementation:

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
// @> no require!(fund.amount_raised >= fund.goal)
// @> no require!(current_time >= fund.deadline)
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(())
}

The only constraint on FundWithdraw is has_one = creator, which ensures
the signer is the campaign creator — but places no restriction on when or
under what conditions they may withdraw.

Risk

Likelihood:

  • A creator launches a campaign, waits for contributions to arrive, then
    immediately calls withdraw before the deadline or goal is reached

  • Since there are zero state-based guards, this is exploitable on every
    campaign by any creator at any point after the first contribution

Impact:

  • Creators can exit-scam contributors by withdrawing partial or full funds
    before the campaign period ends or the goal is met

  • Contributors have no recourse — the SOL is gone, and their refund path
    is also broken (H-1), leaving funds unrecoverable through any mechanism

Proof of Concept

Since withdraw has no conditional checks beyond creator identity, it can
be called the moment fund.amount_raised > 0. The sequence below shows a
complete exit-scam path requiring only standard instructions:

// 1. Creator launches campaign: goal = 100 SOL, deadline = now + 30 days
fund_create("legit project", "real description", 100_000_000_000)
// 2. Contributors deposit 10 SOL (10% of goal, campaign still active)
contribute(10_000_000_000) // from multiple users
// 3. Creator immediately withdraws — no error thrown
withdraw()
// amount = fund.amount_raised = 10_000_000_000
// Creator receives 10 SOL
// fund.amount_raised still = 10 SOL (not reset — separate issue)
// Contributors have no refund path

Recommended Mitigation

Two conditions must be enforced before any withdrawal is permitted: the goal
must be met, and the deadline must have passed. Both checks should be added
at the top of the function before any lamport manipulation occurs.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
+ let fund = &ctx.accounts.fund;
+ require!(
+ fund.amount_raised >= fund.goal,
+ ErrorCode::GoalNotMet
+ );
+ require!(
+ fund.deadline != 0 &&
+ fund.deadline <= Clock::get()?.unix_timestamp as u64,
+ ErrorCode::DeadlineNotReached
+ );
let amount = ctx.accounts.fund.amount_raised;

Also add the new error variant:

+ #[msg("Campaign goal has not been met")]
+ GoalNotMet,
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 9 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-01] No check for if campaign reached deadline before withdraw

## Description A Malicious creator can withdraw funds before the campaign's deadline. ## Vulnerability Details There is no check in withdraw if the campaign ended before the creator can withdraw funds. ```Rust 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(()) } ``` ## Impact A Malicious creator can withdraw all the campaign funds before deadline which is against the intended logic of the program. ## Recommendations Add check for if campaign as reached deadline before a creator can withdraw ```Rust pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> { //add this if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() { return Err(ErrorCode::DeadlineNotReached.into()); } //stops here 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(()) } ``` ## POC keep everything in `./tests/rustfund.rs` up on to `Contribute to fund` test, then add the below: ```TypeScript it("Creator withdraws funds when deadline is not reached", async () => { const creatorBalanceBefore = await provider.connection.getBalance(creator.publicKey); const fund = await program.account.fund.fetch(fundPDA); await new Promise(resolve => setTimeout(resolve, 150)); //default 15000 console.log("goal", fund.goal.toNumber()); console.log("fundBalance", await provider.connection.getBalance(fundPDA)); console.log("creatorBalanceBefore", await provider.connection.getBalance(creator.publicKey)); await program.methods .withdraw() .accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .rpc(); const creatorBalanceAfter = await provider.connection.getBalance(creator.publicKey); console.log("creatorBalanceAfter", creatorBalanceAfter); console.log("fundBalanceAfter", await provider.connection.getBalance(fundPDA)); }); ``` this outputs: ```Python goal 1000000000 fundBalance 537590960 creatorBalanceBefore 499999999460946370 creatorBalanceAfter 499999999960941400 fundBalanceAfter 37590960 ✔ Creator withdraws funds when deadline is not reached (398ms) ``` We can notice that the creator withdraws funds from the campaign before the deadline.

Support

FAQs

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

Give us feedback!