Rust Fund

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

withdraw() has no goal-met or deadline-passed check — creator can rug-pull at any time

Root + Impact

Description

The withdraw() instruction transfers fund.amount_raised lamports to the creator with zero preconditions. The creator can invoke this at any point — immediately after the first contribution, before the deadline, and regardless of whether the funding goal has been met.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
@> // BUG: No check — fund.amount_raised >= fund.goal
@> // BUG: No check — Clock::get()?.unix_timestamp >= fund.deadline as i64
**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:

  • Any campaign creator can call withdraw() the moment any contribution lands — no exploit complexity required beyond simply invoking the instruction

  • The creator is financially incentivised to withdraw early whenever the goal appears unachievable, leaving contributors with no recourse

  • Because amount_raised is not zeroed post-withdrawal, repeated calls are structurally possible (though the first call captures all contribution lamports)

Impact:

  • All SOL contributed to a campaign can be drained by the creator at any time, destroying the platform's trustless guarantee

  • Contributors lose all funds and have no on-chain refund path because the vault is empty

  • The platform's core value proposition — funds returned if goal not met — is completely negated

Proof of Concept

it('creator withdraws before deadline with goal unmet', async () => {
// Fund: goal 10 SOL, deadline 24h from now
// Contributor deposits 1 SOL
await program.methods.contribute(new BN(1_000_000_000))
.accounts({ fund: fundPda, contributor: contributor.publicKey,
contribution: contribPda, systemProgram })
.signers([contributor]).rpc();
// Creator withdraws immediately — deadline not passed, goal not met
const creatorBalBefore = await provider.connection.getBalance(creator.publicKey);
await program.methods.withdraw()
.accounts({ fund: fundPda, creator: creator.publicKey, systemProgram })
.signers([creator]).rpc();
const creatorBalAfter = await provider.connection.getBalance(creator.publicKey);
@>assert.isTrue(creatorBalAfter > creatorBalBefore, 'Creator received funds early');
// Contributor cannot refund — vault is empty
await assert.rejects(
program.methods.refund().accounts({...}).signers([contributor]).rpc(),
/InsufficientFunds/
);
});

Recommended Mitigation

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
+ let clock = Clock::get()?;
+ let fund = &ctx.accounts.fund;
+ require!(
+ clock.unix_timestamp >= fund.deadline as i64,
+ ErrorCode::DeadlineNotReached
+ );
+ require!(
+ fund.amount_raised >= fund.goal,
+ ErrorCode::GoalNotMet
+ );
let amount = ctx.accounts.fund.amount_raised;
+ ctx.accounts.fund.amount_raised = 0;
**ctx.accounts.fund.to_account_info()
.try_borrow_mut_lamports()? = ...
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!