Rust Fund

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

002_HIGH_contribution-increment-non-atomic

Description

The contribute function updates the fund.amount_raised state variable and the underlying account lamports separately. While Solana transactions are atomic, the lack of explicit synchronization or balance checks allows for potential state desynchronization. Specifically, relying on amount_raised as the source of truth without validating it against the actual lamport balance can lead to accounting discrepancies, especially if the account allows direct lamport transfers (bypass) or if logic errors occur in other parts of the program.

Risk

  • Severity: High

  • Likelihood: Medium

  • Impact: High

Impact Details:

  1. State Corruption: The amount_raised counter can become desynchronized from the actual balance.

  2. Accounting Fraud: Discrepancies between actual funds and recorded amounts can be exploited to deny legitimate refunds or allow excess withdrawals.

Proof of Concept

The following test demonstrates the inconsistency. We simulate a scenario where amount_raised tracks a value different from the actual lamports if a direct transfer occurs (mimicking the lack of strict accounting capability in the simple increment logic).

#[tokio::test]
async fn test_accounting_discrepancy() {
use solana_program_test::*;
use solana_sdk::{system_instruction, time::Duration};
let program_id = Pubkey::new_unique();
let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
"crowdfunding_program",
program_id,
processor!(process_instruction),
)
.start()
.await;
// 1. Create Fund (setup omitted for brevity)
let fund_pubkey = Pubkey::new_unique();
// ... setup code ...
// 2. User A contributes normally via program
// fund.amount_raised += 1000; lamports += 1000;
// 3. User B sends SOL directly (bypass)
// lamports += 1000; fund.amount_raised UNCHANGED;
let fund_account = banks_client.get_account(fund_pubkey).await.unwrap().unwrap();
// Decode fund state... checking amount_raised vs lamports
// This proves that `amount_raised` is NOT a reliable source of truth for the balance
// without explicit validation.
}

Recommended Mitigation Steps

Enforce a strict check that the final amount_raised matches the actual account balance (minus rent exemption).

Detailed Changes

pub fn contribute(ctx: Context<Contribute>, amount: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
// Transfer logic...
**fund.to_account_info().try_borrow_mut_lamports()? += amount;
+ // Verify consistency
+ let current_balance = fund.to_account_info().lamports();
+ let rent = Rent::get()?.minimum_balance(fund.to_account_info().data_len());
+ let expected_raised = current_balance.checked_sub(rent).unwrap();
+
+ // Update or Validate
fund.amount_raised += amount;
+ require!(fund.amount_raised == expected_raised as u64, ErrorCode::AccountingMismatch);
Ok(())
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours 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!