Rust Fund

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

No goal check on withdraw — creator drains funds without meeting goal

Title: No goal check on withdraw — creator drains funds without meeting goal
Impact: High. Creator can withdraw all raised funds regardless of whether the campaign met its goal.
Likelihood: High. No goal condition exists — withdraw works the moment any lamports arrive.
Reference Files: programs/rustfund/src/lib.rs:90-105

Description

The withdraw() instruction transfers the fund's entire amount_raised to the creator with only an access control check. There is no validation that amount_raised >= goal — the creator can withdraw even when the campaign is far below its funding target. The protocol description states creators should withdraw "once their campaign succeeds" but the code enforces no success condition.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// NO: require amount_raised >= goal
**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)?;
}

The Fund struct declares a goal field and amount_raised field specifically to track campaign success — but withdraw() never compares them.

Risk

Impact: High. A creator sets a goal of 100 SOL, receives 10 SOL in contributions, and immediately calls withdraw() — walking away with 10 SOL while contributors expected either a fully funded campaign or a refund.
Likelihood: High. The creator needs only to call withdraw() with no threshold requirement. The test file never verifies the goal was met before withdrawal, confirming the check was never intended to exist.
With a goal of 1,000 SOL and 50 SOL raised, the creator drains all 50 SOL instantly — the goal is meaningless.

Proof of Concept

it("Creator withdraws without meeting goal", async () => {
await program.methods.contribute(new anchor.BN(500_000_000))
.accounts({ fund: fundPDA, contributor: creator.publicKey, contribution: contribPDA, systemProgram: SystemProgram.programId }).rpc();
const fund = await program.account.fund.fetch(fundPDA);
assert(fund.amountRaised.lt(fund.goal)); // 0.5 < 1.0 — goal NOT met
const balBefore = await connection.getBalance(creator.publicKey);
await program.methods.withdraw()
.accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: SystemProgram.programId }).rpc();
assert((await connection.getBalance(creator.publicKey)) > balBefore); // drained without goal!
});

The PoC proves the creator withdraws funds without meeting the campaign's funding goal.

Recommended Mitigation

require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotMet);

Add a goal comparison before allowing withdrawal, ensuring creators can only access funds from successful campaigns.

Updates

Lead Judging Commences

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