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 Chainlink 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:
- Source identification: Tags sysvar reads (
Clock::get(),SlotHashes::get()) and account-data hash operations as weak entropy sources. - Taint propagation: Tracks tainted values through arithmetic operations (modulo, bitwise AND, XOR) that are commonly used to convert raw values to bounded random numbers.
- Sink identification: Flags tainted values used in array indexing, branching conditions, or token/NFT attribute assignments in high-value contexts.
- VRF recognition: Recognizes Switchboard VRF and Chainlink VRF account patterns and suppresses findings when VRF results are used correctly.
- 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.
Related Detectors
- Oracle Manipulation — detects manipulation of off-chain oracle data
- Missing Signer Check — detects missing authorization that can combine with weak randomness to amplify impact