Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

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.

  • Campaign creators requesting withdrawals prior to the deadline being reached.

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
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!