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'
 
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 { createContract } from 'tevm/contract'
import { parseAbi, hexToBytes, keccak256 } from 'tevm/utils'
 
const hashPrecompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function hash(bytes) returns (bytes32)']),
    address: '0x0000000000000000000000000000000000000321'
  }),
  call: async ({ data }) => {
    const gasPerByte = 10n
    const gasUsed = BigInt(hexToBytes(data).length) * gasPerByte + 100n
    const hash = hexToBytes(keccak256(data))
    return { returnValue: hash, executionGasUsed: gasUsed }
  },
})
 
const node = createTevmNode({
  customPrecompiles: [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('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 instanceof Error ? error.message : String(error)}`,
        ),
        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('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 instanceof Error ? error.message : 'Unknown precompile error',
        ),
        executionGasUsed: gasLimit < 100n ? gasLimit : 100n,
      }
    }
  },
})

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