Rust Fund

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

`rustfund::withdraw` performs no goal or deadline check, letting a creator drain all contributions from a campaign that never succeeded (rug pull)

rustfund::withdraw performs no goal or deadline check, letting a creator drain all contributions from a campaign that never succeeded (rug pull)

Description

  • Per the project spec, a creator may withdraw "once their campaign succeeds" — i.e. only after the funding goal is reached (and realistically only after the deadline).

  • withdraw transfers the full fund.amount_raised to the creator with no check that amount_raised >= goal and no check that the deadline has passed. The only restriction is has_one = creator + Signer on the account struct, which gates who calls it, not when.

  • As a result the creator can call withdraw at any moment — immediately after the first contribution — and take all contributed SOL from a campaign that has not met its goal.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised; // takes everything raised
// ❌ no `require!(amount >= ctx.accounts.fund.goal)`
// ❌ 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(())
}
#[derive(Accounts)]
pub struct FundWithdraw<'info> {
#[account(mut, seeds = [fund.name.as_bytes(), creator.key().as_ref()], bump, has_one = creator)]
pub fund: Account<'info, Fund>, // gates WHO (creator), not WHEN
#[account(mut)]
pub creator: Signer<'info>,
pub system_program: Program<'info, System>,
}

Risk

Likelihood: High

  • Requires only the creator's own signature — no special timing or external conditions. Any creator can do it to any of their campaigns at will.

Impact: High

  • Direct theft of contributor funds: a creator can collect contributions and immediately withdraw them while the campaign is unsuccessful, defeating the entire trust model of the platform (contributors believe funds are only released on success). Combined with H-1 (refunds return 0), contributors have no recourse.

Proof of Concept

Add to tests/poc_h2.ts and run with anchor test:

it("lets the creator withdraw before the goal is met and with no deadline", async () => {
// 100 SOL goal campaign, only 1 SOL contributed -> NOT successful
// creator calls withdraw and still receives the 1 SOL
// (full runnable test committed in tests/poc_h2.ts)
});

The committed test asserts that with amount_raised (1 SOL) < goal (100 SOL) and no deadline reached, withdraw still succeeds and the creator's balance increases by ~1 SOL.

Recommended Mitigation

Gate withdraw on campaign success: require the goal to be met (and require the deadline to have passed / be set). Also zero amount_raised after withdrawing to prevent any re-withdraw / accounting confusion.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
- let amount = ctx.accounts.fund.amount_raised;
+ let fund = &ctx.accounts.fund;
+ // Only allow withdrawal of a SUCCEEDED campaign.
+ require!(fund.dealine_set, ErrorCode::DeadlineNotSet);
+ require!(
+ Clock::get()?.unix_timestamp as u64 >= fund.deadline,
+ ErrorCode::DeadlineNotReached
+ );
+ require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotReached);
+ 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)?;
+ ctx.accounts.fund.amount_raised = 0;
Ok(())
}

(Requires adding GoalNotReached and DeadlineNotSet variants to ErrorCode.)

Updates

Lead Judging Commences

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