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 at any time

Root + Impact

Description

The spec says "Creators can withdraw funds once their campaign succeeds." This implies the goal must be met and the campaign period must be over. The withdraw() function enforces neither. It lets the creator take all raised SOL immediately after the first contribution, no matter how far the campaign is from its goal or deadline. This is a direct rug-pull vector.

// programs/rustfund/src/lib.rs
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)?;
// @> no check: amount_raised >= goal
// @> no check: deadline has passed
Ok(())
}

Risk

Likelihood:
Any campaign creator can do this. It's just a single function call with no barriers.

Impact:
All contributor funds can be stolen instantly. Combined with H-01 (refunds are broken), contributors have zero recourse — their SOL is gone permanently. This completely destroys the trust model of the platform.


Proof of Concept

The creator simply calls withdraw() whenever they want. There are no checks for campaign success, no time restrictions, nothing.

it("H-03: creator withdraws before goal or deadline", async () => {
// Create fund: goal = 1000 SOL, deadline not yet set
const bigGoal = new anchor.BN(1000_000_000_000); // 1000 SOL
await program.methods
.fundCreate(fundName, description, bigGoal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// Someone contributes 0.5 SOL — far from the 1000 SOL goal
const [contributionPDA] = await PublicKey.findProgramAddress(
[fundPDA.toBuffer(), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.contribute(new anchor.BN(500000000))
.accounts({
fund: fundPDA,
contributor: creator.publicKey,
contribution: contributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// Creator immediately withdraws — no goal check, no deadline check
const balanceBefore = await provider.connection.getBalance(creator.publicKey);
await program.methods
.withdraw()
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc(); // should revert, but it succeeds — rug pull complete
const balanceAfter = await provider.connection.getBalance(creator.publicKey);
expect(balanceAfter).to.be.greaterThan(balanceBefore);
});

Recommended Mitigation

Add goal and deadline validation before allowing withdrawal. The creator should only be able to withdraw after the deadline has passed and only if the campaign reached its goal. Also reset amount_raised to prevent double-withdrawal (see M-01).

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
+ let fund = &ctx.accounts.fund;
+ let now: u64 = Clock::get().unwrap().unix_timestamp.try_into().unwrap();
+
+ if fund.deadline != 0 && fund.deadline > now {
+ return Err(ErrorCode::DeadlineNotReached.into());
+ }
+ if fund.amount_raised < fund.goal {
+ return Err(ErrorCode::GoalNotMet.into());
+ }
+
let amount = ctx.accounts.fund.amount_raised;
// ... transfer logic ...
+ ctx.accounts.fund.amount_raised = 0;
Ok(())
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours 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!