RustFund

First Flight #36
Beginner FriendlyRust
100 EXP
View results
Submission Details
Severity: high
Valid

Missing Update of Contribution Amount in `FundContribute`" issue LEads to Loss of Fund

https://github.com/CodeHawks-Contests/2025-03-rustfund/blob/main/programs/rustfund/src/lib.rs#L25-L52

Summary

The "rustfund" contains a critical vulnerability in the FundContribute function where the contribution amount (contribution.amount) is not updated after a contributor transfers SOL to the fund. While the total amount raised (fund.amount_raised) is correctly incremented, the individual contribution record remains unchanged, defaulting to 0. This bug prevents contributors from receiving refunds in the FundRefund function, as it relies on the contribution.amount value to determine the refundable amount. This vulnerability undermines the contract's refund mechanism, a core feature of crowdfunding systems, potentially leading to loss of funds for contributors.


Vulnerability Details

The vulnerability arises in the FundContribute function, which handles contributions to a fund. When a contributor sends SOL, the contract transfers the amount to the fund account and updates fund.amount_raised, but it fails to update the contribution.amount field in the Contribution account. This field is critical for tracking individual contributions and enabling refunds.

Root Cause

The root cause is the absence of an instruction to increment contribution.amount after the SOL transfer. The code initializes contribution.amount to 0 for new contributions but does not update it with the contributed amount. Below is the relevant snippet from the FundContribute function:

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
let contribution = &mut ctx.accounts.contribution;
if fund.deadline != 0 && fund.deadline < Clock::get().unwrap().unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::DeadlineReached.into());
}
// Initialize or update contribution record
if contribution.contributor == Pubkey::default() {
contribution.contributor = ctx.accounts.contributor.key();
contribution.fund = fund.key();
contribution.amount = 0; // Initialized to 0, but never updated
}
// Transfer SOL from contributor to fund account
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.contributor.to_account_info(),
to: fund.to_account_info(),
},
);
system_program::transfer(cpi_context, amount)?;
fund.amount_raised += amount; // Updates total, but not individual contribution
Ok(())
}

In this snippet:

  • contribution.amount is set to 0 when the contribution account is first initialized.

  • After the SOL transfer (system_program::transfer(cpi_context, amount)?), there is no line like contribution.amount += amount; to record the contribution.

  • Only fund.amount_raised is updated, tracking the aggregate total but not the individual contributor’s stake.

Dependency in FundRefund

The FundRefund function relies on contribution.amount to determine how much SOL to refund, as shown below:

pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
@> let amount = ctx.accounts.contribution.amount; // Uses contribution.amount for refund
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(())
}

Since contribution.amount remains 0 due to the bug in FundContribute, the amount variable in FundRefund will always be 0, meaning no SOL is transferred back to the contributor, even when a refund is valid (e.g., after the deadline).


Impact

This vulnerability has a severe impact on the contract’s functionality and trustworthiness:

  • Loss of Funds for Contributors: Contributors cannot reclaim their contributions, even when eligible for a refund (e.g., if the deadline passes and the goal isn’t met). Since contribution.amount is always 0, the refund mechanism transfers no SOL, effectively locking contributors’ funds in the fund account.

Tools Used

The analysis was conducted using the following tools and resources:

  • Manual Code Review

Recommendations

To address this vulnerability, the following steps are recommended:

  1. Update contribution.amount in FundContribute:
    Modify the contribute function to increment contribution.amount after the SOL transfer. Here’s the corrected code snippet:

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
let contribution = &mut ctx.accounts.contribution;
if fund.deadline != 0 && fund.deadline < Clock::get().unwrap().unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::DeadlineReached.into());
}
// Initialize or update contribution record
if contribution.contributor == Pubkey::default() {
contribution.contributor = ctx.accounts.contributor.key();
contribution.fund = fund.key();
contribution.amount = 0;
}
// Transfer SOL from contributor to fund account
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.contributor.to_account_info(),
to: fund.to_account_info(),
},
);
system_program::transfer(cpi_context, amount)?;
fund.amount_raised += amount;
+ contribution.amount += amount; // Add this line to record the contribution
Ok(())
}

This ensures contribution.amount reflects the actual amount contributed, enabling accurate refunds.

Updates

Appeal created

bube Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Contribution amount is not updated

Support

FAQs

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