Rust Fund

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

`contribute()` has no cap at goal amount leaving over-contributors with no refund path

Root + Impact

Description

  • The contribute instruction is designed for contributors to fund a campaign up to its stated goal, with the expectation that contributions are refundable if the goal is not met.

  • contribute accepts new contributions even after fund.amount_raised >= fund.goal with no cap. Late contributors who push the total above the goal have no mechanism to recover their excess — the creator withdraws the full pot including the excess, and refund eligibility requires the goal to not be met, so over-funded amounts are permanently non-refundable.

// No check like:
@> // require!(fund.amount_raised + amount <= fund.goal, ErrorCode::GoalAlreadyMet)

Risk

Likelihood:

  • A late contributor who sends SOL to a campaign that has already reached its goal enters this state automatically — no special conditions are required beyond contributing after the goal is met.

  • Popular campaigns where the goal is reached quickly are most exposed, as many contributors may arrive after the goal is already visible as met.

Impact:

  • Late contributors who push the total above the goal lose their entire contribution with no recourse — the creator withdraws the full pot including the excess, and the refund path is permanently closed for over-funded amounts.

  • The protocol makes an implicit promise that unmet-goal contributions are refundable, but over-funded contributions are silently non-refundable under the current logic.

Proof of Concept

Place this test in tests/ and run anchor test. The test demonstrates that a late contributor can send SOL to an already-funded campaign with no cap, losing their contribution permanently since refunds require the goal to not be met.

it("late contributor loses SOL when contributing after goal is met", async () => {
// First contributor meets the goal exactly
await program.methods.contribute(goalAmount)
.accounts({ fund, contributor: earlyContributor.publicKey, contribution: earlyContribution })
.signers([earlyContributor])
.rpc();
// Late contributor contributes more — no cap prevents this
const extra = new BN(100_000);
await program.methods.contribute(extra)
.accounts({ fund, contributor: lateContributor.publicKey, contribution: lateContribution })
.signers([lateContributor])
.rpc(); // succeeds, no error
// lateContributor cannot refund — goal was met, refund requires goal NOT met
// Creator withdraws everything including the excess
});

Recommended Mitigation

Add a require! check at the top of contribute() that ensures fund.amount_raised + amount <= fund.goal, preventing contributions once the campaign goal is reached.

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
+ require!(
+ fund.amount_raised.checked_add(amount)
+ .ok_or(ErrorCode::CalculationOverflow)? <= fund.goal,
+ ErrorCode::GoalAlreadyMet
+ );
// ... rest of contribute logic
}
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!