Summary
contribute
function does not update the contribution.amount
field when contributors add funds, leading to incorrect refund calculations if the funding goal isn't met. The refund process relies on contribution.amount
to determine how much each contributor should get back, meaning contributors may receive nothing or incorrect amounts in case of a refund.
Vulnerability Details
#[account]
#[derive(InitSpace)]
pub struct Contribution {
pub contributor: Pubkey,
pub fund: Pubkey,
pub amount: u64,
}
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(())
}
Impact
Since FundRefund
processes refunds based on contribution.amount
, this leads to:
Funds incorrectly distributed during refunds → Some contributors may lose their money entirely.
Loss of contributor trust → Crowdfunding is based on fairness; if refunds break, the contract is unusable.
Potential legal consequences → A crowdfunding platform failing to properly refund users can face liability.
Tools Used
Anchor Framework for Testing
Proof of Concept
it("Fails to process refunds correctly due to incorrect contribution tracking", async () => {
await program.methods
.contribute(new anchor.BN(100))
.accounts({ fund: fundPDA, contributor: contributor.publicKey, contribution: contributionPDA })
.signers([contributor])
.rpc();
let contribution = await program.account.contribution.fetch(contributionPDA);
expect(contribution.amount.toString()).to.equal("100");
await program.methods
.contribute(new anchor.BN(50))
.accounts({ fund: fundPDA, contributor: contributor.publicKey, contribution: contributionPDA })
.signers([contributor])
.rpc();
contribution = await program.account.contribution.fetch(contributionPDA);
expect(contribution.amount.toString()).to.equal("100");
await program.methods
.refund()
.accounts({ fund: fundPDA, contributor: contributor.publicKey, contribution: contributionPDA })
.signers([contributor])
.rpc();
const refundedBalance = await getContributorBalance(contributor.publicKey);
expect(refundedBalance).to.be.lessThan(originalBalance);
});
Recommendations
Modify contribute to update contribution.amount
before completing the transaction:
pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
let contribution = &mut ctx.accounts.contribution;
if contribution.contributor == Pubkey::default() {
contribution.contributor = ctx.accounts.contributor.key();
contribution.fund = fund.key();
contribution.amount = 0;
}
contribution.amount = contribution
.amount
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
}