Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Severity: low
Valid

Unsafe lamport manipulation (no rent exemption check) in refund and withdraw function.

Root + Impact

Description

  • The refund and withdraw functions in the RustFund program directly manipulate the lamport balance of the fund account (via try_borrow_mut_lamports()) to transfer SOL to contributors/creators. But:


    • No check is performed to ensure the fund account’s remaining lamports meet Solana’s rent-exempt minimum balance after the transfer.

    • If the transfer reduces the fund account’s lamports below the rent-exempt threshold, the account is automatically closed by the Solana network. This deletes all critical data (e.g., contribution records, campaign details, deadline status) and renders core platform functionality (refunds, withdrawals) permanently non-functional.

// Root cause in the codebase with @> marks to highlight the relevant section
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());
}
**ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.fund.to_account_info().lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;
**ctx.accounts.contributor.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.contributor.to_account_info().lamports()
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
// Reset contribution amount after refund
ctx.accounts.contribution.amount = 0;
Ok(())
}

Risk

Likelihood:

  • The code does not:
    Fetch the current rent parameters (via Rent::get()?).
    Calculate the rent-exempt minimum balance for the fund account (based on its data size: Rent::minimum_balance(fund.data_len())).
    Validate that fund.lamports() - transfer_amount >= minimum_balance before modifying lamports.


Impact:

  • Impact 1: Permanent Loss of Campaign Data

    If the fund account is closed:

    • All Contribution records (linking contributors to their donated amounts) are lost → contributors cannot prove their donations and cannot claim refunds.

    • Campaign metadata (name, description, goal, deadline) is deleted → creators cannot verify campaign status or withdraw funds even if the goal was met.

  • Impact 2: Irreversible Fund Loss

    • If the fund account is closed, remaining lamports (unrefunded/withdrawn funds) are sent to the account’s owner (e.g., the creator), but contributors lose access to their funds (no records to prove eligibility for refunds).

    • In worst-case scenarios, the closed account cannot be recovered, leading to permanent loss of all funds in the campaign.

Proof of Concept

Prerequisites:
A deployed RustFund program instance.
A campaign (fund account) with:
Data size = 5265 bytes (per the Fund struct’s INIT_SPACE).
Current rent-exempt minimum balance = 0.002 SOL (2,000,000 lamports, based on Solana’s 2025 rent parameters).
Total lamports in the fund account = 2,500,000 lamports (500,000 lamports above the rent-exempt minimum).
A contributor eligible for a refund of 600,000 lamports.
Reproduction Steps:
The contributor calls the refund function to claim 600,000 lamports.
The program subtracts 600,000 lamports from the fund account:
2,500,000 - 600,000 = 1,900,000 lamports (below the 2,000,000 rent-exempt minimum).
The transaction succeeds, and the lamport transfer is completed.
Solana’s runtime detects the fund account has insufficient balance for rent exemption and automatically closes the account.
All data in the fund account (contribution records, campaign details) is deleted.
Result
The transaction succeeds, the account is closed, and all campaign data/fund access is lost.

Recommended Mitigation

// Adding rent-exemption validation before modifying lamports in both refund and withdraw functions.
// Below is the corrected code for the refund function (the same logic applies to withdraw):
pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let fund = &mut ctx.accounts.fund;
let contribution = &mut ctx.accounts.contribution;
let contributor = &mut ctx.accounts.contributor;
// Step 1: Validate contribution amount > 0
let amount = contribution.amount;
if amount == 0 {
return Err(ErrorCode::NoContributionToRefund.into());
}
// Step 2: Validate deadline and goal (existing logic)
let current_timestamp = Clock::get()?.unix_timestamp as u64;
if fund.deadline == 0 || fund.deadline > current_timestamp {
return Err(ErrorCode::DeadlineNotReached.into());
}
if fund.amount_raised >= fund.goal {
return Err(ErrorCode::FundGoalReachedNoRefund.into());
}
// Step 3: Fetch Solana rent parameters and calculate minimum balance
let rent = Rent::get()?;
let min_balance = rent.minimum_balance(fund.to_account_info().data_len());
// Step 4: Check if fund account remains rent-exempt after refund
let fund_lamports = fund.to_account_info().lamports();
if fund_lamports.checked_sub(amount).ok_or(ProgramError::InsufficientFunds)? < min_balance {
return Err(ErrorCode::InsufficientBalanceForRentExemption.into());
}
// Step 5: Safe lamport transfer (existing logic)
**fund.to_account_info().try_borrow_mut_lamports()? = fund_lamports
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;
**contributor.to_account_info().try_borrow_mut_lamports()? = contributor
.to_account_info()
.lamports()
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
// Step 6: Reset contribution amount
contribution.amount = 0;
Ok(())
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[L-03] Unsafe Direct Lamport Manipulation in refund(), withdraw() Functions

## Description The `refund` function in the provided code directly manipulates the lamports of accounts using `try_borrow_mut_lamports()`. This approach bypasses the Solana runtime's safety checks, leading to potential security vulnerabilities and program instability. ## Vulnerability Details In the `refund` function, lamports are transferred between accounts by directly adjusting their balances: &#x20; ```Rust **ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.fund.to_account_info().lamports().checked_sub(amount).ok_or(ProgramError::InsufficientFunds)?; **ctx.accounts.contributor.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.contributor.to_account_info().lamports().checked_add(amount).ok_or(ErrorCode::CalculationOverflow)?; ``` This method of direct lamport manipulation can lead to several issues: 1. **Bypassing Rent Exemption Checks:** Accounts in Solana must maintain a minimum balance to be rent-exempt. Directly reducing an account's lamports without verifying rent exemption can result in the account being marked for deletion by the Solana runtime. 2. **Ownership Constraints:** Only the owning program of an account can modify its data and lamport balance. Direct manipulation without proper checks can violate these constraints, leading to program errors. 3. **Lack of Atomicity:** Direct lamport transfers lack the atomic transaction guarantees provided by the system program's transfer instruction, potentially leading to inconsistent states in case of program interruptions. ## Impact Exploiting this vulnerability can result in unauthorized fund transfers, violation of Solana's account ownership rules, and potential loss of funds due to accounts becoming non-rent-exempt. ## Recommendations Replace the direct lamport manipulation with Solana's system program transfer instruction to ensure safe and compliant fund transfers in refund() & withdraw() functions: &#x20; ```Rust let cpi_context = CpiContext::new( ctx.accounts.system_program.to_account_info(), system_program::Transfer { from: ctx.accounts.fund.to_account_info(), to: ctx.accounts.contributor.to_account_info(), }, ); system_program::transfer(cpi_context, amount)?; ``` This approach leverages Solana's native mechanisms for transferring lamports, ensuring adherence to the platform's safety and security protocols.

Support

FAQs

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

Give us feedback!