H-01. Creators Can Withdraw Funds Without Meeting Campaign Goals
Severity: High
Category: Fund Management / Economic Security Violation
Summary
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)
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();
await program.methods.contribute(new anchor.BN(2 * LAMPORTS_PER_SOL))
.accounts({
fund,
contributor: contributor.publicKey,
contribution,
systemProgram: SystemProgram.programId,
})
.signers([contributor])
.rpc();
await program.methods.setDeadline(new anchor.BN(Math.floor(Date.now() / 1000) - 86400))
.accounts({ fund, creator: creator.publicKey })
.signers([creator])
.rpc();
await program.methods.withdraw()
.accounts({
fund,
creator: creator.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([creator])
.rpc();
Fund goal: 5 SOL
Contributed amount: 2 SOL
Withdrawal succeeded despite not meeting goal
Fund balance after withdrawal: 0.00089088 SOL (rent only)
*/
Tools Used
Recommendations
Add conditional logic to the withdraw
function to ensure the campaign has reached its fundraising goal before allowing withdrawals:
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:
#[error_code]
pub enum ErrorCode {
// existing errors...
+ #[msg("Campaign goal not met")]
+ GoalNotMet,
}