[H-01] Refund Won’t Work Because contribution.amount
Is Not Updated During contribute
Function
Summary
The contribute
function does not update contribution.amount
, which results in the refund function always using amount = 0
. This means that contributors will never be able to successfully withdraw their funds when requesting a refund.
Vulnerability Details
Below is the implementation of 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(())
}
Issue
The function never updates contribution.amount
when a user contributes.
Because contribution.amount
remains 0
, any future refund attempts will also try to refund 0
lamports.
This makes the refund function ineffective, as no actual funds will be transferred back to the contributor.
Impact
Refunds will always fail silently because the amount to be refunded is never set correctly. Even though contributors successfully send SOL to the fund, they will never be able to withdraw it back, leading to locked or lost funds.
Proof of Concept
Modify the existing test case to track the contributor and fund account balances before and after a refund attempt. The results will show no changes in balances, proving that the refund mechanism does not work.
it("Refunds contribution", async () => {
console.log("fundBalanceBefore", await provider.connection.getBalance(fundPDA));
await new Promise(resolve => setTimeout(resolve, 15000));
const contributorBalanceBefore = await provider.connection.getBalance(otherUser.publicKey);
console.log("contributorBalanceBefore", contributorBalanceBefore);
await program.methods
.refund()
.accounts({
fund: fundPDA,
contribution: contributionPDA,
contributor: otherUser.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
}).signers([otherUser])
.rpc();
const contributorBalanceAfter = await provider.connection.getBalance(otherUser.publicKey);
const contributionAccount = await program.account.contribution.fetch(contributionPDA);
console.log("fundBalanceAfter", await provider.connection.getBalance(fundPDA));
console.log("contributorBalanceAfter", contributorBalanceAfter);
});
Expected output:
fundBalanceBefore
and fundBalanceAfter
remain unchanged.
contributorBalanceBefore
and contributorBalanceAfter
remain the same.
contribution.amount
remains 0
.
Tools Used
Manual Review
Recommendations
Update contribution.amount
during the contribution process to ensure it accurately tracks the amount deposited.
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)?;
fund.amount_raised += amount;
+ contribution.amount += amount; // Fix: Track the amount contributed
Ok(())
}
This change ensures that contribution.amount
is updated correctly, allowing refunds to work as intended.