Rust Fund

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

Missing Goal and Deadline validation in `withdraw()` allows creator to withdraw raised funds at any time, even when campaign failed

Description

The withdraw instruction enables campaign creators to withdraw the SOL raised by their campaign. According to the protocol specification:

However, the implementation of withdraw() does not enforce any requirements regarding fund.goal or fund.deadline. It immediately transfers all lamports equivalent to fund.amount_raised to the creator without validating if the campaign met its goal or if the campaign duration has ended.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
@> let amount = ctx.accounts.fund.amount_raised;
@> // @> Missing validation: require!(fund.amount_raised >= fund.goal);
@> // @> Missing validation: require!(Clock::get()?.unix_timestamp >= fund.deadline);
​
**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(())
}

Because there is neither a check for fund.amount_raised >= fund.goal nor a check for Clock::get()?.unix_timestamp >= fund.deadline, a creator can withdraw funds prematurely or when the campaign is an outright failure.

Risk

Likelihood: High (Any creator can call withdraw() at any moment after receiving contributions)

Impact: High (Contributors' funds are taken by the creator even though the campaign failed to meet its goal, violating the "All-or-Nothing" crowdfunding model)

Proof of Concept

Recommended Mitigation

Require that the deadline has passed, the funding goal has been achieved, and reset amount_raised upon successful withdrawal:

+#[error_code]
+pub enum ErrorCode {
+ ...
+ #[msg("Goal not reached")]
+ GoalNotReached,
+ #[msg("Campaign is still active")]
+ CampaignActive,
+}
​
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &mut ctx.accounts.fund;
+
+ let current_time: u64 = Clock::get()?.unix_timestamp.try_into()
+ .map_err(|_| ErrorCode::CalculationOverflow)?;
+
+ require!(fund.dealine_set && fund.deadline != 0, ErrorCode::DeadlineNotReached);
+ require!(current_time >= fund.deadline, ErrorCode::CampaignActive);
+ require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotReached);
​
let amount = fund.amount_raised;
+ require!(amount > 0, ProgramError::InsufficientFunds);
+ fund.amount_raised = 0;
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!