Rust Fund

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

Withdraw lacks success criteria (goal/deadline)

Root + Impact

Description

  • Normal behavior: withdrawal should be gated by success criteria such as reaching the goal and passing the deadline.

  • Issue: withdraw transfers `amount_raised` to the creator with no goal or deadline checks, enabling early withdrawal

@> 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(())
@> }

Risk

Likelihood:

  • After any contribution, the creator can call `withdraw` at any time because no success criteria are checked.

  • Deadline defaults to 0 and does not gate withdrawals.

Impact:

  • Creator can drain funds before the goal or deadline conditions are met.

  • Contributors lose escrow guarantees.

Proof of Concept

#[tokio::test]
async fn f002_withdraw_before_success_criteria() {
let mut ctx = start_test().await;
let creator = Keypair::new();
let contributor = Keypair::new();
fund_keypair(&mut ctx, &creator, 5 * SOL).await.unwrap();
fund_keypair(&mut ctx, &contributor, 5 * SOL).await.unwrap();
let name = unique_name("f002");
let fund = create_fund(&mut ctx, &creator, &name, 2 * SOL).await;
contribute(&mut ctx, &fund, &contributor, 1 * SOL).await;
let fund_state = fetch_fund(&mut ctx, &fund).await;
assert!(fund_state.amount_raised < 2 * SOL);
assert_eq!(fund_state.deadline, 0);
let fund_balance_before = get_balance(&mut ctx, &fund).await;
withdraw(&mut ctx, &fund, &creator).await.unwrap();
let fund_balance_after = get_balance(&mut ctx, &fund).await;
assert_eq!(
fund_balance_before - fund_balance_after,
fund_state.amount_raised
);
}

Recommended Mitigation

diff --git a/programs/rustfund/src/lib.rs b/programs/rustfund/src/lib.rs
@@ pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
- let amount = ctx.accounts.fund.amount_raised;
+ let now: u64 = Clock::get().unwrap().unix_timestamp.try_into().unwrap();
+ if ctx.accounts.fund.deadline == 0 || ctx.accounts.fund.deadline > now {
+ return Err(ErrorCode::DeadlineNotReached.into());
+ }
+ if ctx.accounts.fund.amount_raised < ctx.accounts.fund.goal {
+ return Err(ErrorCode::GoalNotMet.into());
+ }
+ let amount = ctx.accounts.fund.amount_raised;
@@ pub enum ErrorCode {
+ #[msg("Goal not met")]
+ GoalNotMet,
Updates

Lead Judging Commences

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