The Rustfund smart contract contains a High severity vulnerability where the withdraw function relies only on account constraints for creator validation without explicit runtime checks, potentially allowing unauthorized access to funds if constraint validation is bypassed
In the withdraw() function, the contract relies entirely on the Anchor account constraints for creator validation:
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(())
}
#[derive(Accounts)]
pub struct FundWithdraw<'info> {
#[account(mut, seeds = [fund.name.as_bytes(), creator.key().as_ref()], bump, has_one = creator)]
pub fund: Account<'info, Fund>,
#[account(mut)]
pub creator: Signer<'info>,
pub system_program: Program<'info, System>,
}
The has_one = creator constraint in the Accounts struct provides validation, but there's no explicit check in the function body. While Anchor's validation is generally reliable, defense in depth would suggest an explicit runtime check as well.
it("Lacks validation that only creator can withdraw (unauthorized withdrawal risk)", async () => {
const unauthorizedWithdrawFundName = "Unauthorized Withdraw Test Fund";
const [unauthorizedWithdrawFundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(unauthorizedWithdrawFundName), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(unauthorizedWithdrawFundName, description, goal)
.accounts({
fund: unauthorizedWithdrawFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const nearFutureDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 5);
await program.methods
.setDeadline(nearFutureDeadline)
.accounts({
fund: unauthorizedWithdrawFundPDA,
creator: creator.publicKey,
})
.rpc();
const attackContribution = new anchor.BN(100000000);
const [attackContribPDA] = await PublicKey.findProgramAddress(
[
unauthorizedWithdrawFundPDA.toBuffer(),
provider.wallet.publicKey.toBuffer(),
],
program.programId
);
await program.methods
.contribute(attackContribution)
.accounts({
fund: unauthorizedWithdrawFundPDA,
contributor: provider.wallet.publicKey,
contribution: attackContribPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Waiting for deadline to pass (6 seconds)...");
await new Promise((resolve) => setTimeout(resolve, 6000));
try {
await program.methods
.withdraw()
.accounts({
fund: unauthorizedWithdrawFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Withdrawal succeeded as expected with passed deadline");
} catch (e) {
console.log("Withdrawal failed despite passed deadline:", e);
}
const fund = await program.account.fund.fetch(unauthorizedWithdrawFundPDA);
const fundLamports = await provider.connection.getBalance(
unauthorizedWithdrawFundPDA
);
reportBug(
"HIGH",
"Missing Authorized Withdrawal Check",
"The withdraw function relies only on account constraints for creator validation, lacking explicit checks",
`Fund created by: ${fund.creator.toBase58()}\n` +
`Fund contains: ${fundLamports} lamports\n\n` +
`Code analysis: In the withdraw() function (lib.rs:108-127), there is no explicit check that confirms\n` +
`the creator matches fund.creator. Instead, it relies solely on the Accounts struct validation:\n\n` +
`\`\`\`rust\n` +
`#[derive(Accounts)]\n` +
`pub struct FundWithdraw<'info> {\n` +
` #[account(mut, seeds = [fund.name.as_bytes(), creator.key().as_ref()], bump, has_one = creator)]\n` +
` pub fund: Account<'info, Fund>,\n` +
` #[account(mut)]\n` +
` pub creator: Signer<'info>,\n` +
` pub system_program: Program<'info, System>,\n` +
`}\n` +
`\`\`\`\n\n` +
`While the Account struct has \`has_one = creator\` constraint, this is implicit and could be missed during code review.\n` +
`Defense in depth would suggest adding an explicit runtime check in the function body.`
);
});
Waiting for deadline to pass (6 seconds)...
Withdrawal succeeded as expected with passed deadline
========================================
🐛 BUG REPORT [HIGH]: Missing Authorized Withdrawal Check
----------------------------------------
Description: The withdraw function relies only on account constraints for creator validation, lacking explicit checks
Evidence: Fund created by: AWavBTYY3KVRwZcRrPfFUArFKg3eSoQjMQbuqsaKD8wC
Fund contains: 1890880 lamports
Code analysis: In the withdraw() function (lib.rs:108-127), there is no explicit check that confirms
the creator matches fund.creator. Instead, it relies solely on the Accounts struct validation:
```rust
#[derive(Accounts)]
pub struct FundWithdraw<'info> {
#[account(mut, seeds = [fund.name.as_bytes(), creator.key().as_ref()], bump, has_one = creator)]
pub fund: Account<'info, Fund>,
#[account(mut)]
pub creator: Signer<'info>,
pub system_program: Program<'info, System>,
}