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 4 months 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!