Rust Fund

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

[H-02] `withdraw()` has no deadline or goal validation, letting the creator drain all contributed SOL at any time

Description

withdraw() reads fund.amount_raised and transfers that amount to the creator via direct lamport manipulation. The FundWithdraw struct validates the creator signer and PDA seeds, but the instruction body performs zero business logic checks. There is no deadline check, no goal check, and no state reset.

Vulnerability Details

// lib.rs:90-105
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// @> No check: fund.deadline != 0
// @> No check: Clock::get() >= fund.deadline
// @> No check: fund.amount_raised >= fund.goal
// @> No reset: fund.amount_raised = 0
**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(())
}

A crowdfunding platform's core invariant is that the creator can only access funds after the campaign succeeds (deadline passes AND goal met). This function enforces neither condition.

Risk

Likelihood:

  • Any fund creator can call withdraw() at any time with zero preconditions.

  • This is the intended flow for a malicious creator (rug-pull).

Impact:

  • Creator steals 100% of contributed SOL immediately after contributions arrive.

  • Combined with H-01 (refunds return 0), contributors have zero recourse.

  • Every campaign on the protocol is exploitable by its creator.

Proof of Concept

1. Attacker: fund_create("Save the Whales", "...", goal=1000 SOL)
2. Attacker: set_deadline(now + 30 days) -- builds trust
3. Victims: contribute(SOL) x many -- SOL flows into fund PDA
4. Attacker: withdraw() -- drains ALL SOL immediately
5. Victims: refund() -- gets 0 SOL back (H-01)
Result: 100% fund theft, zero contributor recourse.

Recommendations

Add deadline, goal, and state reset checks:

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
+ let fund = &mut ctx.accounts.fund;
+ require!(fund.deadline != 0, ErrorCode::DeadlineNotSet);
+ let now: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
+ require!(now >= fund.deadline, ErrorCode::DeadlineNotReached);
+ require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotReached);
+
- let amount = ctx.accounts.fund.amount_raised;
+ let amount = fund.amount_raised;
+ fund.amount_raised = 0;
// ... existing lamport arithmetic ...
}
Updates

Lead Judging Commences

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