EIP-4399 PREVRANDAO Vulnerability Patterns

December 21, 2025

EIP-4399 replaces the DIFFICULTY opcode with PREVRANDAO after Ethereum’s transition to Proof-of-Stake, providing a new source of on-chain randomness from the beacon chain.

This article summarizes 1 EIP-4399 vulnerability pattern with real audit cases to help you understand the risks of using PREVRANDAO for randomness.

EIP-4399 Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                    WHAT IS PREVRANDAO?                                  │
└─────────────────────────────────────────────────────────────────────────┘

Before PoS (Proof-of-Work):
→ DIFFICULTY opcode returned block difficulty (mining difficulty)
→ Used by some contracts as "randomness" source
→ Miners could influence it, but cost was high

After PoS (The Merge):
→ DIFFICULTY opcode repurposed to PREVRANDAO
→ Returns previous block's RANDAO mix from beacon chain
→ Each validator contributes to RANDAO via BLS signature
→ Still manipulable, but with different attack model

┌─────────────────────────────────────────────────────────────────────────┐
│                    HOW RANDAO WORKS                                     │
└─────────────────────────────────────────────────────────────────────────┘

Beacon Chain RANDAO accumulation:

Slot N-2     Slot N-1     Slot N       Slot N+1
┌────────┐   ┌────────┐   ┌────────┐   ┌────────┐
│Proposer│   │Proposer│   │Proposer│   │Proposer│
│   A    │   │   B    │   │   C    │   │   D    │
└───┬────┘   └───┬────┘   └───┬────┘   └───┬────┘
    │            │            │            │
    ▼            ▼            ▼            ▼
  RANDAO      RANDAO       RANDAO      RANDAO
  mix_N-2     mix_N-1      mix_N       mix_N+1
    │            │            │
    └─────┬──────┘            │
          │                   │
          ▼                   ▼
    XOR(mix_N-2,      XOR(mix_N-1,
        sig_B)            sig_C)

Each block:
  new_mix = XOR(previous_mix, BLS_signature_of_proposer)

In execution layer:
  block.prevrandao = RANDAO mix from PREVIOUS block

┌─────────────────────────────────────────────────────────────────────────┐
│                    THE MANIPULATION PROBLEM                             │
└─────────────────────────────────────────────────────────────────────────┘

Attack Model: Block Proposer has 1 bit of influence per slot

Normal case:
┌────────────┐
│ Proposer C │ ─── Proposes block ──► RANDAO updated with sig_C
└────────────┘

Attack case:
┌────────────┐
│ Proposer C │ ─── Withholds block ──► RANDAO NOT updated (stays at mix_N-1)
└────────────┘
      │
      └─► Sacrifices: block rewards + transaction fees
          Gains: Control over randomness outcome

Why this matters:
→ Proposer knows RANDAO mix BEFORE deciding to propose
→ Can calculate outcome of random selection
→ If outcome unfavorable: withhold block
→ Cost = foregone rewards; Gain = rigged randomness

┌─────────────────────────────────────────────────────────────────────────┐
│                    BLOCK.TIMESTAMP FINE-TUNING                          │
└─────────────────────────────────────────────────────────────────────────┘

Proposers also have limited control over block.timestamp:

Slot time window (12 seconds):
├─────────────────────────────────────────┤
│                                         │
t=0s                                    t=12s
│    ← Proposer can set timestamp here →  │

Constraints:
→ timestamp must be > parent block's timestamp
→ timestamp must be ≤ current real time (no future timestamps)
→ In practice: a few seconds of adjustment range

Why this matters for seed = prevrandao + timestamp:

prevrandao = 0x1234...5678 (fixed for this slot)
timestamp options = 1703145600, 1703145601, 1703145602, ...

seed_1 = prevrandao + 1703145600 → winner_index = seed_1 % 10 = 3
seed_2 = prevrandao + 1703145601 → winner_index = seed_2 % 10 = 7
seed_3 = prevrandao + 1703145602 → winner_index = seed_3 % 10 = 1 ← Attacker!

Even ±1 second change can completely change the winner when using modulo!

Vulnerability Patterns

1. Predictable Randomness Using PREVRANDAO

Prerequisite Knowledge:

EIP-4399 states that PREVRANDAO provides randomness with specific security properties. The beacon chain RANDAO implementation gives every block proposer 1 bit of influence power per slot. A proposer can deliberately refuse to propose a block (at the cost of block rewards) to prevent the RANDAO mix from being updated, allowing them to influence random outcomes. Additionally, block.timestamp can be manipulated by proposers within certain constraints (~12 second window).

