Rust Fund

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

Creator Can Rug Pull Funds Before Goal Met or Deadline Passed

Severity Impact Likelihood
CRITICAL High High

Scope: programs/rustfund/src/lib.rs:90-105

Description

In a legitimate crowdfunding platform, creators should only be able to withdraw funds after the campaign succeeds - meaning the funding goal is met and the deadline has passed. This protects contributors from creators who might take the money and run.

The withdraw() function allows the creator to withdraw all raised funds without any validation that the goal was met or the deadline passed. A malicious creator can immediately drain all contributions the moment they arrive.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// @> BUG: No check for fund.amount_raised >= fund.goal
// @> BUG: No check for Clock::get() > fund.deadline
**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(())
}

Risk

Likelihood: High

  • Any creator can call withdraw() at any time after receiving contributions

  • No special conditions or timing required - exploit is always available

  • Malicious actors will specifically target this for guaranteed profit

Impact: High

  • Complete theft of all contributed funds

  • Contributors have no recourse - funds are gone before deadline

  • Destroys trust in the entire platform

  • Classic "rug pull" attack vector

Proof of Concept

it("Creator withdraws immediately after contribution - before deadline, before goal met", async () => {
// Create fund with 10 SOL goal, 1 hour deadline
await program.methods
.fundCreate(fundName, "Rug pull test", new anchor.BN(10 * LAMPORTS_PER_SOL))
.accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: SystemProgram.programId })
.signers([creator])
.rpc();
// Set deadline 1 hour in future
await program.methods
.setDeadline(new anchor.BN(Math.floor(Date.now() / 1000) + 3600))
.accounts({ fund: fundPDA, creator: creator.publicKey })
.signers([creator])
.rpc();
// Contributor adds 2 SOL (goal is 10 SOL - NOT MET)
await program.methods
.contribute(new anchor.BN(2 * LAMPORTS_PER_SOL))
.accounts({ fund: fundPDA, contributor: contributor.publicKey, contribution: contributionPDA, systemProgram: SystemProgram.programId })
.signers([contributor])
.rpc();
const creatorBalanceBefore = await provider.connection.getBalance(creator.publicKey);
// EXPLOIT: Creator withdraws despite goal not met and deadline not passed
await program.methods
.withdraw()
.accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: SystemProgram.programId })
.signers([creator])
.rpc();
const creatorBalanceAfter = await provider.connection.getBalance(creator.publicKey);
const stolen = creatorBalanceAfter - creatorBalanceBefore;
// Creator stole ~2 SOL
expect(stolen).to.be.greaterThan(1.9 * LAMPORTS_PER_SOL);
});

Recommended Mitigation

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &ctx.accounts.fund;
let amount = fund.amount_raised;
+ // Ensure deadline has passed
+ let current_time: u64 = Clock::get()?.unix_timestamp.try_into().unwrap();
+ require!(
+ fund.deadline != 0 && current_time > fund.deadline,
+ ErrorCode::DeadlineNotReached
+ );
+
+ // Ensure goal was met
+ require!(
+ fund.amount_raised >= fund.goal,
+ ErrorCode::GoalNotMet
+ );
**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(())
}

Also add the new error code:

#[error_code]
pub enum ErrorCode {
// ... existing errors ...
+ #[msg("Goal not met")]
+ GoalNotMet,
}
Updates

Lead Judging Commences

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