Rust Fund

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

No deadline or goal check in withdraw()

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

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

//Missing deadline and goal validation in withdraw() allows creator to drain funds immediatelyDescription:The withdraw() instruction (lib.rs:90-105) only verifies has_one = creator on the fund account. It never checks:Whether the deadline has passed.Whether the fundraising goal has been met (fund.amount_raised >= fund.goal).Whether a deadline has even been set.Combined with fund_create initializing deadline = 0 and contribute skipping the deadline check when deadline == 0 (line 29: if fund.deadline != 0 && fund.deadline < Clock::get()...), this creates an instant drain path:Creator calls fund_create(name, desc, goal=100 SOL).Contributors send SOL via contribute().Creator immediately calls withdraw() which drains all raised funds.The withdraw() function performs a raw lamport transfer without any business logic validation.Impact:The creator can steal all contributed funds immediately after a campaign starts.Contributors have no protection if the goal is not met or the deadline is not reached.This results in a complete loss of trust in the platform.Proof of Concept:rust// 1. Create fund with no deadline (deadline = 0)
fund_create(name="rug", description="trust me", goal=100 * LAMPORTS_PER_SOL)
// 2. Victim contributes
contribute(fund, amount=10 * LAMPORTS_PER_SOL)
// 3. Creator drains immediately (no deadline/goal check)
withdraw(fund, creator)
// Fund.amount_raised == 10 SOL -> creator gets 10 SOL
// Goal was 100 SOL, but withdraw doesn't check!
Use code with caution.See lib.rs:90-105:rustpub fn withdraw

Risk

Likelihood:

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

  • Reason 2

Impact:

  • Impact 1

  • Impact 2

Proof of Concept

# Finding 1: Missing deadline and goal validation in withdraw() allows creator to drain funds immediately
**Severity:** High
**Title:** Missing deadline and goal validation in withdraw() allows creator to drain funds immediately
## Description
The `withdraw()` instruction (`lib.rs:90-105`) checks the `has_one = creator` constraint but lacks business logic validation. It fails to verify if the campaign deadline has passed, if the target goal was met, or if a deadline was even set. Because a new campaign initializes with `deadline = 0`, a creator can instantly call `withdraw()` and drain user contributions without meeting any project parameters.
## Impact
Malicious creators can rug-pull and drain all contributed SOL immediately after a campaign begins, leaving contributors with zero protection or recourse.
## Proof of Concept
This execution trace shows how an attacker exploits the un-set deadline logic to drain user funds immediately:
1. Creator initializes a campaign with a 100 SOL goal (`deadline` defaults to 0).
2. A victim contributes 10 SOL; the deadline check is skipped because `deadline == 0`.
3. Creator calls `withdraw()` and successfully extracts the 10 SOL immediately.
```rust
// 1. Initialized with zero deadline
fund_create(name="RugCampaign", goal=100 * LAMPORTS_PER_SOL)
// 2. Victim contributes (bypasses check because deadline == 0)
contribute(fund, amount=10 * LAMPORTS_PER_SOL)
// 3. Creator drains contract immediately
withdraw(fund, creator) // Drains 10 SOL without checking goal
```
## Recommended Mitigation
This mitigation adds structural require statements to enforce that the campaign has ended and its financial goals are met before allowing withdrawals.
Add the following validation requirements inside the `withdraw` function body:
```rust
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &ctx.accounts.fund;
// Enforce that a timeline was configured
require!(fund.deadline != 0, ErrorCode::DeadlineNotSet);
// Enforce that the campaign duration has ended
require!(
Clock::get().unwrap().unix_timestamp as u64 >= fund.deadline,
ErrorCode::DeadlineNotReached
);
// Enforce that the fundraising goal was met
require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotMet);
// Proceed with existing transfer logic...
Ok(())
}
```

Recommended Mitigation

- remove this code
+ add this code
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 9 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!