fund.goal is never read anywhere in the program, so the "withdraw only on success" guarantee does not existRustFund's value proposition is escrow: contributors send SOL to a campaign on the understanding that the creator can only take it after the campaign succeeds, and that they can get it back otherwise. The README is explicit — "Secure Withdrawals: Creators can withdraw funds once their campaign succeeds" and "Withdraws raised funds after successful campaigns".
withdraw implements no success condition of any kind. It reads amount_raised, moves that many lamports to the creator, and returns. The goal is not compared, the deadline is not compared, and no state flag records that the campaign ever ended. A creator can therefore call withdraw in the same block as the first contribution, with the goal 1% met and weeks left on the clock.
fund.goal is stored at line 16 and declared at line 186 — and that is the whole of its existence. It is never read by any instruction in the file. The success criterion the entire product is built around is written to storage and then ignored, which is what turns "escrow" into "a wallet the creator can empty".
The unused ErrorCode::UnauthorizedAccess variant declared at line 204 and referenced nowhere in the program is a further signal that an intended gate was never written.
Likelihood:
A single instruction call with no preconditions. The creator needs no timing, no race, no cooperation, and no unusual state — withdraw is callable from the moment the fund is created.
There is no on-chain signal that would let a contributor detect the risk in advance. Nothing in the account state distinguishes a campaign that can be drained early from one that cannot, because every campaign can.
The incentive points straight at it: an early withdrawal converts a conditional claim into unconditional cash at zero cost, in one instruction.
The "creator" is not a vetted role. fund_create (lines 12-22) is permissionless — any signer can become a creator — so the privileged actor here is an arbitrary, unbonded stranger, not a project admin.
Impact:
Loss of the entire contributed balance. The creator receives 100% of amount_raised with the campaign's stated condition unmet. (The amount is bounded by checked_sub at line 95 against the PDA's real balance, so the creator takes exactly what was contributed and cannot reach beyond it — the harm is that they take it at all, not that they take extra.)
The core protocol invariant — "funds are released only on success" — is absent from the code, so the trustlessness the README claims ("in a trustless and transparent manner") does not hold. This is not a creator abusing a documented privilege; it is the documented restriction on that privilege never having been implemented.
Contributors are structurally out-raced. Refund eligibility begins at the deadline (line 69) while withdrawal eligibility begins at fund creation, so the creator's window strictly contains and precedes every contributor's window. There is no ordering in which a contributor can act first.
A campaign with a 100 SOL goal and a deadline in the future raises 5 SOL. The creator withdraws all 5 SOL immediately. Setup and helpers are identical to the other reports; only the scenario differs.
Output:
5% of the goal met, an hour left on the clock, and the creator walks away with everything the contributors sent.
The creator is authenticated (has_one = creator, line 163), but authentication is not authorisation. The restriction the protocol advertises is not who may withdraw — that part works — it is when. That second condition is absent from the code, so an authorised actor performs an action the specification forbids. The unused goal field is the direct evidence that the check was intended and omitted rather than deliberately left out.
Gate the withdrawal on the two conditions the README states, and record that it happened so it cannot be repeated.
Add the matching error variant (UnauthorizedAccess at line 204 is currently unused and could be repurposed, but a dedicated variant is clearer):
# 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, } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.