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

Forking & Reforking

Forking creates a local copy of an Ethereum (or any EVM) network at a point in time. Use it for:

  • Testing against production contracts and state
  • Debugging complex transactions
  • Developing with real-world data
  • Simulating DeFi protocols
  • "What-if" analysis

Basic Forking

Fork from Mainnet

import { createTevmNode, http } from "tevm";
 
const node = createTevmNode({
  fork: {
    transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY")({}),
    blockTag: "latest",
  },
});
 
await node.ready();

Fork from Optimism

import { createTevmNode, http } from "tevm";
import { optimism } from "tevm/common";
 
const node = createTevmNode({
  fork: {
    transport: http("https://mainnet.optimism.io")({}),
    blockTag: "latest",
  },
  common: optimism,
});
 
await node.ready();

Fork from a Specific Block

import { createTevmNode, http } from "tevm";
 
const node = createTevmNode({
  fork: {
    transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY")({}),
    blockTag: 17_500_000n,
  },
});
 
await node.ready();

Fork Transport Options

Tevm forks from any EIP-1193 compatible provider:

import { createTevmNode, http } from "tevm";
import { createPublicClient, http as viemHttp } from "viem";
import { BrowserProvider } from "ethers";
 
// 1. Tevm http transport (recommended)
const tevmNode1 = createTevmNode({
  fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}) },
});
 
// 2. viem PublicClient
const publicClient = createPublicClient({
  transport: viemHttp("https://mainnet.infura.io/v3/YOUR-KEY"),
});
const tevmNode2 = createTevmNode({
  fork: { transport: publicClient },
});
 
// 3. Ethers.js Provider (wrap to match EIP-1193)
const ethersProvider = new BrowserProvider(window.ethereum);
const tevmNode3 = createTevmNode({
  fork: {
    transport: {
      request: async ({ method, params }) =>
        ethersProvider.send(method, params || []),
    },
  },
});

Provider Selection

ProviderProsCons
http from TevmOptimized for Tevm, minimal dependenciesLimited middleware support
Public RPC nodesFree, easy to useRate limits, may be slow
Alchemy/Infura/etcFast, reliable, archive dataRequires API key, may have costs
Local Ethereum nodeFull control, no rate limitsResource intensive to run
Another Tevm instanceMemory efficientAdds complexity

How Forking Works

Tevm uses lazy loading with local caching:

  1. Initial Fork: no immediate copy of the chain.
  2. Lazy Loading: only accessed accounts/slots are fetched from the remote.
  3. Local Cache: fetched data is cached locally; subsequent reads skip the network.
  4. Local Modifications: writes are stored locally and override forked state.
import { createTevmNode, http } from "tevm";
import { createAddress } from "tevm/address";
import { performance } from "node:perf_hooks";
 
const node = createTevmNode({
  fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}) },
});
await node.ready();
 
const vm = await node.getVm();
const daiAddress = createAddress("0x6B175474E89094C44Da98b954EedeAC495271d0F");
 
const t0 = performance.now();
const daiContract = await vm.stateManager.getAccount(daiAddress);
console.log(`First access: ${performance.now() - t0}ms`);
 
const t1 = performance.now();
await vm.stateManager.getAccount(daiAddress);
console.log(`Cached access: ${performance.now() - t1}ms`);

Reforking Strategies

Two approaches:

1. Use a Node as Transport (recommended)

Memory-efficient — reuses the source's cached state:

import { createTevmNode, http, hexToBigInt } from "tevm";
import { requestEip1193 } from "tevm/decorators";
 
const sourceNode = createTevmNode({
  fork: {
    transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
    blockTag: 17_000_000n,
  },
}).extend(requestEip1193());
 
await sourceNode.ready();
 
const currentBlock = await sourceNode.request({ method: "eth_blockNumber" });
 
const forkNode = createTevmNode({
  fork: {
    transport: sourceNode,
    blockTag: hexToBigInt(currentBlock),
  },
});
 
await forkNode.ready();
// Changes in forkNode do not affect sourceNode

Advantages: memory-efficient, avoids duplicate fetches, isolated, multi-fork from one source.

2. Deep Copy

For full isolation (memory-intensive):

