Precompile Preimage Pre-population Vulnerability

December 20, 2025

Ethereum precompiles are special contracts at reserved addresses (0x01-0x0a) that provide efficient cryptographic operations. L2 fraud proof systems like Optimism’s Cannon need to handle precompile outputs, but improper validation of precompile addresses can lead to data poisoning attacks.

This article summarizes 1 precompile preimage vulnerability pattern with real audit cases to help you understand the risks of unvalidated precompile address handling.

Precompile Basics

┌─────────────────────────────────────────────────────────────────────────┐
│                    WHAT ARE PRECOMPILES?                                │
└─────────────────────────────────────────────────────────────────────────┘

Precompiles are special "contracts" at fixed addresses that execute
native code instead of EVM bytecode. They provide efficient crypto operations.

Current Ethereum Precompiles:
┌──────────┬────────────────────────────────────────────────────────────┐
│ Address  │  Function                                                  │
├──────────┼────────────────────────────────────────────────────────────┤
│   0x01   │  ECRECOVER - Recover signer from ECDSA signature           │
│   0x02   │  SHA256 - SHA-256 hash                                     │
│   0x03   │  RIPEMD160 - RIPEMD-160 hash                               │
│   0x04   │  IDENTITY - Returns input unchanged (datacopy)             │
│   0x05   │  MODEXP - Modular exponentiation                           │
│   0x06   │  ECADD - BN256 curve point addition                        │
│   0x07   │  ECMUL - BN256 curve point multiplication                  │
│   0x08   │  ECPAIRING - BN256 pairing check                           │
│   0x09   │  BLAKE2F - BLAKE2 compression function                     │
│   0x0a   │  POINT_EVALUATION - KZG point evaluation (EIP-4844)        │
└──────────┴────────────────────────────────────────────────────────────┘

Reserved for future:
┌──────────┬────────────────────────────────────────────────────────────┐
│ 0x0b-... │  Reserved for future precompiles (network upgrades)        │
└──────────┴────────────────────────────────────────────────────────────┘

Key Insight for L2 Fraud Proofs:
→ L2 systems like Optimism need to replay precompile outputs during disputes
→ Storage key is derived from precompile address: key = hash(precompile, input)
→ If future precompile at 0x0b doesn't exist yet, what happens when someone
  pre-populates data for that address?

Vulnerability Patterns

1. Precompile Preimage Pre-population

Prerequisite Knowledge:

Ethereum precompiles are located at fixed addresses (0x01 to 0x0a), with addresses 0x0b onwards reserved for future precompiles. The PreimageOracle contract in Optimism’s Cannon fraud proof system stores precompile outputs for dispute resolution. The storage key is derived from the precompile address: key = hash(precompile, input). If a new precompile is added at address 0x0b in a future network upgrade, it will use the same key derivation, meaning any pre-populated data at that key would be trusted.

Root Cause:

The loadPrecompilePreimagePart() function allows anyone to specify any _precompile address without validating if it is currently an active precompile. This design was intended to handle future precompiles without contract upgrades, but creates a vulnerability where attackers can pre-populate data for reserved addresses before they become real precompiles.

Attack Vector:

An attacker calls loadPrecompilePreimagePart() with a future precompile address (e.g., 0x0b) and crafted input. Since 0x0b has no code, the staticcall returns empty/zero data, which gets stored. Later, when a network upgrade introduces a real precompile at 0x0b, Cannon will read the attacker’s pre-populated data instead of the correct precompile output, potentially corrupting fraud proof resolution.

