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
| Provider | Pros | Cons |
|---|---|---|
http from Tevm | Optimized for Tevm, minimal dependencies | Limited middleware support |
| Public RPC nodes | Free, easy to use | Rate limits, may be slow |
| Alchemy/Infura/etc | Fast, reliable, archive data | Requires API key, may have costs |
| Local Ethereum node | Full control, no rate limits | Resource intensive to run |
| Another Tevm instance | Memory efficient | Adds complexity |
How Forking Works
Tevm uses lazy loading with local caching:
- Initial Fork: no immediate copy of the chain.
- Lazy Loading: only accessed accounts/slots are fetched from the remote.
- Local Cache: fetched data is cached locally; subsequent reads skip the network.
- 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 sourceNodeAdvantages: 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

