FABRKNT
Reth Expert — Production Engineering
Production Engineering
Lesson 6 of 25·CONTENT15 min35 XP

Treat this page as a workbench, not a blog post. The goal is to extract a reusable mental model from the source and carry it into the rest of the Fabrknt stack.

Course
Reth Expert — Production Engineering
Lesson role
CONTENT
Sequence
6 / 25

Lesson 6 — Custom precompiles

Question

Precompiles = built-in Solidity functions at magic addresses (0x01-0x09). Add your own = extend the EVM for chain-specific operations.

Principle (minimum model)

  • Precompile architecture. Address (e.g. 0x1A) + Rust function fn(input: &[u8], gas_limit: u64) -> (Bytes, u64). Returns output + gas used.
  • Why precompiles. Common operations too slow in pure Solidity (e.g. ed25519 verify); shared utility (CLOB ops); chain-specific (HyperEVM order placement).
  • Integration via revm. EvmConfig::with_precompiles(custom_precompiles). Revm dispatches to your function at the address.
  • Gas cost. Determined by you. Underprice → DoS; overprice → unusable. Benchmark.
  • Failure semantics. Return empty Bytes + gas charge OR revert. Pick deliberate semantics.
  • Production examples. Hyperliquid CLOB precompiles (0xCL...AB read best bid; 0xCL...C1 place order). Tempo merchant attestation precompile.
  • Tests. Deploy a Solidity contract that calls the precompile; assert correct output + gas charge.

Worked example + steps

Custom precompiles

You want SHA-256 inside the EVM. Two roads: add a new opcode (a new byte in the instruction stream — SHA256 next to ADD), or add a precompile (a native Rust function the EVM calls when a contract does CALL 0x00...02). Real Ethereum picked the second: precompiles at addresses 0x01 through 0x0a cover ecrecover, sha256, modexp, and the BN254 elliptic-curve ops. Foundry's cheatcodes (vm.deal, vm.warp) are the same trick at industrial scale.

Custom opcodes break consensus with every wallet, indexer, and Solidity compiler on the planet. Custom precompiles don't. This lesson is why the answer is different, and how to register one.

1. Opcode vs precompile

OpcodePrecompile
Invocationbytecode instructionCALL to a special address
Addingmodifies interpreteradds an entry in the precompile registry
Tooling impactbreaks Solidity, ABIsmostly transparent
Use casetight inner loopsheavy operations like pairings, hashing

Real Ethereum already has precompiles at addresses 0x01–0x0a (ecrecover, sha256, ripemd160, identity, modexp, BN254 ops, BLAKE2F, point eval).

2. A real precompile — the identity precompile (0x04)

This is the entire identity_run from crates/precompile/src/identity.rs:

use super::calc_linear_cost;
use crate::{
    eth_precompile_fn, EthPrecompileOutput, EthPrecompileResult, Precompile, PrecompileHalt,
    PrecompileId,
};
use primitives::Bytes;

eth_precompile_fn!(identity_precompile, identity_run);

/// Address of the identity precompile.
pub const FUN: Precompile = Precompile::new(
    PrecompileId::Identity,
    crate::u64_to_address(4),
    identity_precompile,
);

/// The base cost of the operation
pub const IDENTITY_BASE: u64 = 15;
/// The cost per word
pub const IDENTITY_PER_WORD: u64 = 3;

/// Takes the input bytes, copies them, and returns it as the output.
pub fn identity_run(input: &[u8], gas_limit: u64) -> EthPrecompileResult {
    let gas_used = calc_linear_cost(input.len(), IDENTITY_BASE, IDENTITY_PER_WORD);
    if gas_used > gas_limit {
        return Err(PrecompileHalt::OutOfGas);
    }
    Ok(EthPrecompileOutput::new(
        gas_used,
        Bytes::copy_from_slice(input),
    ))
}

That's a production precompile in Ethereum mainnet. Read it line by line:

  • Address u64_to_address(4)0x0000…0004. The address is part of the Precompile::new — you don't get to put it anywhere; it's compiled in.
  • Gas formula base + per_word * ceil(len / 32). Linear in input. IDENTITY_BASE = 15, IDENTITY_PER_WORD = 3 — the real values from the Yellow Paper.
  • Halt vs revertPrecompileHalt::OutOfGas means the entire frame halts with no refund, distinct from a regular revert.
  • EthPrecompileOutput carries (gas_used, output_bytes).

3. Registering custom precompiles