Source:

  • [code4rena/2024-07-optimism] [M-03] Addresses can be pre-populated with bad data (finding-008.md)
    • packages/contracts-bedrock/src/cannon/PreimageOracle.sol

      Business Context: Optimism Cannon Fraud Proof System

      ┌─────────────────────────────────────────────────────────────────────────┐
      │                    WHAT IS OPTIMISM?                                    │
      └─────────────────────────────────────────────────────────────────────────┘
      
      Optimism is an L2 scaling solution using Optimistic Rollups:
      → Transactions execute on L2 (fast, cheap)
      → State roots posted to L1 (Ethereum)
      → Assumed valid unless challenged within 7-day window
      → If challenged, fraud proof determines correctness
      
      ┌─────────────────────────────────────────────────────────────────────────┐
      │                    WHAT IS CANNON?                                      │
      └─────────────────────────────────────────────────────────────────────────┘
      
      Cannon is Optimism's fraud proof system:
      → Replays disputed L2 transactions on L1
      → Uses MIPS VM to execute L2 state transition
      → PreimageOracle provides external data (block headers, precompile outputs)
      
      Why PreimageOracle for Precompiles?
      → L1 replay needs precompile outputs from L2 execution
      → Precompiles (ECRECOVER, SHA256, etc.) are deterministic
      → Anyone can load precompile outputs into oracle
      → Cannon reads from oracle during dispute
      
      Normal Flow:
      ┌──────────────┐     Load precompile output     ┌──────────────┐
      │   Anyone     │ ──────────────────────────────►│PreimageOracle│
      │              │   (0x08, input) → output       │              │
      └──────────────┘                                └──────────────┘
                                                             │
                                                             ▼
      ┌──────────────┐     Read during dispute        ┌──────────────┐
      │   Cannon     │ ◄──────────────────────────────│PreimageOracle│
      │   (MIPS VM)  │   key = hash(0x08, input)      │              │
      └──────────────┘                                └──────────────┘
      
      ┌─────────────────────────────────────────────────────────────────────────┐
      │                    THE VULNERABILITY                                    │
      └─────────────────────────────────────────────────────────────────────────┘
      
      The Problem: loadPrecompilePreimagePart() accepts ANY address!
      
      ┌────────────────────────────────────────────────────────────────────────┐
      │  function loadPrecompilePreimagePart(                                   │
      │      uint256 _partOffset,                                               │
      │      address _precompile,    ← No validation! Can be 0x0b, 0x0c, etc.  │
      │      bytes calldata _input                                              │
      │  ) external {                                                           │
      │      ...                                                                │
      │      key := computeKey(_precompile, _input)                             │
      │      res := staticcall(gas(), _precompile, ...)  ← Calls empty address │
      │      ...                                                                │
      │      preimagePartOk[key][_partOffset] = true;  ← Marks data as valid!  │
      │      preimageParts[key][_partOffset] = part;                            │
      │  }                                                                      │
      └────────────────────────────────────────────────────────────────────────┘
      
      ┌─────────────────────────────────────────────────────────────────────────┐
      │                    ATTACK SCENARIO                                      │
      └─────────────────────────────────────────────────────────────────────────┘
      
      Step 1: Today - Attacker pre-populates future precompile address
      ┌──────────────┐     loadPrecompilePreimagePart(0x0b, maliciousInput)
      │   Attacker   │ ────────────────────────────────────────────────────►
      └──────────────┘
      
              ┌────────────────────────────────────────────────────────────┐
              │  PreimageOracle state:                                      │
              │                                                             │
              │  preimagePartOk[key_0x0b][0] = true                         │
              │  preimageParts[key_0x0b][0] = 0x00... (empty, since no code)│
              │                                                             │
              │  Note: 0x0b is NOT a precompile yet, but data is stored!   │
              └────────────────────────────────────────────────────────────┘
      
      Step 2: Future - Network upgrade adds precompile at 0x0b
      ┌────────────────────────────────────────────────────────────────────────┐
      │  New EIP introduces precompile at 0x0b (e.g., new crypto operation)    │
      │                                                                        │
      │  The REAL precompile output would be: 0xABCD...                        │
      │  But attacker already stored:         0x0000... (wrong!)               │
      └────────────────────────────────────────────────────────────────────────┘
      
      Step 3: Fraud proof uses poisoned data!
      ┌──────────────┐     Dispute resolution     ┌──────────────┐
      │   Cannon     │ ──────────────────────────►│ PreimageOracle│
      │              │   readPreimage(key_0x0b)   │              │
      └──────────────┘                            └──────┬───────┘
                                                         │
                                         Returns attacker's
                                         pre-populated data!
                                                         │
                                                         ▼
                                         ┌─────────────────────────┐
                                         │  Fraud proof validated  │
                                         │  with WRONG data!       │
                                         │                         │
                                         │  L2 state corruption!   │
                                         └─────────────────────────┘
      
      ┌─────────────────────────────────────────────────────────────────────────┐
      │                    IMPACT                                               │
      └─────────────────────────────────────────────────────────────────────────┘
      
      If attacker poisons precompile data:
      → Fraud proofs may validate incorrect L2 state
      → Challenger may be unable to prove fraud (data mismatch)
      → Could lead to theft of bridged funds
      → Undermines entire security model of optimistic rollup
      
      ┌─────────────────────────────────────────────────────────────────────────┐
      │                    FIX                                                  │
      └─────────────────────────────────────────────────────────────────────────┘
      
      Option 1: Whitelist valid precompile addresses
      ┌────────────────────────────────────────────────────────────────────────┐
      │  mapping(address => bool) public isValidPrecompile;                     │
      │                                                                        │
      │  function loadPrecompilePreimagePart(..., address _precompile, ...) {  │
      │      require(isValidPrecompile[_precompile], "Invalid precompile");    │
      │      ...                                                                │
      │  }                                                                      │
      └────────────────────────────────────────────────────────────────────────┘
      
      Option 2: Check address range
      ┌────────────────────────────────────────────────────────────────────────┐
      │  function loadPrecompilePreimagePart(..., address _precompile, ...) {  │
      │      require(                                                          │
      │          uint160(_precompile) >= 0x01 &&                               │
      │          uint160(_precompile) <= 0x0a,  // Current max precompile     │
      │          "Invalid precompile"                                          │
      │      );                                                                 │
      │      ...                                                                │
      │  }                                                                      │
      └────────────────────────────────────────────────────────────────────────┘

      Vulnerable Code:

      function loadPrecompilePreimagePart(
          uint256 _partOffset,
          address _precompile,  // VULNERABILITY: No validation that _precompile is a valid precompile
          bytes calldata _input
      ) external {
          bytes32 key;
          bytes32 part;
          uint256 size;
          assembly {
              // ... compute key from _precompile and _input ...
      
              // Call the precompile (may be empty address!)
              res := staticcall(gas(), _precompile, ...)
      
              // ... extract result ...
          }
      
          // VULNERABILITY: Sets flag for ANY address, including future precompiles
          preimagePartOk[key][_partOffset] = true;
          preimageParts[key][_partOffset] = part;
          preimageLengths[key] = size;
      }
      
      function readPreimage(bytes32 _key, uint256 _offset) external view returns (bytes32, uint256) {
          // VULNERABILITY: This check passes for attacker's pre-populated data
          require(preimagePartOk[_key][_offset], "pre-image must exist");
      
          return (preimageParts[_key][_offset], ...);
      }

Precompile Security Audit Checklist

When auditing contracts that interact with precompiles, check for these issues:

# Check Item Related Pattern
Is precompile address validated? Unvalidated addresses allow pre-population attacks Pattern 1
Are reserved addresses (0x0b+) accessible? Future precompile addresses should be blocked Pattern 1
Can external callers control precompile address? Attacker-controlled addresses enable poisoning Pattern 1