RustFund

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

Missing Contribution Amount Update

Description

In the contribute function of the Rustfund smart contract, the contribution.amount variable is never updated with the actual contribution amount. While funds are successfully transferred from the contributor to the fund account, the individual contribution record is not properly tracked. This means that a contributor's personal contribution history is not accurately maintained, resulting in a loss of traceability and potentially resulting in fund loss during refund operations.

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)?;
// @audit-note REPTOU: Overflow possible ? (but u64 so...)
fund.amount_raised += amount;

Impact

  1. Contributors cannot receive proper refunds based on their actual contribution amount

  2. The contract maintains incorrect accounting records of individual contributions

  3. When a refund is requested, a contributor will only be able to withdraw what's stored in their contribution record (which remains at 0)

Proof of Concept (PoC)

The vulnerability is demonstrated in the provided test case:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Rustfund } from "../target/types/rustfund";
import { PublicKey } from '@solana/web3.js';
import { expect } from 'chai';
describe("rustfund", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Rustfund as Program<Rustfund>;
const creator = provider.wallet;
const otherUser = anchor.web3.Keypair.generate();
const fundName = "0xWithdrawers Fund";
const description = "VULN-01";
const goal = new anchor.BN(1000000000); // 1 SOL
const contribution = new anchor.BN(500000000); //0.5 SOL
const deadline = new anchor.BN(Math.floor(Date.now() / 1000) + 10); // 10 sec from now (testing)
let fundPDA: PublicKey;
let fundBump: number;
let contributionPDA: PublicKey;
let contributionBump: number;
before(async () => {
// Generate PDA for fund
[fundPDA, fundBump] = await PublicKey.findProgramAddress(
[Buffer.from(fundName), creator.publicKey.toBuffer()],
program.programId
);
});
it("Creates a fund", async () => {
await program.methods
.fundCreate(fundName, description, goal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const fund = await program.account.fund.fetch(fundPDA);
console.log("fundName", fund.name);
console.log("fundDescription", fund.description);
console.log("fundGoal", fund.goal);
console.log("fundCreator", fund.creator);
console.log("fundAmountRaised", fund.amountRaised);
});
it("Contributes to fund", async () => {
// Generate PDA for contribution
[contributionPDA, contributionBump] = await PublicKey.findProgramAddress(
[fundPDA.toBuffer(), provider.wallet.publicKey.toBuffer()],
program.programId
);
// Perform a contribution of 0.5 SOL
await program.methods
.contribute(contribution)
.accounts({
fund: fundPDA,
contributor: provider.wallet.publicKey,
contribution: contributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const fund = await program.account.fund.fetch(fundPDA);
const contributionAccount = await program.account.contribution.fetch(contributionPDA);
console.log("fundBalanceAfter", await provider.connection.getBalance(fundPDA));
console.log("contributorAmountAfter", contributionAccount.amount);
// We ensure that the amountRaised is equal to the only contributor amount
expect(fund.amountRaised.toString()).to.equal(contribution.toString());
// We check now that the contributor account amount is still 0 whereas
// he just contributed
expect(contributionAccount.amount.toString()).to.equal("0");
});
});

Save the above test as, for example, tests/01.ts in your project's test directory and run the test :

anchor test

The test verifies that:

The fund's amountRaised is correctly updated to 0.5 SOL
However, the contributor's recorded amount in the contribution account remains at 0

This confirms that while the SOL transfer occurs properly, the individual contribution record is not updated.

Concrete Impact Example

To illustrate the real-world impact of this vulnerability, consider this scenario:

  • A startup launches a campaign with a 1000 SOL goal

  • The deadline is reached and 4 contributors have sent 200 SOL each to the contract

  • Contributors ask for a refund, but their contribution amount is still 0, so they are unable to retrieve their funds

Recommendation

Add a line to update the contribution amount after the transfer:

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
// [existing code...]
system_program::transfer(cpi_context, amount)?;
// Update both the fund total and the individual contribution record
fund.amount_raised += amount;
contribution.amount += amount; // Add this line to fix the vulnerability
Ok(())
}
Updates

Appeal created

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