RustFund

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

Permanent Loss of Contributor Funds: Missing Update to contribution.amount in the contribute() rustfund Contract

Summary

The rustfund contract contains a logical error in the contribute() function that prevents contribution.amount from updating after a user makes a donation. Even though the code increments fund.amount_raised, the individual contributor’s record is never updated. As a result, the refund mechanism relies on a zeroed contribution.amount, preventing contributors from recovering the correct amount of funds. This issue disrupts the expected crowdfunding flow, undermines the integrity of individual contributions, and ultimately breaks the refund logic for users who should be entitled to their donated lamports if a project does not reach its goal.

Vulnerability Details

The rustfund contract fails to update the contribution.amount field in the contribute() function. While fund.amount_raised reflects the total lamports contributed, individual contributors’ amounts remain at zero, effectively breaking the logic for refunds. This oversight compromises the contract’s guarantee that users can retrieve their funds if the project does not succeed or if they become eligible for a refund.

In its current state, once a user initiates a valid contribution, there is no proper record of their deposit aside from the aggregated fund total. Any subsequent refund() call will use the uninitialized contribution.amount (which remains zero), meaning contributors are unable to recover their deposits. Although this issue does not inherently enable an external attacker to steal funds directly, it causes loss of user funds through an incomplete or misleading refund process.

Impact

This logic flaw undermines the contract’s refund mechanism, potentially causing permanent loss of contributed funds. Contributors are led to believe they can retrieve their deposits if the crowdfunding goal is not met or the deadline passes; however, because contribution.amount never reflects the actual amount contributed, no valid refund can occur. This defect results in a direct financial impact for users who cannot recover their funds, and it diminishes trust in the contract’s overall integrity.

Likelihood Explanation

This vulnerability manifests whenever contributors interact with the contribute() and refund() functions in a real-world scenario. Because the missing code update is consistent across all calls, every contribution will fail to correctly record the contributor’s amount. Consequently, any refund operation will lead to the same zero-amount issue. This makes the flaw highly likely to occur and reliably reproducible for every user who attempts to donate and then request a refund.

Proof of Concept

The logical error lies in the contribute() function, where the amount is transferred to the fund and fund.amount_raised is incremented, yet contribution.amount remains unchanged. As a result, if refund() is called later, the contributed funds are not reimbursed because contribution.amount remains at zero.

Code Analysis

Below is an abridged version of the contribute() function focusing on the relevant sections:

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
// ... Preliminary code ...
// Initialize or update contribution record
if contribution.contributor == Pubkey::default() {
contribution.contributor = ctx.accounts.contributor.key();
contribution.fund = fund.key();
contribution.amount = 0;
}
// (!) The amount is transferred but 'contribution.amount' is never updated
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;
Ok(())
}

After system_program::transfer(...), the update to contribution.amount is missing. The required line should be:

contribution.amount = contribution.amount.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;

Explanation

Since contribution.amount never increments during a contribution, the contract correctly records the transferred amount in fund.amount_raised but fails to mirror that amount in the contribution account. Consequently, refund() relies on a contribution.amount that remains zero, preventing users from retrieving their funds.

Vulnerable Scenario

  1. Alice creates a new fund using fund_create().

  2. Alice contributes 0.5 SOL via contribute(). Internally, fund.amount_raised increments, but contribution.amount remains at 0.

  3. The fund’s deadline passes, and refund() is called.

  4. The refund() function attempts to return the amount stored in contribution.amount, which is 0, so Alice does not get her 0.5 SOL back.

Test and Result

This test aims to verify that when a user contributes a specific amount to the fund, both contribution.amount and fund.amountRaised are updated accordingly. After invoking the contribute() method and fetching the relevant on-chain accounts, the test checks if the recorded amounts match the expected value. In the provided output, contribution.amount remains at zero instead of reflecting the correct 500000000 lamports, confirming that the code to increment this field is missing or not executed, resulting in the failing assertion.

  • Add the following test to tests/rustfund.ts after of the function test Contributes to fund

it("Contributes to fund", async () => {});
it("should update the contribution amount when a user contributes", async () => {
// Derive the PDA for the contribution account
[contributionPDA, contributionBump] = await PublicKey.findProgramAddress(
[fundPDA.toBuffer(), provider.wallet.publicKey.toBuffer()],
program.programId
);
// Invoke the 'contribute' function to transfer the specified amount
await program.methods
.contribute(contribution)
.accounts({
fund: fundPDA,
contributor: provider.wallet.publicKey,
contribution: contributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// Fetch the updated 'fund' and 'contribution' accounts to validate changes
const fundAccount = await program.account.fund.fetch(fundPDA);
const contributionAccount = await program.account.contribution.fetch(
contributionPDA
);
// Confirm that 'contribution.amount' correctly reflects the contributed amount
expect(contributionAccount.amount.toNumber()).to.equal(
contribution.toNumber(),
"The contribution.amount was not correctly updated"
);
// Verify that 'fund.amountRaised' also matches the newly contributed amount
expect(fundAccount.amountRaised.toNumber()).to.equal(
contribution.toNumber(),
"The fund.amountRaised was not correctly updated"
);
});
it("Refunds contribution", async () => {});
1) rustfund
should update the contribution amount when a user contributes:
The contribution.amount was not correctly updated
+ expected - actual
-0
+500000000

Confirmation

This flaw is confirmed by observing that contribution.amount never increases after a contribution. Its persistent zero value leads to refund() failing to return the appropriate funds. A safe and effective fix is to update contribution.amount within contribute(), for example by using checked_add to avoid overflow.

Tools Used

Manual Code Review
The code was systematically examined line by line. It was inspected in detail to identify logical inconsistencies and confirm the error through targeted testing.

Recommendations

Include a line to increment the contribution.amount within the contribute() function, ensuring it tracks each user's donation amount. Use a safe addition operation to prevent overflow:

contribution.amount = contribution.amount.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;

This change ensures the refund mechanism properly returns the correct amount to contributors.

Updates

Appeal created

bube Lead Judge 3 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.