Rust Fund

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

withdraw has zero business conditions — creator can rug-pull all contributions at any time

Root + Impact

Description

  • Normal behavior: Per the README, creators can withdraw funds once their campaign succeeds, i.e. only after the deadline has passed AND the goal is reached.

  • Specific issue: `withdraw` only checks `has_one = creator` (authorization is correct) but verifies no business condition: no `deadline` check, no `amount_raised >= goal` check. The creator can call `withdraw` immediately after the first contribution, draining the entire PDA.

Risk

Likelihood:

  • Reason 1: The creator calls withdraw right after any contribute, with no time or goal gate.

  • Reason 2: No on-chain state prevents it; the function transfers `fund.amount_raised` unconditionally.

Impact:

  • Impact 1: Complete loss of all contributor funds the moment the creator chooses to withdraw.

  • Impact 2: Contradicts the documented campaign-success withdrawal rule; native rug-pull.

Proof of Concept

This test proves the creator can drain all funds before the campaign succeeds. We create a fund, contribute 0.5 SOL as a contributor, then call withdraw immediately as the creator. The fund PDA balance drops to 0 even though the deadline is not reached and the goal is not met.


it("H-3: creator withdraws before deadline/goal", async () => {
await program.methods.fundCreate(fundName, description, goal).accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: anchor.web3.SystemProgram.programId }).rpc();
await program.methods.contribute(contribution).accounts({ fund: fundPDA, contributor: contributor.publicKey, contribution: contributionPDA, systemProgram: anchor.web3.SystemProgram.programId }).signers([contributor]).rpc();
await program.methods.withdraw().accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: anchor.web3.SystemProgram.programId }).rpc();
const bal = await provider.connection.getBalance(fundPDA);
expect(bal).to.equal(0);
});

Recommended Mitigation

withdraw must require the deadline to be set and reached, and the goal to be met, before transferring funds to the creator.


pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &ctx.accounts.fund;
+ require!(fund.dealine_set, ErrorCode::DeadlineNotSet);
+ require!(
+ Clock::get()?.unix_timestamp as u64 >= fund.deadline,
+ ErrorCode::DeadlineNotReached
+ );
+ require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotReached);
let amount = fund.amount_raised;
// ... transfer ...
}
Updates

Lead Judging Commences

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