RustFund

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

Missing Authorized Withdrawal Check

Summary

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

Vulnerability Details

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.

Impact

This vulnerability has high severity because:

  • It affects a critical function that controls fund withdrawals

  • If the constraint validation is somehow bypassed, it could allow unauthorized access to funds

  • It violates the principle of defense in depth, relying on a single validation layer

POC

it("Lacks validation that only creator can withdraw (unauthorized withdrawal risk)", async () => {
// Create a new fund for testing unauthorized withdrawal
const unauthorizedWithdrawFundName = "Unauthorized Withdraw Test Fund";
const [unauthorizedWithdrawFundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(unauthorizedWithdrawFundName), creator.publicKey.toBuffer()],
program.programId
);
// Create the fund
await program.methods
.fundCreate(unauthorizedWithdrawFundName, description, goal)
.accounts({
fund: unauthorizedWithdrawFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// Set a deadline just 5 seconds in the future
const nearFutureDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 5);
await program.methods
.setDeadline(nearFutureDeadline)
.accounts({
fund: unauthorizedWithdrawFundPDA,
creator: creator.publicKey,
})
.rpc();
// Make a contribution to have funds to withdraw
const attackContribution = new anchor.BN(100000000); // 0.1 SOL
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 to withdraw from fund (should work since the deadline has passed)
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);
}
// Check contract source code for validation
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.`
);
});

Output:

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>,
}

While the Account struct has has_one = creator constraint, this is implicit and could be missed during code review.
Defense in depth would suggest adding an explicit runtime check in the function body.

## Tools Used
- Manual code review
## Recommendations
Add an explicit runtime check in the withdraw function to validate the creator:
```rust
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &ctx.accounts.fund;
let creator = &ctx.accounts.creator;
// Add explicit validation
require!(fund.creator == creator.key(), ErrorCode::UnauthorizedAccess);
let amount = fund.amount_raised;
// ... rest of function
}
Updates

Appeal created

bube Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

[Invalid] Lack of access control in `withdraw` function

There are enough security checks in `withdraw` function. Anchor enforces that creator must sign the transaction. And the `has_one = creator` ensures that the fund’s creator matches the provided creator account.

Support

FAQs

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