Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Withdraw Has No Goal or Deadline Validation — Creator Can Rug-Pull All Contributed SOL

Withdraw Has No Goal or Deadline Validation — Creator Can Rug-Pull All Contributed SOL

Description

  • The RustFund crowdfunding model is designed so that the creator can only withdraw contributed SOL after the campaign succeeds — meaning the funding goal has been reached and the deadline has passed. This is the fundamental trust guarantee that protects contributors.

  • The withdraw() function allows the creator to transfer fund.amount_raised lamports to their own wallet without any validation whatsoever. There is no check that the funding goal was met, no check that the deadline has passed, and no check on campaign status. The creator can drain all contributed funds immediately.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
@> let amount = ctx.accounts.fund.amount_raised;
@> // ❌ No check: amount_raised >= goal
@> // ❌ No check: deadline has passed
**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(())
}

Risk

Likelihood: High

  • The creator is the sole authorized caller of withdraw() (enforced by has_one = creator). No on-chain validation occurs on goal achievement or deadline status — the function body is a straight transfer with zero guards.

  • Combined with H-01 (refunds always return 0), the creator faces zero consequences — contributors have no recourse and no recovery mechanism.

Impact: High

  • All contributed SOL is transferred to the creator in a single transaction. This is a direct rug-pull vector enabling complete theft of contributor funds.

  • The platform's core crowdfunding invariant is violated: contributors who deposited SOL trusting the refund mechanism lose 100% of their funds with no on-chain path to recovery.

Severity: High

Proof of Concept

A creator creates a fund with a 100 SOL goal and a deadline 1 year away. A contributor deposits 5 SOL — only 5% of the goal. The creator immediately calls withdraw() and receives all 5 SOL.

it("Proves creator can withdraw before goal met and before deadline", async () => {
await program.methods.fundCreate("poc-h003", "rug pull", goal100).rpc();
await program.methods.setDeadline(deadline1y).rpc();
await program.methods.contribute(new anchor.BN(5 * LAMPORTS_PER_SOL)).signers([contributor]).rpc();
const balanceBefore = await provider.connection.getBalance(creator);
await program.methods.withdraw().rpc();
const balanceAfter = await provider.connection.getBalance(creator);
expect(balanceAfter).to.be.greaterThan(balanceBefore); // Rug pull successful
});

Recommended Mitigation

Add goal achievement and deadline passage checks before allowing withdrawal.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
+ let fund = &ctx.accounts.fund;
+ require!(
+ fund.amount_raised >= fund.goal,
+ ErrorCode::GoalNotReached
+ );
+ require!(
+ fund.deadline != 0 && fund.deadline < Clock::get()?.unix_timestamp as u64,
+ ErrorCode::DeadlineNotReached
+ );
let amount = ctx.accounts.fund.amount_raised;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 3 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!