import { createTevmNode, http } from "tevm";
 
const originalNode = createTevmNode({
  fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}) },
});
await originalNode.ready();
 
const independentNode = await originalNode.deepCopy();

Working with Forked State

Reading State

import { createTevmNode, http } from "tevm";
import { createAddress } from "tevm/address";
import { formatEther, hexToBytes } from "viem";
 
const node = createTevmNode({
  fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}) },
});
await node.ready();
 
const vm = await node.getVm();
 
// 1. EOA state
const vitalikAddress = createAddress("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
const vitalikAccount = await vm.stateManager.getAccount(vitalikAddress);
if (vitalikAccount) {
  console.log(`Balance: ${formatEther(vitalikAccount.balance)} ETH`);
  console.log(`Nonce: ${vitalikAccount.nonce}`);
}
 
// 2. Contract code
const uniswapV2Router = createAddress("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D");
const routerCode = await vm.stateManager.getContractCode(uniswapV2Router);
console.log(`Code size: ${routerCode.length} bytes`);
 
// 3. Storage
const slot0 = await vm.stateManager.getContractStorage(
  uniswapV2Router,
  hexToBytes("0x0000000000000000000000000000000000000000000000000000000000000000"),
);
 
// 4. Low-level call
const result = await vm.evm.runCall({
  to: uniswapV2Router,
  data: hexToBytes("0xc45a0155"), // factory()
  gasLimit: 100000n,
});
console.log("Factory address:", result.execResult.returnValue);

Modifying State

import { createTevmNode, http } from "tevm";
import { createAddress } from "tevm/address";
import { createImpersonatedTx } from "tevm/tx";
import { parseEther, formatEther, hexToBytes } from "viem";
 
const node = createTevmNode({
  fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}) },
});
await node.ready();
 
const vm = await node.getVm();
 
// 1. Modify balance
const testAddress = createAddress("0x1234567890123456789012345678901234567890");
let account = await vm.stateManager.getAccount(testAddress);
if (!account) {
  account = {
    nonce: 0n,
    balance: 0n,
    storageRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
    codeHash: "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
  };
}
account.balance += parseEther("100");
await vm.stateManager.putAccount(testAddress, account);
 
// 2. Impersonate
node.setImpersonatedAccount("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
 
const tx = createImpersonatedTx({
  from: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  to: testAddress,
  value: parseEther("1"),
  gasLimit: 21000n,
});
 
const txResult = await vm.runTx({ tx });
console.log(`Success: ${!txResult.execResult.exceptionError}`);
console.log(`Gas used: ${txResult.gasUsed}`);
 
// 3. Direct storage write
const uniswapV3Factory = createAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984");
await vm.stateManager.putContractStorage(
  uniswapV3Factory,
  hexToBytes("0x0000000000000000000000000000000000000000000000000000000000000000"),
  hexToBytes("0x0000000000000000000000001234567890123456789012345678901234567890"),
);

Advanced Use Cases

  • DeFi protocol testing — flash loans, liquidations, arbitrage
  • Vulnerability analysis — manipulate state to trigger edge cases
  • Governance simulation — DAO votes, proposals
  • MEV strategy testing — without risking real capital

DeFi Protocol Example

import { createTevmNode, http } from "tevm";
import { createAddress } from "tevm/address";
import { createImpersonatedTx } from "tevm/tx";
import { hexToBytes, parseEther } from "viem";
 
async function simulateFlashLoan() {
  const node = createTevmNode({
    fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}) },
  });
  await node.ready();
 
  const trader = createAddress("0x1234567890123456789012345678901234567890");
  const vm = await node.getVm();
 
  await vm.stateManager.putAccount(trader, {
    nonce: 0n,
    balance: parseEther("10"),
    storageRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
    codeHash: "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
  });
 
  const aaveLendingPool = createAddress("0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9");
  const flashLoanCalldata = hexToBytes("0xab9c4b5d...");
 
  const tx = createImpersonatedTx({
    from: trader,
    to: aaveLendingPool,
    data: flashLoanCalldata,
    gasLimit: 5000000n,
  });
 
  const result = await vm.runTx({ tx });
  console.log(`Success: ${!result.execResult.exceptionError}`);
  if (result.execResult.exceptionError) {
    console.log(`Error: ${result.execResult.exceptionError}`);
  } else {
    console.log(`Gas used: ${result.gasUsed}`);
  }
 
  const finalAccount = await vm.stateManager.getAccount(trader);
  console.log(`Final balance: ${finalAccount.balance}`);
}
 
