Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Custom Precompiles

Tevm lets you register JavaScript functions as EVM precompiles at fixed addresses — useful for crypto, oracles, complex math, bridges, custom data structures, and test helpers.

Quick Start

Basic Example

import { createTevmNode, definePrecompile, PREFUNDED_ACCOUNTS } from 'tevm'
import { createAddress } from 'tevm/address'
import { createContract } from 'tevm/contract'
import { hexToBytes, parseAbi } from 'tevm/utils'
import { createImpersonatedTx } from 'tevm/tx'
import { EvmError, EvmErrorMessage } from 'tevm/evm'
 
const customPrecompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function double(bytes) returns (bytes)']),
    address: '0x0000000000000000000000000000000000000123'
  }),
  call: async ({ data }) => {
    const input = Array.from(hexToBytes(data))
    return {
      returnValue: new Uint8Array(input.map(byte => Number(byte) * 2)),
      executionGasUsed: 200n,
    }
  },
})
 
const node = createTevmNode({
  customPrecompiles: [customPrecompile.precompile()],
})
 
const tx = createImpersonatedTx({
  impersonatedAddress: createAddress(PREFUNDED_ACCOUNTS[0].address),
  to: customPrecompile.contract.address,
  data: '0x00',
  gasLimit: 100000n,
  maxFeePerGas: 10n,
  maxPriorityFeePerGas: 1n,
})
 
const vm = await node.getVm()
const result = await vm.runTx({ tx })

Complex Example

import { createTevmNode, definePrecompile } from 'tevm'
import { createAddress } from 'tevm/address'
import { createContract } from 'tevm/contract'
import { parseAbi, hexToBytes } from 'tevm/utils'
import { createImpersonatedTx } from 'tevm/tx'
import { EvmError, EvmErrorMessage } from 'tevm/evm'
import { keccak256 } from 'tevm/crypto'
 
const hashPrecompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function hash(bytes) returns (bytes32)']),
    address: '0x0000000000000000000000000000000000000321'
  }),
  call: async ({ data, gasLimit }) => {
    const gasPerByte = 10n
    const gasUsed = BigInt(hexToBytes(data).length) * gasPerByte + 100n
    const hash = keccak256(data)
    return { returnValue: hash, executionGasUsed: gasUsed }
  },
})
 
const node = createTevmNode({
  customPrecompiles: [customPrecompile.precompile(), hashPrecompile.precompile()],
})

Precompile Interface

Every precompile needs an ABI/address and a call handler:

const contract = createContract({
  abi: parseAbi(['function myFunction(uint256) returns (uint256)']),
  address: '0x0000000000000000000000000000000000000123'
})
const call = async ({ data, gasLimit }: PrecompileInput): Promise<PrecompileOutput> => {
  return {
    returnValue: new Uint8Array([/* result data */]),
    executionGasUsed: 1000n,
    // Optional: exceptionError for when the operation fails
  }
}
const myPrecompile = definePrecompile({ contract, call })
 
const node = createTevmNode({
  customPrecompiles: [myPrecompile.precompile()]
})

Example Implementations

JavaScript State Example

const storage = new Map<string, Uint8Array>()
 
const statePrecompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function store(bytes32,bytes32)']),
    address: '0x0000000000000000000000000000000000000124'
  }),
  call: async ({ data, gasLimit }) => {
    const bytes = hexToBytes(data)
    const key = bytes.slice(0, 32)
    const value = bytes.slice(32, 64)
    storage.set(Buffer.from(key).toString('hex'), value)
    return { returnValue: new Uint8Array(), executionGasUsed: 200n }
  },
})

Gas Calculation Example

const gasPrecompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function processWithGas(bytes)']),
    address: '0x0000000000000000000000000000000000000125'
  }),
  call: async ({ data, gasLimit }) => {
    const gasUsed = BigInt(hexToBytes(data).length * 100)
    if (gasUsed > gasLimit) {
      return {
        returnValue: new Uint8Array(),
        exceptionError: new EvmError(EvmErrorMessage.OUT_OF_GAS),
        executionGasUsed: gasLimit,
      }
    }
    return { returnValue: new Uint8Array(), executionGasUsed: gasUsed }
  },
})

Error Handling Example

const errorPrecompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function process(bytes)']),
    address: '0x0000000000000000000000000000000000000126'
  }),
  call: async ({ data, gasLimit }) => {
    try {
      if (data === '0x') {
        throw new Error('Empty input not allowed')
      }
      return { returnValue: processData(data), executionGasUsed: 200n }
    } catch (error) {
      return {
        returnValue: new Uint8Array(),
        exceptionError: new EvmError(`Precompile error: ${error.message}`),
        executionGasUsed: gasLimit,
      }
    }
  },
})

Multiple Precompiles Example

const precompileA = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function processA() returns (bytes)']),
    address: '0x0000000000000000000000000000000000000127'
  }),
  call: async () => ({ returnValue: new Uint8Array([1]), executionGasUsed: 200n }),
})
 
const precompileB = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function processB() returns (bytes)']),
    address: '0x0000000000000000000000000000000000000128'
  }),
  call: async () => ({ returnValue: new Uint8Array([2]), executionGasUsed: 200n }),
})
 
const node = createTevmNode({
  customPrecompiles: [precompileA.precompile(), precompileB.precompile()],
})

Use Cases

  • Cryptographic operations (encryption, hashing, signature verification)
  • Oracle / external data simulation for local testing
  • Complex math that would be gas-intensive in Solidity
  • Cross-chain bridge verification logic
  • Custom data structures (trees, graphs, etc.)
  • Testing helpers (time manipulation, state snapshots)

Best Practices

Gas Calculation

Calculate gas based on actual work performed, similar to native EVM ops.

const precompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function process(bytes)']),
    address: createAddress('0x0000000000000000000000000000000000000123')
  }),
  call: async ({ data, gasLimit }) => {
    const baseGas = 100n
    const dataGas = BigInt(hexToBytes(data).length * 10)
    const totalGas = baseGas + dataGas
 
    if (totalGas > gasLimit) {
      return {
        returnValue: new Uint8Array(),
        exceptionError: new EvmError(EvmErrorMessage.OUT_OF_GAS),
        executionGasUsed: gasLimit,
      }
    }
    return { returnValue: processData(data), executionGasUsed: totalGas }
  },
})

Error Handling

Use appropriate error types and include detailed info.

const precompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function process(bytes32,uint256)']),
    address: createAddress('0x0000000000000000000000000000000000000123')
  }),
  call: async ({ data, gasLimit }) => {
    try {
      if (hexToBytes(data).length < 36) {
        return {
          returnValue: new Uint8Array(),
          exceptionError: new EvmError('Invalid input: insufficient data'),
          executionGasUsed: 100n,
        }
      }
      return { returnValue: result, executionGasUsed: gasUsed }
    } catch (error) {
      console.error('Precompile execution error:', error)
      return {
        returnValue: new Uint8Array(),
        exceptionError: new EvmError(error.message || 'Unknown precompile error'),
        executionGasUsed: Math.min(100n, gasLimit),
      }
    }
  },
})

State Management

For precompiles that maintain state across calls via EVM storage:

const statePrecompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function getData(bytes32) returns (bytes32)', 'function setData(bytes32,bytes32)']),
    address: createAddress('0x0000000000000000000000000000000000000124')
  }),
  call: async ({ data, gasLimit }) => {
    const vm = await node.getVm()
    const stateManager = vm.stateManager
    const address = createAddress(statePrecompile.contract.address)
 
    const selector = data.slice(0, 4)
    const isGetData = selector[0] === 0x9b & selector[1] === 0x18 & selector[2] === 0x30 & selector[3] === 0x4c
 
    if (isGetData) {
      const key = data.slice(4, 36)
      const value = await stateManager.getStorage(address, key)
      return { returnValue: value, executionGasUsed: 200n }
    } else {
      const key = data.slice(4, 36)
      const value = data.slice(36, 68)
      await stateManager.putStorage(address, key, value)
      return { returnValue: new Uint8Array(), executionGasUsed: 500n }
    }
  },
})

Performance

Cache results for expensive deterministic operations.

const precompile = definePrecompile({
  // Contract definition...
  call: async ({ data, gasLimit }) => {
    const cacheKey = data.toString()
    if (resultsCache.has(cacheKey)) {
      return resultsCache.get(cacheKey)
    }
    const result = performExpensiveOperation(data)
    resultsCache.set(cacheKey, { returnValue: result, executionGasUsed: gasUsed })
    return { returnValue: result, executionGasUsed: gasUsed }
  },
})

Related Resources