Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

withdraw has no goal or deadline check — creator can drain the fund at any time

Description

withdraw (programs/rustfund/src/lib.rs) reads fund.amount_raised and transfers it to creator unconditionally. There is no check that fund.amount_raised >= fund.goal and no check that fund.deadline has passed:

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)...;
**ctx.accounts.creator.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.creator.to_account_info().lamports().checked_add(amount)...;
Ok(())
}

The README states "Creators can withdraw funds once their campaign succeeds". Neither success condition is enforced.

Risk

Impact: High — the creator can take every contributed lamport immediately after contributions arrive, regardless of goal or deadline. All contributors lose their funds; the all-or-nothing model the protocol promises does not exist. After the withdrawal the fund PDA holds only rent, so any later refund fails with InsufficientFunds — contributors have no recourse. Irreversible; affects every campaign.

Likelihood: High — a single ordinary withdraw call by the creator, no preconditions, no special timing.

Proof of Concept

Test from tests/poc.ts (raw @solana/web3.js, instructions built by hand, run against solana-test-validator --bpf-program <program_id> target/deploy/rustfund.so):

it("F1 [High]: creator withdraws before deadline and with goal not reached", async () => {
const GOAL = 10 * LAMPORTS_PER_SOL, CONTRIB = 2 * LAMPORTS_PER_SOL;
const f = fundPda("F1", creator.publicKey);
await send(ixFundCreate(f, creator.publicKey, "F1", "d", GOAL), creator);
await send(ixSetDeadline(f, creator.publicKey, now() + 30 * 24 * 3600), creator); // deadline 30 days ahead
await send(ixContribute(f, victim.publicKey, CONTRIB), victim);
const st = await getFund(f);
expect(st.amountRaised).to.be.lessThan(st.goal); // goal NOT reached
const before = await bal(creator.publicKey);
await send(ixWithdraw(f, creator.publicKey), creator); // must fail — succeeds
const after = await bal(creator.publicKey);
expect(after - before).to.be.greaterThan(CONTRIB - 10_000); // creator received ~2 SOL
});

Output:

goal=10 raised=2 deadline in 2591999s
creator gained 1.999995 SOL
✔ F1 [High]: creator withdraws before deadline and with goal not reached

Recommended Mitigation

Require both success conditions before transferring, and reset the counter afterwards:

let now: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
require!(fund.dealine_set && fund.deadline <= now, ErrorCode::DeadlineNotReached);
require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotReached);
// ... transfer ...
fund.amount_raised = 0;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!