Rust Fund

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

Immediate Rug Pull via Unrestricted Withdrawal

Root + Impact

Description

  • Normal behavior for a crowdfunding platform dictates that funds can only be withdrawn by the creator after the funding goal has been met and the campaign is successful.

  • The specific issue is that the withdraw function lacks any verification of the campaign's success (goal met) or status, allowing the creator to withdraw funds immediately after they are deposited.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// @> No check for fund.amount_raised >= fund.goal
// @> No check for deadline passed (optional, but standard)
**ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.fund.to_account_info().lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;

Risk

Likelihood:

  • This will occur whenever a malicious or impatient creator decides to call withdraw() before the goal is met.

  • It requires no special privileges other than being the creator.

Impact:

  • The Creator can steal all contributed funds immediately (Rug Pull).

  • The "Refund" guarantee becomes impossible to fulfill because the vault is empty.

Proof of Concept

This test simulates a malicious creator scenario. First, a fund is created with a high goal that is clearly not met. A user contributes funds, and the creator immediately invokes withdraw(). The test verifies that the withdrawal succeeds and drains the vault to near zero effectively bypassing all "goal" protections.

it("Allows rug pull before goal met", async () => {
// 1. Create fund with 10 SOL goal
const goal = new anchor.BN(10000000000);
await program.methods.fundCreate("Rug", "Desc", goal).accounts({...}).rpc();
// 2. User contributes 1 SOL (Goal NOT met)
await program.methods.contribute(new anchor.BN(1000000000)).accounts({...}).rpc();
// 3. Creator withdraws immediately
await program.methods.withdraw().accounts({...}).rpc();
// 4. Verify vault is empty
const balance = await provider.connection.getBalance(fundPDA);
expect(balance).to.be.lt(1000000000); // Should be near 0
});

Recommended Mitigation

We introduce a check to enforce that withdrawals are only permitted if the amount_raised is greater than or equal to the goal. This locks the funds in the contract until the campaign is successfully funded.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
+ let fund = &ctx.accounts.fund;
+ if fund.amount_raised < fund.goal {
+ return Err(ErrorCode::GoalNotMet.into());
+ }
let amount = ctx.accounts.fund.amount_raised;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 10 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-02] H-01. Creators Can Withdraw Funds Without Meeting Campaign Goals

# H-01. Creators Can Withdraw Funds Without Meeting Campaign Goals **Severity:** High\ **Category:** Fund Management / Economic Security Violation ## Description The `withdraw` function in the RustFund contract allows creators to prematurely withdraw funds without verifying if the campaign goal was successfully met. ## Vulnerability Details In the current RustFund implementation (`lib.rs`), the `withdraw` instruction lacks logic to verify that the campaign's `amount_raised` is equal to or greater than the `goal`. Consequently, creators can freely withdraw user-contributed funds even when fundraising objectives haven't been met, undermining the core economic guarantees of the platform. **Vulnerable Component:** - File: `lib.rs` - Function: `withdraw` - Struct: `Fund` ## Impact - Creators can prematurely drain user-contributed funds. - Contributors permanently lose the ability to receive refunds if the creator withdraws early. - Severely damages user trust and undermines the economic integrity of the RustFund platform. ## Proof of Concept (PoC) ```js // Create fund with 5 SOL goal await program.methods .fundCreate(FUND_NAME, "Test fund", new anchor.BN(5 * LAMPORTS_PER_SOL)) .accounts({ fund, creator: creator.publicKey, systemProgram: SystemProgram.programId, }) .signers([creator]) .rpc(); // Contribute only 2 SOL (below goal) await program.methods .contribute(new anchor.BN(2 * LAMPORTS_PER_SOL)) .accounts({ fund, contributor: contributor.publicKey, contribution, systemProgram: SystemProgram.programId, }) .signers([contributor]) .rpc(); // Set deadline to past await program.methods .setDeadline(new anchor.BN(Math.floor(Date.now() / 1000) - 86400)) .accounts({ fund, creator: creator.publicKey }) .signers([creator]) .rpc(); // Attempt withdrawal (should fail but succeeds) await program.methods .withdraw() .accounts({ fund, creator: creator.publicKey, systemProgram: SystemProgram.programId, }) .signers([creator]) .rpc(); /* OUTPUT: Fund goal: 5 SOL Contributed amount: 2 SOL Withdrawal succeeded despite not meeting goal Fund balance after withdrawal: 0.00089088 SOL (rent only) */ ``` ## Recommendations Add conditional logic to the `withdraw` function to ensure the campaign has reached its fundraising goal before allowing withdrawals: ```diff pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> { let fund = &mut ctx.accounts.fund; + require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotMet); let amount = 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(()) } ``` Also define the new error clearly: ```diff #[error_code] pub enum ErrorCode { // existing errors... + #[msg("Campaign goal not met")] + GoalNotMet, } ```

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!