H-02. Contribution Amount Not Recorded, Breaking Refund Mechanism
Severity: High
Category: Fund Management / Economic Integrity
Summary
The RustFund
protocol fails to properly update individual contribution amounts (contribution.amount
) upon receiving contributions, resulting in a permanently broken refund mechanism where contributors receive 0 SOL
when requesting refunds.
Vulnerability Details
The contribute
function within the RustFund contract (lib.rs
) correctly updates the total funds raised (fund.amount_raised
), but crucially neglects to record individual contributions in the contributor's account (contribution.amount
). As a result, when refunds are requested, the protocol attempts to refund 0 SOL
, leading to permanent loss of contributor funds.
Vulnerable Component:
-
File: lib.rs
-
Function: contribute
-
Struct: Contribution
Impact
-
Contributors permanently lose their contributions if a campaign fails.
-
Refund functionality is non-operational.
-
This vulnerability significantly undermines trust and violates the economic guarantees promised by the RustFund platform.
Proof of Concept (PoC)
await program.methods
.fundCreate(FUND_NAME, "Test fund", new anchor.BN(5 * anchor.web3.LAMPORTS_PER_SOL))
.accounts({
fund,
creator: creator.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([creator])
.rpc();
await program.methods
.contribute(new anchor.BN(2 * anchor.web3.LAMPORTS_PER_SOL))
.accounts({
fund,
contributor: contributor.publicKey,
contribution,
systemProgram: SystemProgram.programId,
})
.signers([contributor])
.rpc();
const pastTimestamp = Math.floor(Date.now() / 1000) - 86400;
await program.methods
.setDeadline(new anchor.BN(pastTimestamp))
.accounts({
fund,
creator: creator.publicKey,
})
.signers([creator])
.rpc();
const fundBalance = await provider.connection.getBalance(fund);
const contributionAccount = await program.account.contribution.fetch(contribution);
const contributorBalanceBefore = await provider.connection.getBalance(contributor.publicKey);
console.log("Fund account balance before refund:", fundBalance / anchor.web3.LAMPORTS_PER_SOL, "SOL");
console.log("Recorded contribution amount:", contributionAccount.amount / anchor.web3.LAMPORTS_PER_SOL, "SOL");
console.log("Contributor balance before refund:", contributorBalanceBefore / anchor.web3.LAMPORTS_PER_SOL, "SOL");
await program.methods
.refund()
.accounts({
fund,
contributor: contributor.publicKey,
contribution,
systemProgram: SystemProgram.programId,
})
.signers([contributor])
.rpc();
const contributorBalanceAfter = await provider.connection.getBalance(contributor.publicKey);
console.log("Contributor balance after refund:", contributorBalanceAfter / anchor.web3.LAMPORTS_PER_SOL, "SOL");
console.log("Refund amount returned:", (contributorBalanceAfter - contributorBalanceBefore) / anchor.web3.LAMPORTS_PER_SOL, "SOL");
Fund account balance before refund: 2.03759096 SOL
Recorded contribution amount: 0 SOL
Contributor balance before refund: 11.9927616 SOL
Refund succeeded!
Contributor balance after refund: 11.9927616 SOL
Refund amount returned: 0 SOL
*/
Tools Used
Recommendations
Explicitly update the contribution.amount
within the contribute
function after successfully transferring SOL from the contributor:
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());
}
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)?;
+ // Properly record the contribution amount
+ contribution.amount = contribution.amount
+ .checked_add(amount)
+ .ok_or(ErrorCode::CalculationOverflow)?;
fund.amount_raised += amount;
Ok(())
}