Rust Fund

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

refund ignores the goal, never decrements amount_raised, and is bypassable when deadline == 0

Description

Per the specification, a contributor may refund only when the deadline has been reached and the goal was not met. refund checks neither the goal nor a properly-set deadline, and it never decrements the fund's tracked total:

pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let amount = ctx.accounts.contribution.amount;
if ctx.accounts.fund.deadline != 0
&& ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::DeadlineNotReached.into());
}
// move `amount` lamports fund -> contributor
// ...
ctx.accounts.contribution.amount = 0; // fund.amount_raised is NEVER updated
Ok(())
}

Three defects compound here:

  1. No goal check. A contributor can refund even on a successful campaign (goal met). Combined with the ungated withdraw, the creator and every contributor race for the same lamports — whoever calls first is paid and the loser's checked_sub reverts.

  2. amount_raised is never decremented. After a refund the fund's real lamport balance drops but fund.amount_raised stays inflated. withdraw computes its payout from that stale, too-large value, so settlement no longer matches reality — it either reverts (griefing the creator) or, as contributions and refunds interleave, pays out against amounts that are no longer held.

  3. deadline == 0 bypasses the guard entirely. The check is deadline != 0 && deadline > now. If the creator never calls set_deadline, deadline is 0, the condition short-circuits to false, and refunds are allowed immediately — a contributor can contribute and refund in the same slot, well before any intended deadline.

Risk

Impact: High. Broken refund/settlement accounting: refunds succeed on successful campaigns, the raised-total desyncs from the real balance and corrupts withdraw, and refunds are possible before any deadline. Contributor and creator funds are mis-settled.

Likelihood: High. All three paths are reachable with ordinary calls; the deadline == 0 bypass is the default state right after fund_create.

Proof of Concept

// anchor test (sketch) — refund before any deadline, accounting left stale
await program.methods.fundCreate("camp", "desc", new BN(1_000_000_000))
.accounts({ fund, creator: creator.publicKey }).signers([creator]).rpc();
await program.methods.contribute(new BN(500_000_000))
.accounts({ fund, contribution, contributor: alice.publicKey }).signers([alice]).rpc();
// deadline was never set (== 0) -> the guard is skipped
await program.methods.refund()
.accounts({ fund, contribution, contributor: alice.publicKey }).signers([alice]).rpc(); // succeeds pre-deadline
const f = await program.account.fund.fetch(fund);
assert(f.amountRaised.eq(new BN(500_000_000))); // STILL 500_000_000 — desynced from the now-empty fund

Recommended Mitigation

Require a finalized, failed campaign, and keep amount_raised in lockstep:

let fund = &mut ctx.accounts.fund;
require!(fund.dealine_set, ErrorCode::DeadlineNotSet);
require!(Clock::get()?.unix_timestamp as u64 >= fund.deadline, ErrorCode::DeadlineNotReached);
require!(fund.amount_raised < fund.goal, ErrorCode::GoalMet);
let amount = ctx.accounts.contribution.amount;
fund.amount_raised = fund.amount_raised.checked_sub(amount).ok_or(ErrorCode::CalculationOverflow)?;
// ... move `amount` lamports fund -> contributor ...
ctx.accounts.contribution.amount = 0;
Updates

Lead Judging Commences

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