Install in Claude Code
Copygit clone --depth 1 https://github.com/PurpleAILAB/Vigilo /tmp/randomness && cp -r /tmp/randomness/packages/opencode/skills/randomness ~/.claude/skills/randomnessThen start a new Claude Code session; the skill loads automatically.
Definition
SKILL.md
# Insecure Randomness Vulnerability Analysis
**OWASP SC09:2025** - Insecure Randomness allows attackers to predict or manipulate "random" outcomes for profit.
**2025-2026 Statistics**: Randomness exploits caused $85M+ in losses, with NFT mint exploits and lottery manipulations being the most common vectors.
---
## Why Randomness Fails (Root Causes)
### Root Cause 1: On-Chain Data is Public
Everything on-chain is visible to everyone, including miners/validators.
```solidity
// VULNERABLE: Uses public blockchain data
function roll() external returns (uint256) {
return uint256(keccak256(abi.encodePacked(
block.timestamp, // @audit Miner controls this
block.difficulty, // @audit Known before execution
msg.sender // @audit Attacker controls this
))) % 100;
}
```
**Attacker's view**: "I can simulate this off-chain and only submit when I win."
### Root Cause 2: Miner/Validator Control
Block proposers control block.timestamp and can reorder transactions.
```solidity
// VULNERABLE: Miner can manipulate
function lottery() external {
require(block.timestamp % 10 == 0); // @audit Miner waits for favorable timestamp
winner = msg.sender;
}
```
### Root Cause 3: Predictable Future State
Using future blockhash that can be predicted or influenced.
```solidity
// VULNERABLE: Blockhash only available for 256 blocks
function reveal(uint256 commitBlock) external {
require(block.number > commitBlock + 10);
uint256 random = uint256(blockhash(commitBlock + 10)); // @audit Returns 0 after 256 blocks!
}
```
### Root Cause 4: Single-Transaction Exploitation
Randomness generated and used in same transaction can be front-run or simulated.
```solidity
// VULNERABLE: Same-tx randomness
function mint() external {
uint256 tokenId = uint256(keccak256(abi.encodePacked(block.timestamp, totalSupply)));
_mint(msg.sender, tokenId);
// @audit Attacker simulates result, only mints if rare
}
```
---
## The Randomness Source Analysis (Core Artifact)
For each randomness usage, document:
| Function | Source | Controller | Predictability | Attack Vector |
|----------|--------|------------|----------------|---------------|
| lottery() | block.timestamp | Miner | 100% | Miner manipulation |
| roll() | blockhash | Anyone | 100% | Off-chain simulation |
| mint() | tx + state | Attacker | 100% | Selective execution |
| draw() | VRF | None | 0% | Secure |
---
## Detection Patterns
### Pattern 1: block.timestamp as Randomness
**Root Cause**: Miner/Validator Control
```solidity
// VULNERABLE: Timestamp is miner-controllable
function randomize() public view returns (uint256) {
return uint256(keccak256(abi.encodePacked(block.timestamp))) % 100;
}
function flipCoin() external {
if (uint256(block.timestamp) % 2 == 0) { // @audit Binary outcome, easily manipulated
_winner(msg.sender);
}
}
```
**Attack Flow**:
1. Attacker (or colluding validator) sees pending lottery
2. Adjusts block.timestamp within allowed range (~15 seconds)
3. Picks timestamp that gives winning result
4. Profits from "random" outcome
**Search Queries**:
```
Grep("block\\.timestamp.*%|block\\.timestamp.*random", glob="**/*.sol")
Grep("keccak256.*block\\.timestamp", glob="**/*.sol")
```
### Pattern 2: blockhash Exploitation
**Root Cause**: Predictable Future State
```solidity
// VULNERABLE: blockhash is predictable and limited
function getRandomNumber(uint256 blockNumber) public view returns (uint256) {
return uint256(blockhash(blockNumber));
// @audit blockhash(block.number) = 0
// @audit blockhash(block.number - 257) = 0 (expired)
}
// VULNERABLE: Future blockhash commit scheme
mapping(address => uint256) public commitBlock;
function commit() external {
commitBlock[msg.sender] = block.number;
}
function reveal() external {
uint256 bn = commitBlock[msg.sender];
require(block.number > bn + 10, "Too early");
uint256 random = uint256(blockhash(bn + 10)); // @audit Returns 0 after 256 blocks!
// ...
}
```
**Attack Flow**:
1. Attacker commits
2. Waits 257 blocks
3. blockhash returns 0
4. Outcome becomes deterministic
**Search Queries**:
```
Grep("blockhash\\(|block\\.prevrandao", glob="**/*.sol")
Grep("block\\.number.*\\-.*256|block\\.number.*>.*256", glob="**/*.sol")
```
### Pattern 3: prevrandao (RANDAO) Misuse
**Root Cause**: Validator can bias RANDAO
```solidity
// DANGEROUS on L1, CRITICAL on L2
function getRandomFromPrevrandao() public view returns (uint256) {
return block.prevrandao; // @audit On L2, sequencer fully controls this!
}
```
**L1 vs L2 Differences**:
| Chain | prevrandao Behavior | Manipulation Risk |
|-------|---------------------|-------------------|
| Ethereum L1 | RANDAO mixing | Low (but biasable) |
| Arbitrum | L1 block hash | Medium |
| Optimism | L1 block hash | Medium |
| Base | L1 block hash | Medium |
| zkSync | Sequencer controlled | **CRITICAL** |
**Search Queries**:
```
Grep("block\\.prevrandao|block\\.difficulty", glob="**/*.sol")
Grep("PREVRANDAO|RANDAO", glob="**/*.sol")
```
### Pattern 4: msg.sender/tx.origin in Randomness
**Root Cause**: Attacker Controls Input
```solidity
// VULNERABLE: Attacker controls msg.sender
function random() public view returns (uint256) {
return uint256(keccak256(abi.encodePacked(
msg.sender, // @audit Attacker generates addresses until favorable
block.number
)));
}
```
**Attack Flow**:
1. Attacker generates many addresses
2. Simulates random() for each
3. Uses address that gives winning result
4. Deploys contract from that address or uses that EOA
**Search Queries**:
```
Grep("keccak256.*msg\\.sender|msg\\.sender.*random", glob="**/*.sol")
Grep("tx\\.origin.*random|random.*tx\\.origin", glob="**/*.sol")
```
### Pattern 5: Single-Transaction NFT Rarity Exploitation
**Root Cause**: Same-tx randomness is simulatable
```solidity
// VULNERABLE: Rarity determined at mint time
function mint() external payable {