Rust Fund

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

006_MEDIUM_fund-creation-no-goal-validation

Description

When initializing a new crowdfunding campaign via the fund_create instruction, the program accepts a goal parameter of type u64. Critically, the program fails to validate that this goal is strictly positive (> 0).

A funding goal of 0 is fundamentally contradictory to the concept of a crowdfunding campaign. If the goal is zero:

  1. Logic Breakdown: The state of "Funding Reached" becomes true immediately upon creation (amount_raised (0) >= goal (0)).

  2. Instant Withdrawals: If the withdrawal logic relies on amount_raised >= goal, a malicious creator can create a campaign, convince users (via social engineering) to deposit funds, and then immediately withdraw those funds without ever having to achieve a legitimate target.

  3. User Deception: It creates a deceptive campaign state that may confuse analytics tools or indexers that expect a non-zero denominator for progress bars.

Risk

  • Severity: Medium

  • Likelihood: Medium

  • Impact: Medium

Impact Details:

  1. Immediate Rug Pull Risk: The mechanism designed to protect donors (holding funds until a goal is met) is nullified. The campaign is essentially a direct transfer wallet disguised as a crowdfunding contract.

  2. Protocol Integrity: The business logic of the application is violated.

  3. DoS/Griefing: Malformed campaigns clog the network state.

Proof of Concept

The following solana-program-test code proves that a campaign can be initialized with a goal of 0 and subsequently behave as if the funding goal is met immediately.

#![cfg(feature = "test-sbf")]
mod test_utils;
use solana_program_test::*;
use solana_sdk::{
signature::{Keypair, Signer},
transaction::Transaction,
pubkey::Pubkey,
instruction::Instruction,
};
#[tokio::test]
async fn test_vulnerability_zero_goal_initialization() {
// 1. Initialize Test Environment
let program_id = Pubkey::new_unique();
let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
"crowdfunding_program",
program_id,
processor!(process_instruction),
)
.start()
.await;
// 2. Setup Accounts for Campaign Creation
let campaign_keypair = Keypair::new();
let creator = Keypair::new();
// 3. Construct the Malicious Instruction with Goal = 0
// Arguments: [goal: u64, ... other args]
let zero_goal: u64 = 0;
let create_ix = instruction::fund_create(
&program_id,
zero_goal, // <--- VULNERABILITY
&campaign_keypair.pubkey(),
&creator.pubkey(),
);
let mut transaction = Transaction::new_with_payer(
&[create_ix],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &campaign_keypair, &creator], recent_blockhash);
// 4. Process Transaction
let result = banks_client.process_transaction(transaction).await;
// 5. Assert Vulnerability
assert!(
result.is_ok(),
"CRITICAL: Program allowed creation of a campaign with 0 goal."
);
// 6. Verify State
// Ideally, we would fetch the account here to show goal == 0 matches on-chain state.
let account = banks_client.get_account(campaign_keypair.pubkey()).await.unwrap().unwrap();
// let campaign_state: Campaign = ... deserialize ...
// assert_eq!(campaign_state.goal, 0);
}

Recommended Mitigation Steps

A strict invariant check must be added to the fund_create function to ensure goal is at least 1 (or a higher minimum depending on token decimals).

Application of Fix

1. Locate fund_create:
Find the instruction handler responsible for initializing the campaign.

2. Add require! invariant:
Before assigning the goal to the account state, verify goal > 0.

Code Diff:

pub fn fund_create(ctx: Context<FundCreate>, goal: u64) -> Result<()> {
// Input Validation
+ require!(goal > 0, CampaignError::GoalMustBePositive);
let campaign = &mut ctx.accounts.campaign;
campaign.goal = goal;
// ... continue initialization
Ok(())
}
// Update Errors.rs
#[error_code]
pub enum CampaignError {
// ...
+ #[msg("Funding goal must be greater than zero.")]
+ GoalMustBePositive,
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[L-02] No Fund Goal Validation

## Description The contract allows fund creation with a goal amount of zero, which could be misleading to contributors. ## Vulnerability Details The fund_create function doesn't validate that the goal amount is reasonable (greater than zero), allowing the creation of funds with meaningless fundraising goals. ```rust pub fn fund_create(ctx: Context<FundCreate>, name: String, description: String, goal: u64) -> Result<()> { let fund = &mut ctx.accounts.fund; fund.name = name; fund.description = description; fund.goal = goal; // No validation that goal > 0 fund.deadline = 0; fund.creator = ctx.accounts.creator.key(); fund.amount_raised = 0; fund.dealine_set = false; Ok(()) } ``` ## Impact Funds with zero goals could confuse contributors and potentially be used to trick users by making it unclear when the funding target has been reached. ## POC Add to tests/rustfund.ts: ```javascript //audit LOW - No Fund Goal Validation it("Can create a fund with zero goal", async () => { const zeroGoalFundName = "Zero Goal Fund"; const [zeroGoalFundPDA] = await PublicKey.findProgramAddress( [Buffer.from(zeroGoalFundName), creator.publicKey.toBuffer()], program.programId ); await program.methods .fundCreate(zeroGoalFundName, description, new anchor.BN(0)) .accounts({ fund: zeroGoalFundPDA, creator: creator.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .rpc(); const fund = await program.account.fund.fetch(zeroGoalFundPDA); console.log(`Created fund with goal amount: ${fund.goal.toString()}`); }); ``` Output: ```javascript ======================================== 🐛 BUG REPORT [LOW]: No Fund Goal Validation ---------------------------------------- Description: The program allows creating funds with zero or invalid goal amounts Evidence: Created fund with goal amount: 0 ======================================== ``` ## Recommendations Add validation to ensure the goal is greater than zero: ```diff pub fn fund_create(ctx: Context<FundCreate>, name: String, description: String, goal: u64) -> Result<()> { // Validate goal is greater than zero + if goal == 0 { + return Err(ErrorCode::InvalidGoalAmount.into()); } let fund = &mut ctx.accounts.fund; fund.name = name; fund.description = description; fund.goal = goal; fund.deadline = 0; fund.creator = ctx.accounts.creator.key(); fund.amount_raised = 0; fund.dealine_set = false; Ok(()) } ```

Support

FAQs

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

Give us feedback!