Rust Fund

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

No deadline check on withdraw — creator drains funds before campaign ends

Title: No deadline check on withdraw — creator drains funds before campaign ends
Impact: High. Creator can withdraw all raised funds at any time, even before the deadline.
Likelihood: High. No deadline condition exists — withdraw works from the moment lamports arrive.
Reference Files: programs/rustfund/src/lib.rs:90-105

Description

The withdraw() instruction transfers the fund's entire amount_raised to the creator with only an access control check (has_one = creator). There is no validation that the campaign's deadline has passed before allowing withdrawal. A creator can call withdraw() immediately after contributions arrive, before the deadline is reached — breaking the fundamental crowdfunding promise that funds are locked until the campaign ends.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// NO: require deadline passed (deadline != 0 && deadline < now)
**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 contribute() function correctly checks fund.deadline != 0 && fund.deadline < now to block contributions after the deadline — but withdraw() has no reciprocal check, allowing premature withdrawal before the deadline.

Risk

Impact: High. A creator sets a 30-day deadline, receives 50 SOL in the first week, and immediately calls withdraw() — walking away with the funds while contributors expected them locked until the deadline. The deadline becomes meaningless for creator withdrawals.
Likelihood: High. The creator needs only to call withdraw() with no timing restriction. The contribute() deadline check proves the protocol intended to enforce time-based constraints, but withdraw() was left unprotected.
With a 90-day campaign and 100 SOL raised in the first 3 days, the creator drains all 100 SOL immediately, circumventing the advertised campaign lock period.

Proof of Concept

it("Creator withdraws before deadline is reached", async () => {
await program.methods.setDeadline(new anchor.BN(Date.now()/1000 + 86400))
.accounts({ fund: fundPDA, creator: creator.publicKey }).rpc();
await program.methods.contribute(new anchor.BN(500_000_000))
.accounts({ fund: fundPDA, contributor: creator.publicKey, contribution: contribPDA, systemProgram: SystemProgram.programId }).rpc();
const fund = await program.account.fund.fetch(fundPDA);
assert(fund.deadline.gt(new anchor.BN(Date.now()/1000))); // deadline IN FUTURE
const balBefore = await connection.getBalance(creator.publicKey);
await program.methods.withdraw()
.accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: SystemProgram.programId }).rpc();
assert((await connection.getBalance(creator.publicKey)) > balBefore); // drained BEFORE deadline!
});

The PoC proves the creator withdraws funds while the deadline is still in the future.

Recommended Mitigation

require!(fund.deadline != 0 && fund.deadline < Clock::get()?.unix_timestamp as u64, ErrorCode::DeadlineNotReached);

Add a deadline check mirroring contribute(), ensuring withdrawal is only possible after the campaign deadline has passed.

Updates

Lead Judging Commences

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