Skip to main content
Sigvex

Weak Randomness

Detects Solana programs that derive randomness from predictable on-chain sources such as clock timestamps, slot numbers, or account hashes.

Weak Randomness

Overview

Remediation Guide: How to Fix Weak Randomness

The weak-randomness detector identifies Solana programs that generate or rely on randomness derived from predictable on-chain data sources. Because all Solana program execution is deterministic and all on-chain state is publicly visible, values derived from sources such as Clock::unix_timestamp, Clock::slot, transaction hashes, or account data hashes are predictable by an attacker who can observe the blockchain or, in some cases, influence the source values.

Sigvex identifies patterns in the HIR where sysvar clock reads, hash operations over account data, or bitwise operations on slot numbers are used to generate values that subsequently gate high-value outcomes: lottery draws, NFT trait assignments, loot distributions, game outcomes, or financial liquidation decisions.

Unlike Ethereum, Solana has no blockhash-based VRF in the base protocol, and its slot-based timing is even more predictable due to shorter block times and the validator’s visibility into upcoming slots.

Why This Is an Issue

Any value an attacker can compute before submitting a transaction can be exploited in gambling, NFT minting, or game theory contexts. Weak randomness vulnerabilities allow:

  • Lottery manipulation: An attacker computes the “random” seed from current on-chain state, determines which ticket would win, buys only that ticket, and wins guaranteed.
  • NFT trait sniping: In mint-time randomized NFTs, an attacker predicts which mint will produce a rare trait and selects only those transactions.
  • Game outcome prediction: Validators, in particular, have foreknowledge of slot leaders and can time transactions to land in favorable slots.
  • Front-running: An attacker monitors the mempool for transactions that would produce favorable random outcomes and front-runs them.

In Solana’s proof-of-history (PoH) design, slot leaders are known in advance. This means a validator can manipulate slot-based randomness by choosing when to include transactions within their assigned slots.

How to Resolve

The correct solution for on-chain randomness in Solana is to use a Verifiable Random Function (VRF) oracle such as Switchboard VRF or the off-chain aggregator VRF (where available). These provide cryptographically proven randomness that cannot be predicted or manipulated by the requesting program.

// Before: Vulnerable — using clock timestamp as randomness
use solana_program::clock::Clock;
use solana_program::sysvar::Sysvar;

pub fn mint_nft(accounts: &[AccountInfo]) -> ProgramResult {
    let clock = Clock::get()?;

    // VULNERABLE: timestamp is predictable and manipulable by validators
    let random_trait = (clock.unix_timestamp as u64) % 10;

    assign_trait(accounts, random_trait)?;
    Ok(())
}

// After: Use a VRF oracle result committed in a prior transaction
pub fn reveal_nft_trait(accounts: &[AccountInfo]) -> ProgramResult {
    let vrf_result_account = &accounts[0];
    let mint_record = &accounts[1];

    // VRF result was committed in a previous transaction by the oracle
    let vrf_data = VrfResult::try_from_slice(&vrf_result_account.data.borrow())?;

    // Verify the VRF result is for this mint
    let record = MintRecord::try_from_slice(&mint_record.data.borrow())?;
    if vrf_data.request_id != record.vrf_request_id {
        return Err(ProgramError::InvalidAccountData);
    }

    // Use the cryptographically random value
    let random_trait = vrf_data.result[0] as u64 % 10;
    assign_trait(accounts, random_trait)?;
    Ok(())
}

For simpler use cases where a full VRF is not available, a commit-reveal scheme provides some protection against front-running (though not against validators):

// Commit phase (user submits hash of their secret)
pub fn commit(accounts: &[AccountInfo], commitment: [u8; 32]) -> ProgramResult {
    let user_commitment = &accounts[0];
    let mut data = user_commitment.try_borrow_mut_data()?;
    data[..32].copy_from_slice(&commitment);
    Ok(())
}

// Reveal phase (user submits secret, combined with on-chain entropy)
pub fn reveal(accounts: &[AccountInfo], secret: [u8; 32]) -> ProgramResult {
    let user_commitment = &accounts[0];
    let clock = Clock::get()?;

    let stored_commitment = &user_commitment.data.borrow()[..32];
    let computed_commitment = solana_program::hash::hash(&secret).to_bytes();

    if stored_commitment != &computed_commitment {
        return Err(ProgramError::InvalidArgument);
    }

    // Combine secret with slot hash for additional entropy
    let slot_hash = solana_program::hash::hashv(&[&secret, &clock.slot.to_le_bytes()]);
    let random_value = u64::from_le_bytes(slot_hash.to_bytes()[..8].try_into().unwrap()) % 10;

    Ok(())
}

Examples

Vulnerable Code

// Lottery contract using predictable slot-based randomness
pub fn draw_winner(accounts: &[AccountInfo]) -> ProgramResult {
    let lottery = &accounts[0];
    let clock = Clock::get()?;

    let lottery_data = Lottery::try_from_slice(&lottery.data.borrow())?;
    let num_tickets = lottery_data.tickets.len() as u64;

    // VULNERABLE: slot is known to validators before transaction inclusion
    let winner_index = clock.slot % num_tickets;
    let winner = lottery_data.tickets[winner_index as usize];

    // Transfer prize to winner
    // ...
    Ok(())
}

Fixed Code

// Using Switchboard VRF for provably random lottery draw
pub fn draw_winner(accounts: &[AccountInfo]) -> ProgramResult {
    let lottery = &accounts[0];
    let vrf_account = &accounts[1]; // Switchboard VRF account

    // Read the VRF result — this was generated in a prior round
    let vrf_result = switchboard_v2::VrfAccountData::new(vrf_account)?;

    if !vrf_result.status.is_fulfilled() {
        return Err(ProgramError::InvalidAccountData);
    }

    let lottery_data = Lottery::try_from_slice(&lottery.data.borrow())?;
    let num_tickets = lottery_data.tickets.len() as u64;

    // Use cryptographically random value from VRF
    let random_bytes = vrf_result.get_result()?;
    let winner_index = u64::from_le_bytes(random_bytes[..8].try_into().unwrap()) % num_tickets;
    let winner = lottery_data.tickets[winner_index as usize];

    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "weak-randomness",
  "severity": "high",
  "confidence": 0.78,
  "description": "Function draw_winner() derives a random winner index from clock.slot, which is predictable by validators. A validator can manipulate transaction inclusion timing to determine the winner.",
  "location": { "function": "draw_winner", "offset": 24 }
}

Detection Methodology

The detector implements taint-based analysis to identify randomness flows:

  1. Source identification: Tags sysvar reads (Clock::get(), SlotHashes::get()) and account-data hash operations as weak entropy sources.
  2. Taint propagation: Tracks tainted values through arithmetic operations (modulo, bitwise AND, XOR) that are commonly used to convert raw values to bounded random numbers.
  3. Sink identification: Flags tainted values used in array indexing, branching conditions, or token/NFT attribute assignments in high-value contexts.
  4. VRF recognition: Recognizes Switchboard VRF and the off-chain aggregator VRF account patterns and suppresses findings when VRF results are used correctly.
  5. Commit-reveal detection: Identifies commit-reveal patterns and adjusts confidence downward, noting the remaining validator manipulation risk.

Limitations

False positives:

  • Programs that use clock timestamps only for rate-limiting or timeout logic (not for outcome determination) may be flagged.
  • Internal program counters derived from slot numbers for sequential IDs (not used for random selection) may produce low-confidence findings.

False negatives:

  • Randomness imported from a separate program via CPI that internally uses weak sources will not be detected.
  • Multi-step randomness accumulation across transactions is not tracked.

References