simulateFlashLoan();

Performance Optimization

Efficient State Access

import { createTevmNode, http } from "tevm";
import { createAddress } from "tevm/address";
 
const node = createTevmNode({
  fork: {
    transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
    blockTag: 17_000_000n, // Pin block to avoid moving target
  },
});
await node.ready();
 
async function optimizedStateAccess() {
  const vm = await node.getVm();
  const stateManager = vm.stateManager;
 
  // Batch related requests
  const addresses = [
    "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
    "0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT
    "0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI
  ].map(createAddress);
 
  const accounts = await Promise.all(
    addresses.map((addr) => stateManager.getAccount(addr)),
  );
 
  // Parallel code + storage reads
  const usdcAddress = addresses[0];
  const [code, slot0, slot1] = await Promise.all([
    stateManager.getContractCode(usdcAddress),
    stateManager.getContractStorage(usdcAddress, Buffer.from("0".padStart(64, "0"), "hex")),
    stateManager.getContractStorage(usdcAddress, Buffer.from("1".padStart(64, "0"), "hex")),
  ]);
 
  return { accounts, code, storage: [slot0, slot1] };
}

Selective Forking

Skip forking when you don't need real state:

// Minimal local node
const lightNode = createTevmNode({
  common: { chainId: 1 },
});
 
// Conditional fork
const conditionalFork =
  process.env.USE_FORK === "true"
    ? {
        transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
        blockTag: "latest",
      }
    : undefined;
 
const node = createTevmNode({ fork: conditionalFork });

Cache Warmer

Pre-fetch hot contracts:

async function warmCache(node) {
  const vm = await node.getVm();
  const contracts = [
    "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
    "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
    "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", // UniswapV2Router
  ].map(createAddress);
 
  await Promise.all(
    contracts.map((addr) =>
      vm.stateManager.getAccount(addr).then(() => vm.stateManager.getContractCode(addr)),
    ),
  );
}

Best Practices

1. RPC Provider Setup

// Call http transport with empty object
const node = createTevmNode({
  fork: { transport: http("https://ethereum.quicknode.com/YOUR-API-KEY")({}) },
});
 
// Pin a block for deterministic tests
const node = createTevmNode({
  fork: {
    transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
    blockTag: 17_000_000n,
  },
});

2. Error Handling

try {
  const node = createTevmNode({
    fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}) },
  });
  await node.ready();
} catch (error) {
  if (error.message.includes("rate limit")) {
    console.error("RPC rate limit exceeded.");
  } else if (error.message.includes("network")) {
    console.error("Network error.");
  } else {
    console.error("Fork init failed:", error);
  }
}

3. State Handling

// Null-check accounts
const account = await vm.stateManager.getAccount(address);
if (account) {
  account.balance += parseEther("1");
  await vm.stateManager.putAccount(address, account);
}
 
// Handle RPC failures
try {
  const code = await vm.stateManager.getContractCode(contractAddress);
} catch (error) {
  console.error("Failed to fetch contract code:", error);
}

4. Performance

const node = createTevmNode({
  fork: {
    transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
    // 'latest' for recent txs; pinned number for reproducibility
    blockTag: process.env.NODE_ENV === "test" ? 17_000_000n : "latest",
  },
});
// First access: slow (RPC); subsequent: fast (cache).

5. Testing Setup

// Fresh fork per test (isolation)
beforeEach(async () => {
  node = createTevmNode({
    fork: {
      transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
      blockTag: 17_000_000n,
    },
  });
  await node.ready();
});
 
// Or: shared base + deepCopy per test
let baseNode;
before(async () => {
  baseNode = createTevmNode({ fork: { /* ... */ } });
  await baseNode.ready();
});
beforeEach(async () => {
  node = await baseNode.deepCopy();
});

Next Steps

State Management · Mining Modes · Transaction Pool · Forking Example