sequenceDiagram
    participant C as Contract bytecode
    participant I as Revm interpreter
    participant Reg as Precompiles registry
    participant Fn as Custom precompile fn

    C->>I: CALL 0x00...ff
    I->>Reg: lookup(addr)
    Reg-->>I: Found — Precompile
    I->>Fn: run(input, gas_limit)
    Fn-->>I: Ok(gas_used, output)
    I->>C: returndata + gas refund

The Precompiles registry in crates/precompile/src/lib.rs has an extend method designed exactly for this:

pub fn extend(&mut self, other: impl IntoIterator<Item = Precompile>) {
    let iter = other.into_iter();
    let (lower, _) = iter.size_hint();
    self.addresses.reserve(lower);
    self.inner.reserve(lower);
    for item in iter {
        let address = *item.address();
        if let Some(short_idx) = short_address(&address) {
            self.optimized_access[short_idx] = Some(item.clone());
        }
        self.addresses.insert(address);
        self.inner.insert(address, item);
    }
}

So adding your own is just:

let my_pre = Precompile::new(
    PrecompileId::Custom("my_thing"),
    address!("00000000000000000000000000000000000000ff"),
    my_function,
);
precompiles.extend([my_pre]);

my_function follows the same shape as identity_run: fn(&[u8], u64) -> EthPrecompileResult. Wire it through your custom Evm builder where the precompile set is loaded.

The optimized_access array

Notice the optimized_access[short_idx] write. For addresses that fit in a small number, Revm uses a flat array instead of a hashmap — dispatch becomes a single index lookup. This is why standard precompiles (0x01–0x0a) are essentially free to dispatch.

4. Real-world: Foundry's cheatcodes ARE custom precompiles

The most widely-deployed custom precompile in the Rust EVM stack lives in Foundry. Every vm.deal, vm.warp, vm.prank you've ever written in a Solidity test is a CALL to a custom precompile.

From forge-std/src/Base.sol:

address internal constant VM_ADDRESS = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D;

That address is computed as:

address(uint160(uint256(keccak256("hevm cheat code"))))

Foundry constructs Revm with a custom precompile registered at this address that decodes the calldata as a cheatcode invocation (e.g., the selector for deal(address,uint256)) and dispatches into Foundry's Rust code. State mutations like "give this account 10 ETH" happen by directly modifying the in-memory Revm DB before continuing execution.

This is the production case study for the registration pattern in Section 3:

  • Foundry forks the standard precompile set
  • Registers an additional entry at 0x7109... pointing to its cheatcode dispatcher
  • The dispatcher reads calldata, matches a selector, and runs Rust to mutate the EVM state

If you want to see a production custom-precompile design in detail, read foundry-rs/foundry/crates/cheatcodes — it's the same pattern as our example, just at industrial scale (hundreds of cheatcodes, snapshotting, revert support).

5. When to reach for a precompile

  • A computation is too expensive in pure EVM bytecode (BLS pairings, FRI verification, large-radix arithmetic)
  • The same operation is needed by many contracts on your chain
  • You can write a provably-correct Rust implementation

Don't add a precompile to save a few opcodes — the design overhead and consensus risk only pay off for genuinely heavy work.

6. Pricing

The cardinal rule: gas cost should track CPU cost (and ideally a multiple of the worst case). Underprice and an attacker DoSes your chain with a single weird transaction. Real Ethereum has been here multiple times — see the EIP-2929 reset of cold/warm storage costs.

A reasonable workflow:

  1. Benchmark your precompile on the slowest realistic input
  2. Multiply CPU time by an "abuse factor" (commonly 2–5x)
  3. Convert to gas via your chain's gas/CPU ratio
  4. Re-benchmark on adversarial inputs

After this, your precompile is cheap to use in normal code and prohibitively expensive to abuse.

Final check: you ship a precompile at gas cost = 100. An attacker discovers an input shape that takes 10x normal CPU time at the same 100 gas. What's the economic attack? How much does it cost the attacker per second of node CPU? If you can't sketch the math, you can't safely price a precompile — re-read Section 6 and Ethereum's EIP-2929 for what real underpricing has cost mainnet.

Summary (3 lines)

  • Custom precompile = address + Rust function (input, gas_limit) → (output, gas_used). Magic-address dispatch via revm.
  • Why: too-slow in Solidity (crypto) + shared utility (CLOB ops) + chain-specific (HyperEVM ops).
  • Gas cost benchmarked; failure semantics deliberate. Production: Hyperliquid CLOB + Tempo attestation precompiles.