Rust Fund

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

Withdraw Allowed Before Campaign Deadline

Root + Impact

Root:

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// @> Root cause: No check that campaign deadline has passed
**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:

Contributors may not have enough time to contribute or request refunds.

Campaign rules and trust guarantees are violated.

Funds can be misappropriated before the fundraising period ends.

Description

Normal behavior:
A campaign creator should only be able to withdraw funds after the campaign deadline has passed. This ensures that all contributors have the full campaign duration to participate, and that refunds can be claimed if the campaign fails.

Issue:
In the current withdraw function, there is no check on the campaign deadline. As a result, a creator can call withdraw immediately after receiving contributions, even if the campaign is still active. This allows creators to prematurely access funds while contributors may not have had a chance to participate or request refunds.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// @> Root cause: No check that the campaign deadline has passed
// @> Root cause: Funds can be withdrawn even if the campaign is still active
**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

Reason 1 – Every time a campaign creator calls the withdraw instruction, the program allows fund transfer without verifying the campaign deadline.

Reason 2 – This occurs regardless of campaign progress, contributions received, or the remaining time until the deadline, enabling repeated premature withdrawals.

Impact:HIGH

Impact 1: Contributors may lose funds because creators can withdraw before the campaign ends, preventing refunds from being claimed.

Impact 2: The platform’s trust and integrity are compromised, as campaign rules and timelines are not enforced.

Proof of Concept

Campaign Setup:
The campaign is created with a deadline 1 day in the future and a small amount raised (2 SOL out of 10 SOL goal).

Withdraw Call:
The creator calls withdraw immediately after receiving contributions.

Bug Triggered:

There is no check for fund.deadline in the withdraw function.

The function transfers all raised funds to the creator even though the campaign is still active.

Result:

Contributors cannot participate further or request refunds.

Funds are prematurely misappropriated, breaking campaign rules and trust

// Step 1: Create a campaign with a future deadline
let fund = Fund {
name: "Test Campaign".to_string(),
goal: 10_000_000_000, // 10 SOL
amount_raised: 2_000_000_000, // 2 SOL contributed
deadline: Clock::get()?.unix_timestamp as u64 + 86400, // 1 day in the future
creator: creator.key(),
dealine_set: true,
};
// Step 2: Creator calls withdraw immediately, before deadline
withdraw(ctx)?;
// Step 3: Funds are transferred to creator despite campaign still being active
println!("Creator balance: {}", ctx.accounts.creator.lamports());
println!("Fund remaining: {}", ctx.accounts.fund.lamports());

Recommended Mitigation

Code Fix Example:

- 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)?;
+ let now = Clock::get()?.unix_timestamp as u64;
+ // Ensure campaign deadline has passed
+ require!(fund.deadline != 0 && now >= fund.deadline, ErrorCode::DeadlineNotReached);
+
+ let amount = fund.amount_raised;
+ fund.amount_raised = 0; // Prevent double withdrawal
+
+ **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)?;

require!(fund.deadline != 0 && now >= fund.deadline, ErrorCode::DeadlineNotReached);
Ensures that creators cannot withdraw before the campaign ends.

fund.amount_raised = 0;
Prevents multiple withdrawals, maintaining fund integrity.

Safe arithmetic ensures no overflows or underflows during transfers.

Updates

Lead Judging Commences

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