Summary
The withdraw()
function does not check if the funding goal was met or if the deadline has passed.
This allows the campaign creator to withdraw funds at any time, even before the campaign ends.
Vulnerability Details
Function logic fails to check if the funding goal was met or if the deadline has passed. The absence of this makes it possible for contributed funds to be withdrawn early which is against the rules.
Impact
I could think of two impacts:
1. Malicious fund creators can withdraw contributed funds early and disappear (rug pull).
2. Legitimate contributors are left without refunds, even if the campaign failed.
Tools Used
1. Manual Review
POC
Steps to Exploit
1. Create a campaign with goal = 10 SOL.
2. Contributors send only 5 SOL (goal not met).
3. Before the deadline, the creator calls withdraw() and takes 5 SOL.
4. The campaign fails, but the creator has already withdrawn the money.
Flaw in withdraw()
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
**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.creator.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.creator.to_account_info().lamports()
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
Ok(())
}
Recommendations
1. Require that the funding goal is met before withdrawal.
2. Ensure withdrawal is only possible after the deadline.
Fix:
if fund.deadline > Clock::get()?.unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::CampaignNotEnded.into());
}
if fund.amount_raised < fund.goal {
return Err(ErrorCode::FundingGoalNotMet.into());
}