Rust Fund

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

Creator Can Withdraw Before Deadline and Without Meeting Goal

**Impact:** Critical — Creator can withdraw all contributed funds at any time, before deadline expires and without meeting funding goal. Contributors have no protection and can lose 100% of their SOL.
**Likelihood:** High — No barriers prevent immediate withdrawal after campaign creation.
**Reference Files:** `programs/rustfund/src/lib.rs``withdraw()` function, lines 90-105
---
## Description
The `withdraw()` function has **zero validation checks**. It transfers `amount_raised` from the fund PDA to the creator with no conditions. A creator can withdraw all contributed funds at any time — before the deadline expires, before the campaign ends, and even if the funding goal is not met. This completely breaks the crowdfunding trust model where funds should only be released to the creator after a successful campaign.
Vulnerable code:
```rust
// lib.rs:90-105
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// NO deadline check
// NO goal check
// NO success state check
**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(())
}
```
---
## Proof of Concept
```
Scenario: Creator rug-pulls immediately after creating campaign
1. Creator calls fund_create("Scam Fund", "Description", 100 SOL)
- Campaign created with goal = 100 SOL, deadline = 0
2. Creator calls set_deadline(future_timestamp + 7 days)
- Deadline set to 7 days from now
3. Contributors call contribute(10 SOL each)
- 5 contributors each send 10 SOL
- fund.amount_raised = 50 SOL
- Note: contribution.amount = 0 for each (due to separate bug)
4. Creator calls withdraw() IMMEDIATELY (day 1 of 7)
- No deadline check → passes
- No goal check → passes (50 < 100 goal not met)
- All 50 SOL transferred to creator
5. Contributors try to refund after deadline
- contribution.amount = 0 → refund returns 0 SOL
- Contributors lose all funds
```
**Attack scenario:**
1. Attacker creates campaign with attractive description
2. Sets deadline far in future to appear legitimate
3. Wait for contributors to deposit
4. Withdraw all funds immediately
5. Disappear — contributors cannot refund (due to bug #1)
---
## Recommended Mitigation
Add validation checks to `withdraw()`:
```rust
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &mut ctx.accounts.fund;
let amount = fund.amount_raised;
// CHECK 1: Deadline must have passed
let current_time = Clock::get().unwrap().unix_timestamp.try_into().unwrap();
if fund.deadline != 0 && fund.deadline > current_time {
return Err(ErrorCode::DeadlineNotReached.into());
}
// CHECK 2: Goal must be met
if fund.amount_raised < fund.goal {
return Err(ErrorCode::GoalNotMet.into());
}
**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(())
}
```
Add new error:
```rust
#[error_code]
pub enum ErrorCode {
// ... existing errors ...
#[msg("Funding goal not met")]
GoalNotMet,
}
```
---
## References
- [Solana Clock Sysvar](https://docs.solana.com/developing/runtime-facilities/sysvars#clock)
- [Crowdfunding Security Best Practices](https://consensys.github.io/smart-contract-best-practices/)
Updates

Lead Judging Commences

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