[M-01] The refund instructions don’t check whether the fundraising goal has been reached
Summary
Contributors can refund their contributions even when the fundraising goal has been reached, despite the documentation stating otherwise:
Can request refunds if the campaign **fails to meet the goal** and the deadline is reached.
Vulnerability Details
Below is the implementation of the refund
function. As seen in the code, there is no check on whether the fundraising goal (fund.goal
) has been met.
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(())
}
Currently, the refund function only checks if the campaign’s deadline has passed but does not check if the fundraising goal has been met. This means that even if a campaign reaches its goal, contributors can still refund their contributions, effectively withdrawing funds that should have been locked for the project.
Impact
Contributors can refund their contributions at any time, even after the campaign has successfully reached its goal. This could allow contributors to withdraw their funds post-success, potentially draining the fund and leading to unintended consequences. This violates the expected behavior of the contract, where funds should remain locked once the goal is achieved.
Tools Used
Manual Review
Recommendations
Modify the refund
function to include a check that ensures refunds are only allowed if the campaign has failed (i.e., fund.goal <= fund.raised_amount
). This will prevent contributors from withdrawing their funds once the goal has been met.
pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let amount = ctx.accounts.contribution.amount;
// Check if the deadline has been reached
if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::DeadlineNotReached.into());
}
+ if ctx.accounts.fund.goal <= ctx.accounts.fund.raised_amount {
+ return Err(ErrorCode::GoalHasBeenReached.into())
+ }
// Perform the refund operation
**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(())
}