RustFund

First Flight #36
Beginner FriendlyRust
100 EXP
View results
Submission Details
Severity: high
Valid

Critical Vulnerabilities in Crowdfunding Fund Withdrawal

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:

// Exploit: Creator withdraws funds before goal is met
await program.methods
.withdraw()
.accounts({ fund: fundPDA, creator: creator.publicKey })
.signers([creator])
.rpc();
// ❌ Bug: Fund withdrawal was allowed despite fundraising failure.

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:

  • Loss of funds due to unsafe manual balance handling.

  • Risk of bricking the contract if an inconsistent state occurs.

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;
// ✅ Ensure the goal was met before allowing withdrawal
if fund.amount_raised < fund.goal {
return Err(ErrorCode::GoalNotMet.into());
}
let amount = fund.amount_raised;
// ✅ Prevent integer underflow
if ctx.accounts.fund.to_account_info().lamports() < amount {
return Err(ProgramError::InsufficientFunds.into());
}
// ✅ Secure transfer using system instruction
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(())
}
Updates

Appeal created

bube Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

No goal achievement check in `withdraw` function

Unsafe direct lamport manipulation

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.