Root Cause:

The contract uses block.prevrandao + block.timestamp as a seed for pseudo-random number generation, assuming it provides unpredictable randomness. However, both values are known to block proposers before they decide whether to propose a block, and both can be influenced by the proposer.

Attack Vector:

A block proposer who is a participant in a raffle or lottery can:

  1. Check the current RANDAO mix before proposing
  2. Calculate if the random outcome favors them
  3. If unfavorable: withhold the block (RANDAO stays unchanged)
  4. Wait until they have a proposer slot with favorable outcome
  5. Also adjust block.timestamp within allowed limits for fine-tuning

Source:

  • [sherlock/2024-06-boost-aa-wallet] Both block.prevrandao and block.timestamp are not reliable sources of randomness (issue-106.md)
    • boost-protocol/packages/evm/contracts/incentives/ERC20Incentive.sol

      Business Context: Boost Protocol Incentive Raffles

      ┌─────────────────────────────────────────────────────────────────────────┐
      │                    WHAT IS BOOST PROTOCOL?                              │
      └─────────────────────────────────────────────────────────────────────────┘
      
      Boost Protocol is an incentive distribution platform:
      → Projects create "Boosts" to reward users for actions
      → Two strategies: POOL (first-come-first-served) or RAFFLE
      → RAFFLE strategy: Users enter, random winner gets reward
      
      ┌─────────────────────────────────────────────────────────────────────────┐
      │                    RAFFLE FLOW                                          │
      └─────────────────────────────────────────────────────────────────────────┘
      
      Phase 1: Entry Period
      ┌──────────────┐                           ┌──────────────────┐
      │   User A     │ ── claim() ─────────────► │  ERC20Incentive  │
      │   User B     │ ── claim() ─────────────► │                  │
      │   User C     │ ── claim() ─────────────► │  entries[] =     │
      │   ...        │                           │  [A, B, C, ...]  │
      └──────────────┘                           └──────────────────┘
      
      Phase 2: Draw Winner
      ┌──────────────┐                           ┌──────────────────┐
      │    Owner     │ ── drawRaffle() ────────► │  ERC20Incentive  │
      │              │                           │                  │
      └──────────────┘                           │  seed = prevrandao│
                                                 │        + timestamp│
                                                 │                  │
                                                 │  winner = entries│
                                                 │    [seed % len]  │
                                                 └──────────────────┘
      
      ┌─────────────────────────────────────────────────────────────────────────┐
      │                    THE ATTACK                                           │
      └─────────────────────────────────────────────────────────────────────────┘
      
      Attacker who is BOTH:
        1. A raffle participant (in entries[])
        2. A block proposer (validator)
      
      ┌──────────────┐
      │  Attacker    │  Knows in advance:
      │  (Proposer)  │  → Current RANDAO mix
      │              │  → Their position in entries[]
      │              │  → Upcoming proposer slots
      └──────┬───────┘
             │
             ▼
      Calculate: seed = prevrandao + timestamp
                 winner_index = seed % entries.length
                 winner = entries[winner_index]
      
      If winner != Attacker:
          ├── Option 1: Withhold block (RANDAO unchanged)
          └── Option 2: Wait for next proposer slot
      
      If winner == Attacker:
          └── Propose block normally → Win raffle!

      Vulnerable Code:

      // ERC20Incentive.sol
      function drawRaffle() external override onlyOwner {
          if (strategy != Strategy.RAFFLE) revert BoostError.Unauthorized();
      
          // VULNERABILITY: Both prevrandao and timestamp are known to/manipulable by proposer
          LibPRNG.PRNG memory _prng = LibPRNG.PRNG({state: block.prevrandao + block.timestamp});
      
          // Winner determined by manipulable seed
          address winnerAddress = entries[_prng.next() % entries.length];
      
          asset.safeTransfer(winnerAddress, reward);
          emit Claimed(winnerAddress, abi.encodePacked(asset, winnerAddress, reward));
      }

EIP-4399 Audit Checklist

When auditing contracts that use on-chain randomness, check for these issues:

# Check Item Related Pattern
Uses block.prevrandao for high-value outcomes? Block proposers can influence RANDAO by withholding blocks Pattern 1
Combines block.timestamp with other “random” sources? Proposers control timestamp within ~12s window Pattern 1
Single-transaction randomness? If random value is used in same tx as selection, proposer can preview outcome Pattern 1
Uses commit-reveal or VRF for critical randomness? Only verifiable randomness is safe for high-value selection Pattern 1