Rust Fund

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

`withdraw` never checks the funding goal (or deadline), letting the creator withdraw all contributions at any time and rug a failed campaign

Description

Normal behavior: per the protocol design, a creator may withdraw raised funds only once the campaign succeeds (i.e. amount_raised >= goal), so that contributors to a failed campaign retain their right to a refund.

Specific issue: the goal field is stored in fund_create and then never read anywhere in the program. withdraw validates only that the caller is the creator (has_one = creator + Signer); it does not check that the goal was met, nor that the deadline passed. The creator can therefore call withdraw at any moment and drain the full amount_raised, even for an undersubscribed, still-open campaign.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised; // @> no goal check, no deadline check
**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:

  • The creator controls the call and there is no state gating it; it works at any time, for any campaign state.

Impact:

  • The creator drains contributor SOL regardless of whether the campaign succeeded, nullifying the refund guarantee that is the core promise of the platform (a direct rug pull / theft of contributor funds).

Proof of Concept

The test creates a campaign with a large goal, has a contributor add a small amount (goal clearly not met, deadline not set/passed), then the creator immediately calls withdraw and receives the contributed SOL. This demonstrates funds leave the escrow with no success condition satisfied.

await program.methods.fundCreate("camp", "d", new BN(100_000_000_000)) // goal = 100 SOL
.accounts({ fund, creator: creator.publicKey, systemProgram }).signers([creator]).rpc();
await program.methods.contribute(new BN(5_000_000_000)) // only 5 SOL, goal not met
.accounts({ fund, contributor: user.publicKey, contribution, systemProgram }).signers([user]).rpc();
const before = await connection.getBalance(creator.publicKey);
await program.methods.withdraw()
.accounts({ fund, creator: creator.publicKey, systemProgram }).signers([creator]).rpc();
const after = await connection.getBalance(creator.publicKey);
assert.ok(after - before > 4_000_000_000); // creator withdrew contributions on a failed, open campaign

Recommended Mitigation

Gate withdraw on the success condition and zero the accounting after a successful withdrawal.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
+ require!(amount >= ctx.accounts.fund.goal, ErrorCode::GoalNotReached);
...
+ ctx.accounts.fund.amount_raised = 0;
Ok(())
}
Updates

Lead Judging Commences

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