Rust Fund

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

Creator can withdraw all funds at any time without goal or deadline checks

Root + Impact

Description

- Normal behavior: The creator should only be able to withdraw the raised funds after the campaign deadline has passed and the funding goal has been reached.

- Issue: The `withdraw` function allows the creator to drain the entire fund balance at any time with no validation of goal or deadline.


```rust

// @> Root cause - missing goal and deadline checks

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(())

}

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Creator can call withdraw immediately after any contribution is received

  • No time-lock, no goal check and no state validation exists in the function

Impact:

  • 100% of all contributor funds can be stolen by the creator

  • Complete loss of funds for every contributor of the campaign

Proof of Concept

---
```typescript
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Rustfund } from "../target/types/rustfund";
import { expect } from "chai";
import { Keypair, SystemProgram, LAMPORTS_PER_SOL } from "@solana/web3.js";
describe("Unrestricted Withdraw", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Rustfund as Program<Rustfund>;
it("Creator drains all funds without goal/deadline checks", async () => {
const creator = provider.wallet;
const contributor = Keypair.generate();
await provider.connection.requestAirdrop(contributor.publicKey, 5 * LAMPORTS_PER_SOL);
await new Promise(r => setTimeout(r, 1000));
const name = "steal-fund";
const [fundPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from(name), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(name, "desc", new anchor.BN(1 * LAMPORTS_PER_SOL))
.accounts({
fund: fundPda,
creator: creator.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
const [contribPda] = anchor.web3.PublicKey.findProgramAddressSync(
[fundPda.toBuffer(), contributor.publicKey.toBuffer()],
program.programId
);
await program.methods
.contribute(new anchor.BN(2 * LAMPORTS_PER_SOL))
.accounts({
fund: fundPda,
contributor: contributor.publicKey,
contribution: contribPda,
systemProgram: SystemProgram.programId,
})
.signers([contributor])
.rpc();
const creatorBalBefore = await provider.connection.getBalance(creator.publicKey);
await program.methods
.withdraw()
.accounts({
fund: fundPda,
creator: creator.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
const creatorBalAfter = await provider.connection.getBalance(creator.publicKey);
expect(creatorBalAfter - creatorBalBefore).to.be.approximately(2 * LAMPORTS_PER_SOL, 10000);
});
});

Recommended Mitigation

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
+ let fund = &mut ctx.accounts.fund;
+ let clock = Clock::get()?;
+
+ require!(fund.dealine_set, ErrorCode::DeadlineNotSet);
+ require!((clock.unix_timestamp as u64) >= fund.deadline, ErrorCode::DeadlineNotReached);
+ require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotReached);
+
let amount = fund.amount_raised;
+ require!(amount > 0, ErrorCode::NothingToWithdraw);
**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)?;
+ fund.amount_raised = 0;
Ok(())
}
Updates

Lead Judging Commences

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