Missing Access-Control Checks in withdraw() Allow Creator to Drain Funds Before Deadline or Goal Completion (Rug Pull)
ชื่อเรื่อง
withdraw() lacks deadline and goal checks, allowing immediate fund drainage by campaign creator
Impact: High
Likelihood: High
ขอบเขต
src/lib.rs — withdraw instruction handler and FundWithdraw accounts struct
คำอธิบาย
Normal behavior:
RustFund is a crowdfunding platform where contributors send SOL to a Fund account trusting that funds will only be released to the creator once the campaign has legitimately concluded (i.e., after the deadline has passed and/or the funding goal has been met). This is the core trust assumption that distinguishes a crowdfunding contract from a simple wallet.
The issue:
The withdraw instruction transfers the entire fund.amount_raised balance to the creator with no validation of fund.deadline and no validation of fund.goal:Root + Impact
คำอธิบาย
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(())
}
The FundWithdraw account struct only enforces has_one = creator (i.e., only the creator can call it) — it never touches deadline or goal at all:Risk
ความเป็นไปได้:
ผลกระทบ:
Proof of Concept
#[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>,
}
As a result, the creator can call withdraw() the instant a single lamport of contribution has landed in the fund — seconds after fund_create, with the deadline still far in the future and the goal nowhere near met.
Risk
Likelihood:
Occurs on every campaign where the creator calls withdraw() before the funding period ends, which requires no special conditions, no race, and no privileged state — it is reachable on the very first successful contribute() call.
Occurs regardless of whether set_deadline() was ever called, since deadline == 0 is treated the same as "no restriction" everywhere in withdraw().
Impact:
Total, unrecoverable loss of contributor funds: the creator can withdraw 100% of amount_raised at any time, including immediately after the first contribution, before the goal is reached and before the deadline passes.
Breaks the core trust/escrow guarantee that defines a crowdfunding contract — contributors have no on-chain assurance their funds will only move to the creator upon a successful, completed campaign.
Combined with the separate bug where contribute() never increments contribution.amount, contributors additionally have no way to reclaim funds via refund() after the creator drains the fund, making the loss permanent.
Proof of Concept
// 1. Creator calls fund_create("Trip", "desc", goal=1000 SOL)
// -> fund.deadline = 0, fund.amount_raised = 0
// 2. Attacker/contributor calls contribute(amount=1 SOL)
// -> fund.amount_raised = 1 SOL
// -> system_program::transfer moves 1 SOL from contributor into fund account
// 3. Creator immediately calls withdraw()
// -> ctx.accounts.fund.amount_raised == 1 SOL (0.1% of goal, deadline never set/reached)
// -> No check blocks this call
// -> 1 SOL lamports moved from fund account to creator
// -> Campaign has neither met its goal nor reached any deadline