Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Severity: high
Valid

withdraw Has No Deadline or Goal Check — Creator Can Drain All Funds at Any Time

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

# Root + Impact
The protocol's core trust assumption is completely broken because the creator can drain every lamport from the fund immediately after the first contribution. Every contributor loses their entire deposit because the creator can steal all funds without restriction, offering zero protection to contributors.
## Description
The crowdfunding mechanism expects funds to be held until a goal is met and a deadline passes. Under normal operation, contributors expect their funds to remain safely locked within the contract during the fundraising phase.
The `withdraw` function transfers the full `amount_raised` to the creator with zero preconditions. There is no validation check ensuring that the project deadline has passed, no verification that the funding goal was met, and no restriction regarding when the function may be called.
```rust
// Root cause in the codebase: Missing access/state guards in the withdraw function
pub fn withdraw(ctx: Context<Withdraw>) -> Result<()> {
let fund = &mut ctx.accounts.fund;
// @> Missing check: clock.unix_timestamp > fund.deadline
// @> Missing check: fund.amount_raised >= fund.goal
// Transfers full amount to creator immediately
**ctx.accounts.creator.lamports.borrow_mut() += fund.amount_raised;
**ctx.accounts.fund.to_account_info().lamports.borrow_mut() -= fund.amount_raised;
Ok(())
}
```
## Risk
Likelihood: High
- The function requires no special setup or prerequisites to be called maliciously.
- A creator can voluntarily invoke this instruction at any stage of the timeline.
Impact: High
- Complete loss of funds for all users who contributed to the crowdfunding campaign.
## Proof of Concept
The following TypeScript test demonstrates that a creator can successfully execute a withdrawal right after a single contribution, bypassing any timeline protections:
```typescript
it("allows creator to withdraw at any time with no restrictions", async () => {
await program.methods.createFund(new anchor.BN(100 * LAMPORTS_PER_SOL))
.accounts({ creator: creator.publicKey, fund: fundPDA })
.signers([creator]).rpc();
await program.methods.contribute(new anchor.BN(50 * LAMPORTS_PER_SOL))
.accounts({ contributor: contributor.publicKey, fund: fundPDA })
.signers([creator, contributor]).rpc();
const before = await connection.getBalance(creator.publicKey);
await program.methods.withdraw()
.accounts({ creator: creator.publicKey, fund: fundPDA })
.signers([creator]).rpc();
const after = await connection.getBalance(creator.publicKey);
assert(after > before + 49 * LAMPORTS_PER_SOL);
assert((await connection.getBalance(fundPDA)) === 0);
});
```
## Recommended Mitigation
Add three explicit conditional state guards inside the withdrawal handler logic to enforce timeline rules:
```diff
+ require!(fund.deadline != 0, ErrorCode::DeadlineNotSet);
+ require!(Clock::get()?.unix_timestamp > fund.deadline, ErrorCode::DeadlineNotPassed);
+ require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotMet);
```
// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Impact 1

  • Impact 2

Proof of Concept

Recommended Mitigation

- remove this code
+ add this code
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 2 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-01] No check for if campaign reached deadline before withdraw

## Description A Malicious creator can withdraw funds before the campaign's deadline. ## Vulnerability Details There is no check in withdraw if the campaign ended before the creator can withdraw funds. ```Rust 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(()) } ``` ## Impact A Malicious creator can withdraw all the campaign funds before deadline which is against the intended logic of the program. ## Recommendations Add check for if campaign as reached deadline before a creator can withdraw ```Rust pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> { //add this if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() { return Err(ErrorCode::DeadlineNotReached.into()); } //stops here 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(()) } ``` ## POC keep everything in `./tests/rustfund.rs` up on to `Contribute to fund` test, then add the below: ```TypeScript it("Creator withdraws funds when deadline is not reached", async () => { const creatorBalanceBefore = await provider.connection.getBalance(creator.publicKey); const fund = await program.account.fund.fetch(fundPDA); await new Promise(resolve => setTimeout(resolve, 150)); //default 15000 console.log("goal", fund.goal.toNumber()); console.log("fundBalance", await provider.connection.getBalance(fundPDA)); console.log("creatorBalanceBefore", await provider.connection.getBalance(creator.publicKey)); await program.methods .withdraw() .accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .rpc(); const creatorBalanceAfter = await provider.connection.getBalance(creator.publicKey); console.log("creatorBalanceAfter", creatorBalanceAfter); console.log("fundBalanceAfter", await provider.connection.getBalance(fundPDA)); }); ``` this outputs: ```Python goal 1000000000 fundBalance 537590960 creatorBalanceBefore 499999999460946370 creatorBalanceAfter 499999999960941400 fundBalanceAfter 37590960 ✔ Creator withdraws funds when deadline is not reached (398ms) ``` We can notice that the creator withdraws funds from the campaign before the deadline.

Support

FAQs

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

Give us feedback!