Rust Fund

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

M-02] `refund()` skips deadline enforcement when no deadline is set, and never checks if the campaign goal was met

Description

The deadline guard in refund() uses a short-circuit && that skips enforcement when deadline == 0 (the default). Contributors can refund immediately after contributing if the creator hasn't called set_deadline() yet. Additionally, refund() never checks whether fund.amount_raised < fund.goal, so even successful campaigns can be drained by refunds after the deadline.

Vulnerability Details

// lib.rs:66-88
pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let amount = ctx.accounts.contribution.amount;
// @> When deadline == 0 (initial state), first condition is false,
// @> so the entire check is skipped — refund proceeds immediately
if ctx.accounts.fund.deadline != 0
&& ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap()
{
return Err(ErrorCode::DeadlineNotReached.into());
}
// @> No check: fund.amount_raised < fund.goal
// ... lamport transfer ...
}

Two separate issues in the same guard:

  1. Deadline bypass: fund_create() sets deadline = 0. Until the creator calls set_deadline(), the guard evaluates 0 != 0 && ... which is false, so the refund always proceeds. In a protocol where H-01 is fixed, any contributor can refund immediately after contributing, making it impossible for any campaign to accumulate funds.

  2. Missing goal check: Even after the deadline passes, there is no check for amount_raised < goal. If a campaign succeeds (goal met), contributors can still refund after the deadline, draining the fund below the goal and breaking the creator's ability to withdraw.

Risk

Likelihood:

  • Deadline bypass happens on every fund before set_deadline() is called.

  • Missing goal check affects every fund that reaches its goal.

Impact:

  • In a fixed protocol, no campaign can accumulate funds before a deadline is set (contributors immediately refund).

  • Successful campaigns can be drained by refunds after the deadline, breaking the creator-withdrawal flow.

Proof of Concept

// Issue 1: Deadline bypass when deadline == 0
// 1. Creator calls fund_create("Test", "desc", goal=100 SOL)
// fund.deadline = 0, fund.dealine_set = false
// 2. Bob calls contribute(50 SOL)
// fund.amount_raised = 50 SOL
// 3. Bob calls refund() BEFORE creator sets any deadline
// Guard: fund.deadline != 0 && fund.deadline > now
// Evaluates: 0 != 0 && ... => false && ... => false
// Guard is SKIPPED, refund proceeds immediately
// (currently returns 0 due to H-01, but if H-01 is fixed, Bob gets 50 SOL back)
// Issue 2: Missing goal check after deadline
// 1. Creator: fund_create("Project", "desc", goal=100 SOL)
// 2. Creator: set_deadline(now + 7 days)
// 3. Contributors deposit 150 SOL total (goal exceeded)
// 4. Deadline passes
// 5. Contributor calls refund() -- succeeds because there is NO check for:
// require!(fund.amount_raised < fund.goal, ErrorCode::GoalReached)
// 6. Multiple contributors refund, draining fund below 100 SOL goal
// 7. Creator calls withdraw() -- fund no longer has enough lamports

Recommendations

Replace the weak guard with strict checks:

- if ctx.accounts.fund.deadline != 0
- && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap()
- {
- return Err(ErrorCode::DeadlineNotReached.into());
- }
+ require!(ctx.accounts.fund.deadline != 0, ErrorCode::DeadlineNotSet);
+ let now: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
+ require!(now >= ctx.accounts.fund.deadline, ErrorCode::DeadlineNotReached);
+ require!(
+ ctx.accounts.fund.amount_raised < ctx.accounts.fund.goal,
+ ErrorCode::GoalReached
+ );
Updates

Lead Judging Commences

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