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(())
}