Skip to content

Custom Precompiles

Precompiled contracts are special EVM contracts with native implementations at fixed addresses. Tevm lets you create your own precompiles in JavaScript to add custom functionality, improve performance, or implement new features directly in the EVM.

What Are Precompiles?

Precompile Types

  • Ethereum Native ๐Ÿงฉ - Standard precompiles include hash functions, curve operations, and more at reserved addresses
  • Custom JavaScript โšก - Tevm lets you add your own JavaScript functions as precompiles
  • Performance ๐Ÿš€ - Native code is much faster than EVM bytecode for complex operations
  • Cross-Chain ๐ŸŒ‰ - Many L2s use precompiles to implement chain-specific functionality

Quick Start

Basic Example

import { createTevmNode, definePrecompile } from 'tevm'
import { createAddress } from 'tevm/address'
import { createContract } from 'tevm/contract'
import { parseAbi } from 'tevm/utils'
import { createImpersonatedTx } from 'tevm/tx'
import { EvmError, EvmErrorMessage } from 'tevm/evm'
 
// Create a basic precompile that doubles each byte
const customPrecompile = definePrecompile({ 
  contract: createContract({ 
    abi: parseAbi(['function double(bytes) returns (bytes)']), 
    address: '0x0000000000000000000000000000000000000123'
  }), 
  call: async ({ data }) => { 
    const input = Array.from(data) 
    return { 
      returnValue: new Uint8Array(input.map(byte => Number(byte) * 2)), 
      executionGasUsed: 200n, 
    } 
  }, 
})
 
// Create node with the precompile
const node = createTevmNode({ 
  customPrecompiles: [customPrecompile.precompile()], 
})
 
// Create a transaction to call the precompile
const tx = createImpersonatedTx({ 
  impersonatedAddress: createAddress('0x1234567890123456789012345678901234567890'), 
  to: customPrecompile.contract.address, 
  data: '0x00', 
  gasLimit: 21000n, 
}) 
 
// Execute the transaction
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'
 
// Create a cryptographic hash precompile
const hashPrecompile = definePrecompile({ 
  contract: createContract({ 
    abi: parseAbi(['function hash(bytes) returns (bytes32)']), 
    address: '0x0000000000000000000000000000000000000321'
  }), 
  call: async ({ data, gasLimit }) => { 
    // Calculate gas cost based on input size
    const gasPerByte = 10n
    const gasUsed = BigInt(data.length) * gasPerByte + 100n // Base cost + per-byte cost
    
    // Perform the hash operation
    const hash = keccak256(data)
    
    return { returnValue: hash, executionGasUsed: gasUsed } 
  }, 
})
 
// Create a node with multiple precompiles
const node = createTevmNode({ 
  customPrecompiles: [customPrecompile.precompile(), hashPrecompile.precompile()], 
})

Precompile Interface

Define the Contract Interface

Every precompile needs an ABI and address:

const contract = createContract({
  abi: parseAbi(['function myFunction(uint256) returns (uint256)']),
  address: '0x0000000000000000000000000000000000000123'
})

Implement the Call Handler

The call handler receives the input data and gas limit:

const call = async ({ data, gasLimit }: PrecompileInput): Promise<PrecompileOutput> => {
  // Process input data
  // ...
  
  return {
    returnValue: new Uint8Array([/* result data */]),
    executionGasUsed: 1000n,
    // Optional: exceptionError for when the operation fails
  }
}

Create and Register the Precompile

Combine both parts and register with a Tevm Node:

const myPrecompile = definePrecompile({ contract, call })
 
// Create a node with the precompile
const node = createTevmNode({
  customPrecompiles: [
    myPrecompile.precompile()
  ]
})

Example Implementations

State Access Example

const statePrecompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function store(bytes32,bytes32)']),
    address: '0x0000000000000000000000000000000000000124'
  }),
  call: async ({ data, gasLimit }) => {
    // Extract key and value from input data
    const key = data.slice(0, 32)
    const value = data.slice(32)
    
    // Get VM and state manager
    const vm = await node.getVm()
    
    // Store the value at the specified key
    await vm.stateManager.putContractStorage(
      createAddress(statePrecompile.contract.address),
      key,
      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 }) => {
    // Charge 100 gas per byte
    const gasUsed = BigInt(data.length * 100)
    
    // Check if we have enough gas
    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 {
      // Validate input
      if (data.length === 0) {
        return {
          returnValue: new Uint8Array(),
          exceptionError: new EvmError('Custom error: Empty input not allowed'),
          executionGasUsed: 200n,
        }
      }
      
      // Process data
      return { returnValue: processData(data), executionGasUsed: 200n }
    } catch (error) {
      // Handle unexpected errors
      return {
        returnValue: new Uint8Array(),
        exceptionError: new EvmError(`Precompile error: ${error.message}`),
        executionGasUsed: gasLimit,
      }
    }
  },
})

