01. Relevant GitHub Links
02. Summary
The contribute function in the RustFund program does not update the contribution.amount field after a user contributes to a fund. While fund.amount_raised is incremented correctly, contribution.amount remains 0, leading to inaccurate contribution tracking and potential refund issues.
03. Vulnerability Details
Users contribute to a fund using the contribute 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());
}
if contribution.contributor == Pubkey::default() {
contribution.contributor = ctx.accounts.contributor.key();
contribution.fund = fund.key();
contribution.amount = 0;
}
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(())
}
The issue lies in contribution.amount being initialized to 0 and never updated with the contributed amount, remaining 0 even after a successful contribution.
04. Impact
This vulnerability causes inaccurate tracking of individual contributions, as contribution.amount does not reflect the user’s contribution while fund.amount_raised is updated correctly. Additionally, this prevents users from refunding their contributions later. In the refund function, the amount to refund is sourced from contribution.amount, which remains 0 due to this bug. As a result, users cannot reclaim their funds even if the deadline passes and refunds are allowed, effectively locking their contributions.
05. Proof of Concept
Add the following PoC code to rustfund.ts to test the vulnerability:
it.only("contribution.amount is naver update", async () => {
[contributionPDA, contributionBump] = await PublicKey.findProgramAddress(
[fundPDA.toBuffer(), provider.wallet.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(fundName, description, goal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
await program.methods
.contribute(contribution)
.accounts({
fund: fundPDA,
contributor: provider.wallet.publicKey,
contribution: contributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const contributionAccount = await program.account.contribution.fetch(contributionPDA);
console.log("contributionAccount.amount: ", await contributionAccount.amount);
expect(contributionAccount.amount.toString()).to.equal((0).toString());
});
Output:
rustfund
contributionAccount.amount: <BN: 0>
✔ contribution.amount is naver update (694ms)
06. Tools Used
Manual Code Review and Foundry
07. Recommended Mitigation
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;
}
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)?;
+ contribution.amount += amount;
fund.amount_raised += amount;
Ok(())
}