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 }
},
})