Multiple Precompiles Example

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

Use Cases

Common Use Cases

  • ๐Ÿ” Cryptographic Operations - Implement efficient cryptographic operations like encryption, hashing, or signature verification
  • ๐Ÿ”ฎ Oracle Functionality - Simulate oracles or external data sources during local testing
  • ๐Ÿงฎ Complex Math - Perform complex mathematical calculations that would be gas-intensive in Solidity
  • ๐ŸŒ‰ Cross-Chain Bridges - Simulate cross-chain verification logic for testing bridge implementations
  • ๐Ÿ“Š Custom Data Structures - Implement efficient data structure operations (trees, graphs, etc.)
  • ๐Ÿงช Testing Helpers - Create special testing functions like time manipulation or state snapshots

Best Practices

Gas Calculation

const precompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function process(bytes)']),
    address: createAddress('0x0000000000000000000000000000000000000123')
  }),
  call: async ({ data, gasLimit }) => {
    // Calculate gas based on input size and operations
    const baseGas = 100n;                     // Base cost
    const dataGas = BigInt(data.length * 10); // Per-byte cost
    const totalGas = baseGas + dataGas;       // Total cost
    
    // Check gas limit
    if (totalGas > gasLimit) {
      return {
        returnValue: new Uint8Array(),
        exceptionError: new EvmError(EvmErrorMessage.OUT_OF_GAS),
        executionGasUsed: gasLimit,
      }
    }
    
    // Process data
    return { returnValue: processData(data), executionGasUsed: totalGas }
  },
})

Error Handling

const precompile = definePrecompile({
  contract: createContract({
    abi: parseAbi(['function process(bytes32,uint256)']),
    address: createAddress('0x0000000000000000000000000000000000000123')
  }),
  call: async ({ data, gasLimit }) => {
    try {
      // Validate input format
      if (data.length < 36) {
        return {
          returnValue: new Uint8Array(),
          exceptionError: new EvmError('Invalid input: insufficient data'),
          executionGasUsed: 100n,
        }
      }
      
      // Additional validation and processing
      // ...
      
      return { returnValue: result, executionGasUsed: gasUsed }
    } catch (error) {
      // Log error for debugging (will not be visible to the transaction caller)
      console.error('Precompile execution error:', error);
      
      // Return appropriate error to the EVM
      return {
        returnValue: new Uint8Array(),
        exceptionError: new EvmError(error.message || 'Unknown precompile error'),
        executionGasUsed: Math.min(100n, gasLimit), // Charge some minimum gas
      }
    }
  },
})

State Management

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);
    
    // Parse function selector
    const selector = data.slice(0, 4);
    const isGetData = selector[0] === 0x9b & selector[1] === 0x18 & selector[2] === 0x30 & selector[3] === 0x4c;
    
    if (isGetData) {
      // Read from state
      const key = data.slice(4, 36);
      const value = await stateManager.getContractStorage(address, key);
      return { returnValue: value, executionGasUsed: 200n };
    } else {
      // Write to state
      const key = data.slice(4, 36);
      const value = data.slice(36, 68);
      await stateManager.putContractStorage(address, key, value);
      return { returnValue: new Uint8Array(), executionGasUsed: 500n };
    }
  },
})

Performance

const precompile = definePrecompile({
  // Contract definition...
  call: async ({ data, gasLimit }) => {
    // For expensive operations, consider caching results
    const cacheKey = data.toString();
    if (resultsCache.has(cacheKey)) {
      return resultsCache.get(cacheKey);
    }
    
    // Perform computation
    const result = performExpensiveOperation(data);
    
    // Cache the result
    resultsCache.set(cacheKey, { returnValue: result, executionGasUsed: gasUsed });
    
    return { returnValue: result, executionGasUsed: gasUsed };
  },
})

Related Resources