Summary
The withdraw
function contains multiple vulnerabilities that can lead to fund loss, unauthorized withdrawals, and potential contract bricking. The function fails to check if the funding goal was met, allows fund overflows or underflows, and improperly manipulates lamports using direct assignment instead of a proper transfer method.
Vulnerabilities' Details
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(())
}
1️⃣Unchecked Withdrawal Before Goal Is Met (Missing Fundraising Goal Check):
The function does not check whether the funding goal has been met before allowing withdrawal. This allows the creator to withdraw all funds even if the fundraising target was not reached, which contradicts the expected refund logic.
Impact
Contributors lose their money even if the funding goal was never met.
Violates crowdfunding logic, as funds should only be withdrawn if the goal is achieved.
PoC Exploit:
await program.methods
.withdraw()
.accounts({ fund: fundPDA, creator: creator.publicKey })
.signers([creator])
.rpc();
Recommendations
Modify withdraw to only allow withdrawals if the goal was met:
if fund.amount_raised < fund.goal {
return Err(ErrorCode::GoalNotMet.into());
}
2️⃣ Unauthorized Fund Manipulation via Direct Lamport Assignment
**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)?;
However, direct lamport manipulation is unsafe because:
Bypassing invoke_signed leads to bypassing runtime security checks.
If an error occurs mid-transfer, funds may be stuck.
This violates Solana's recommended account transfer patterns.
Impact:
Recommendation:
Replace direct assignment with invoke_signed for safe transfers:
let transfer_instruction = system_instruction::transfer(
&ctx.accounts.fund.to_account_info().key,
&ctx.accounts.creator.to_account_info().key,
amount,
);
invoke_signed(
&transfer_instruction,
&[
ctx.accounts.fund.to_account_info(),
ctx.accounts.creator.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
&[&[fund.name.as_bytes(), creator.key().as_ref(), &[ctx.bumps.fund]]],
)
3️⃣ Integer Underflow When Withdrawing All Funds:
The function subtracts the entire amount_raised from fund.lamports():
ctx.accounts.fund.to_account_info().lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;
If fund.amount_raised is greater than fund.lamports(), this will cause integer underflow, bricking the contract.
Impact:
Contract bricking → If underflow occurs, withdraw will always revert.
Potential DoS attack vector, preventing fund retrieval.
Recommendation:
Use a pre-check to ensure enough lamports exist before subtraction:
if ctx.accounts.fund.to_account_info().lamports() < amount {
return Err(ProgramError::InsufficientFunds.into());
}
Final Comprehensive Secured Code:
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &mut ctx.accounts.fund;
let creator = &mut ctx.accounts.creator;
if fund.amount_raised < fund.goal {
return Err(ErrorCode::GoalNotMet.into());
}
let amount = fund.amount_raised;
if ctx.accounts.fund.to_account_info().lamports() < amount {
return Err(ProgramError::InsufficientFunds.into());
}
let transfer_instruction = system_instruction::transfer(
&ctx.accounts.fund.to_account_info().key,
&ctx.accounts.creator.to_account_info().key,
amount,
);
invoke_signed(
&transfer_instruction,
&[
ctx.accounts.fund.to_account_info(),
ctx.accounts.creator.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
&[&[fund.name.as_bytes(), creator.key().as_ref(), &[ctx.bumps.fund]]],
)?;
Ok(())
}