Rust Fund

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

Creator can withdraw funds at any time, even if campaign fails

Root + Impact

Description

Normal Behavior

A crowdfunding campaign should only allow the creator to withdraw funds after:

  1. The campaign deadline has passed, and

  2. The fundraising goal has been successfully reached.

If these conditions are not met, contributors should remain eligible for refunds.

Issue

The withdraw instruction allows the campaign creator to withdraw all raised funds immediately, without validating whether:

  • the deadline has passed, or

  • the funding goal has been met.

There are no checks enforcing campaign success before funds are transferred to the creator.

// Root cause in the codebase with @> markers
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(())
}

Risk

Likelihood:

  • Reason 1: The withdraw instruction is callable at any time by the campaign creator.

  • Reason 2: There are no conditional checks guarding deadline completion or goal fulfillment.

Impact:

  • Impact 1: Creators can drain campaign funds before contributors are eligible for refunds.

  • Impact 2: Contributors permanently lose funds even if the campaign fails, breaking trust assumptions of the protocol.

Proof of Concept

1. A creator deploys a campaign with a funding goal of 100 SOL.
2. Contributors deposit 10 SOL total.
3. The deadline has not been reached and the goal is unmet.
4. The creator immediately calls `withdraw`.
5. All 10 SOL are transferred to the creator.
6. Contributors can no longer receive refunds.

Explanation:

Because the withdrawal logic does not enforce any success conditions, the creator can bypass the crowdfunding process entirely and extract funds before the campaign concludes.

Recommended Mitigation

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &mut ctx.accounts.fund;
+ // Ensure campaign deadline has passed
+ require!(
+ fund.deadline != 0 &&
+ fund.deadline <= Clock::get()?.unix_timestamp as u64,
+ ErrorCode::DeadlineNotReached
+ );
+ // Ensure funding goal has been met
+ require!(
+ fund.amount_raised >= fund.goal,
+ ErrorCode::GoalNotMet
+ );
let amount = fund.amount_raised;
**fund.to_account_info().try_borrow_mut_lamports()? =
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(())
}

Explanation:

These checks enforce the intended crowdfunding lifecycle by allowing withdrawals only after the campaign has concluded successfully, preserving contributor safety and protocol integrity.

Updates

Lead Judging Commences

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