Rust Fund

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

withdraw() Missing Deadline and Goal Checks Allows Creator to Drain Funds Mid-Campaign

Root + Impact

Description

  • withdraw() is designed to allow the creator to collect raised funds only after a campaign successfully reaches its goal by the deadline — mirroring the crowdfunding model described in the protocol documentation.

  • The function performs no checks on whether the deadline has passed or whether the funding goal has been met, allowing the creator to withdraw all contributed SOL at any time including seconds after the first contribution arrives.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &mut ctx.accounts.fund;
// @> No check: fund.deadline has passed
// @> No check: fund.amount_raised >= fund.goal
// @> Creator can call this at any point during or before campaign
let amount = fund.amount_raised;
**ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? -= amount;
**ctx.accounts.creator.to_account_info().try_borrow_mut_lamports()? += amount;
Ok(())
}

Risk

Likelihood: High

  • The function has zero guards — any creator can call it at any time with no preconditions to satisfy

  • A malicious creator deliberately exploits this on every campaign they create

Impact: High

  • Direct and total loss of contributor funds — creator drains all SOL the moment contributions arrive

  • Contributors cannot get refunds because fund.amount_raised is zeroed out after withdrawal, breaking the refund path as well

Proof of Concept

The test below shows a creator draining the fund immediately after a contribution, while the campaign is still active and the goal is not met. The key observation is that withdraw() succeeds without any error — the protocol places no restriction on when or under what conditions the creator can call it.

it("creator withdraws all funds before deadline and goal", async () => {
await program.methods.fundCreate(name, desc, new anchor.BN(10_000_000_000))
.accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: SystemProgram.programId }).rpc();
const farFuture = new anchor.BN(Math.floor(Date.now() / 1000) + 9999);
await program.methods.setDeadline(farFuture).accounts({...}).rpc();
// Contributor sends 4 SOL — campaign is still active
await program.methods.contribute(new anchor.BN(4_000_000_000))
.accounts({ fund: fundPDA, contributor: contributor.publicKey, contribution: contributionPDA, systemProgram: SystemProgram.programId })
.signers([contributor]).rpc();
const creatorBefore = await provider.connection.getBalance(creator.publicKey);
// Creator withdraws immediately — no deadline check, no goal check
await program.methods.withdraw()
.accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: SystemProgram.programId })
.rpc();
const creatorAfter = await provider.connection.getBalance(creator.publicKey);
// Creator balance increased — contributor 4 SOL stolen mid-campaign
assert.isAbove(creatorAfter, creatorBefore);
});

Recommended Mitigation

Two require! guards must be added at the top of withdraw() before any lamport transfers occur. The first ensures the campaign deadline has passed so contributors had a fair window. The second ensures the funding goal was actually reached so contributors only lose access to their SOL when the campaign they backed was successful.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &mut ctx.accounts.fund;
let clock = Clock::get()?;
+ require!(
+ clock.unix_timestamp as u64 >= fund.deadline,
+ ErrorCode::DeadlineNotReached
+ );
+ require!(
+ fund.amount_raised >= fund.goal,
+ ErrorCode::GoalNotMet
+ );
let amount = fund.amount_raised;
**ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? -= amount;
**ctx.accounts.creator.to_account_info().try_borrow_mut_lamports()? += amount;
Ok(())
}
Updates

Lead Judging Commences

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