Rust Fund

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

Manual lamport debits in refund and withdraw have no rent-exemption floor, so the fund data account can be drained below rent and purged by the runtime

Root + Impact

Description

  • The Fund account is a data-bearing PDA that must always stay rent-exempt; any manual lamport withdrawal from it should leave at least the rent-exempt minimum so the runtime does not reclaim the account.

    Both refund and withdraw debit lamports directly from the fund with only a checked_sub, which prevents u64 underflow but does not stop the balance from dropping below the rent-exempt minimum. Combined with the stale amount_raised from L-1, a debit can leave the account below rent.

// withdraw (the same direct-debit pattern is used in refund):
**ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.fund.to_account_info().lamports()
@> .checked_sub(amount) // guards u64 underflow only, not the rent floor
.ok_or(ProgramError::InsufficientFunds)?;

Risk

Likelihood:

  • When amount_raised exceeds the contributed lamports still in the account (because refunds never decrement it, see L-1), the debit is computed against the full balance, including the rent reserve.

  • When the amount removed leaves the account below the rent-exempt minimum, nothing rejects the transfer.

Impact:

  • The Fund data account can be left below rent-exemption and become eligible for runtime garbage collection, destroying the campaign state.

  • The creator can also claw back part of their own rent deposit. The reachable value is bounded by the rent reserve (about 0.038 SOL) and needs a contrived sub-rent contribute-then-refund sequence, which is why this is Low.

Proof of Concept

Conceptual sequence (becomes reachable once H-2 is fixed so refunds actually move lamports):

// 1. contributor adds x with x <= rent reserve R; amount_raised = x, fund lamports = R + x
// 2. after the deadline, contributor refunds x; fund lamports = R, but amount_raised stays x
// 3. creator withdraw reads amount = x; (R).checked_sub(x) succeeds and leaves the fund below R
const bal = await connection.getBalance(fundPDA);
const minRent = await connection.getMinimumBalanceForRentExemption(fundAccountSize);
assert.isBelow(bal, minRent); // fund dropped below rent-exemption, eligible for purge

Recommended Mitigation

Assert that the fund stays at or above the rent-exempt minimum after every manual debit, which prevents the data account from being purged and stops withdrawals from dipping into rent. A cleaner structural fix is to keep contributed SOL in a dedicated SystemAccount vault PDA separate from the data account, so transfers never touch the data account's rent at all.

Add a WouldBreakRent variant to ErrorCode.

// after each manual lamport debit on the fund account:
+ let info = ctx.accounts.fund.to_account_info();
+ let min_rent = Rent::get()?.minimum_balance(info.data_len());
+ require!(info.lamports() >= min_rent, ErrorCode::WouldBreakRent);
Updates

Lead Judging Commences

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