Rust Fund

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

withdraw() checks neither the funding goal nor the deadline, so a creator can drain all contributions at any time, even from a failed or still-running campaign

Root + Impact

The documented rules say creators "can withdraw funds once their campaign succeeds" and contributors can be refunded if the goal is not met by the deadline. withdraw implements none of that: it transfers the full amount_raised to the creator with no check that amount_raised >= goal, no check that any deadline exists or has passed, and no campaign state of any kind. The moment the first contribution lands, the creator can take all of it and disappear, while the campaign still appears open for further contributions. Combined with refunds paying out zero (the contribution record is never credited), contributors have no protection at either end: the creator can exit with everything at any time, and users can never claim anything back.

Description

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
// @> let amount = ctx.accounts.fund.amount_raised;
// @> // no check: fund.amount_raised >= fund.goal
// @> // no check: deadline set / reached
// @> // no check: funds not already owed back to contributors
**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: High - the only signer needed is the creator, the campaign's own operator, at any block height after any contribution; nothing in the code path resists it, and there is no timelock or state transition to wait for.
Impact: High - total loss of all contributed funds for every contributor of any campaign whose creator acts dishonestly (or whose key is compromised), including campaigns that are far below goal or still within their fundraising window. This is a complete bypass of the platform's trustless crowdfunding premise.

Proof of Concept

Verified with solana-program-test (test passes). A creator opens a fund with a 10 SOL goal. A contributor adds 5 SOL (goal not met, no deadline was ever set). The creator immediately calls withdraw and receives the full 5 SOL; the instruction succeeds.

assert_eq!(f.amount_raised, 5 * SOL); // 50% of the 10 SOL goal
send(withdraw_ix, &[&creator]).await.unwrap(); // succeeds immediately
assert_eq!(creator_after - creator_before, 5 * SOL);

Recommended Mitigation

Gate withdraw on a successful, finished campaign and settle the state: require a deadline to be set and reached and amount_raised >= goal, zero amount_raised (or mark the fund withdrawn) before transferring, so the funds can only leave once, and only on success.

Updates

Lead Judging Commences

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