Forking & Reforking
Forking allows you to create a local copy of an Ethereum network (or any EVM-compatible chain) at a specific point in time. This local copy can be modified without affecting the original network, making it perfect for:
- Testing against production smart contracts and state
- Debugging complex transactions
- Developing with real-world data
- Simulating DeFi protocols and interactions
- Performing "what-if" analysis and scenario testing
Basic Forking
Fork from Mainnet
import { createTevmNode, http } from "tevm";
// Create a node that forks from Ethereum mainnet
const node = createTevmNode({
fork: {
transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY")({}),
blockTag: "latest", // Fork from the latest block
},
});
// Always wait for the node to be ready
await node.ready();
// The node now contains a copy of the entire Ethereum mainnet state
console.log("Forked node ready!");
Fork from Optimism
import { createTevmNode, http } from "tevm";
import { optimism } from "tevm/common";
// Create a node that forks from Optimism
const node = createTevmNode({
fork: {
transport: http("https://mainnet.optimism.io")({}),
blockTag: "latest",
},
common: optimism, // Use Optimism chain configuration
});
await node.ready();
console.log("Optimism fork ready!");
Fork from Specific Block
import { createTevmNode, http } from "tevm";
// Create a node that forks from a specific block
const node = createTevmNode({
fork: {
transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY")({}),
blockTag: 17_500_000n, // Specific block number
},
});
await node.ready();
console.log("Forked from block 17,500,000");
Fork Transport Options
Provider Types
Tevm supports multiple transport types for forking:
import { createTevmNode, http } from "tevm";
import { createPublicClient, http as viemHttp } from "viem";
import { BrowserProvider } from "ethers";
// 1. Using Tevm's http transport (recommended)
const tevmNode1 = createTevmNode({
fork: {
transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
},
});
// 2. Using viem's PublicClient
const publicClient = createPublicClient({
transport: viemHttp("https://mainnet.infura.io/v3/YOUR-KEY"),
});
const tevmNode2 = createTevmNode({
fork: {
transport: publicClient,
},
});
// 3. Using Ethers.js Provider (must be wrapped to match EIP-1193)
const ethersProvider = new BrowserProvider(window.ethereum);
const tevmNode3 = createTevmNode({
fork: {
transport: {
request: async ({ method, params }) => {
return await ethersProvider.send(method, params || []);
},
},
},
});
Provider Selection
Each provider type has different characteristics:
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 implements lazy loading with local caching for forked state:
-
Initial Fork: When you create a forked node, Tevm doesn't immediately copy the entire blockchain state.
-
Lazy Loading: When your code accesses specific accounts or storage slots, Tevm fetches only that data from the remote provider.
-
Local Cache: Fetched data is stored in a local cache, so subsequent reads are fast and don't require network requests.
-
Local Modifications: Any changes you make (e.g., account balance changes, contract deployments) are stored locally and take precedence over the forked state.
This approach provides the perfect balance between having a complete blockchain copy and efficient memory usage.
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");
// First access - fetches from remote provider
console.log("Fetching DAI contract from mainnet...");
const t0 = performance.now();
const daiContract = await vm.stateManager.getAccount(daiAddress);
console.log(`First access: ${performance.now() - t0}ms`);
// Second access - uses local cache
const t1 = performance.now();
await vm.stateManager.getAccount(daiAddress);
console.log(`Cached access: ${performance.now() - t1}ms`);
// The difference in timing demonstrates the caching in action
Reforking Strategies
There are two primary approaches to reforking in Tevm:
1. Using a Node as Transport
This is the recommended approach for most scenarios as it's more memory efficient:
import { createTevmNode, http, hexToBigInt } from "tevm";
import { requestEip1193 } from "tevm/decorators";
// Step 1: Create the source node with EIP-1193 support
const sourceNode = createTevmNode({
fork: {
transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
blockTag: 17_000_000n,
},
}).extend(requestEip1193());
await sourceNode.ready();
// Step 2: Get the current block number
const currentBlock = await sourceNode.request({
method: "eth_blockNumber",
});
// Step 3: Create a new node that forks from the source node
const forkNode = createTevmNode({
fork: {
transport: sourceNode, // Use the source node as a transport
blockTag: hexToBigInt(currentBlock),
},
});
await forkNode.ready();
// Now forkNode is a fork of sourceNode
// Changes in forkNode won't affect sourceNode
This approach has several advantages:
- Memory Efficiency: Reuses the cached state from the source node
- Performance: Avoids duplicate network requests for the same data
- Isolation: Changes in the new fork don't affect the source node
- Flexibility: Can create multiple forks from the same source
2. Using Deep Copy
For complete isolation, you can create an independent copy:
import { createTevmNode, http } from "tevm";
// Step 1: Create the original node
const originalNode = createTevmNode({
fork: {
transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
},
});
await originalNode.ready();
// Let the node fetch some state
// ... perform operations ...
// Step 2: Create a completely independent copy
const independentNode = await originalNode.deepCopy();
// Now independentNode is a complete copy of originalNode with its own state
// Changes to either node won't affect the other
Working with Forked State
Reading State
Access the state of accounts, contracts, and storage:
import { createTevmNode, http } from "tevm";
import { createAddress } from "tevm/address";
import { formatEther, hexToBytes } from "viem";
// Create a node forked from mainnet
const node = createTevmNode({
fork: {
transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
},
});
await node.ready();
const vm = await node.getVm();
// 1. Read an EOA (externally owned account) state
const vitalikAddress = createAddress(
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
);
const vitalikAccount = await vm.stateManager.getAccount(vitalikAddress);
if (vitalikAccount) {
console.log(`Vitalik's balance: ${formatEther(vitalikAccount.balance)} ETH`);
console.log(`Nonce: ${vitalikAccount.nonce}`);
}
// 2. Read a contract's code
const uniswapV2Router = createAddress(
"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
);
const routerAccount = await vm.stateManager.getAccount(uniswapV2Router);
const routerCode = await vm.stateManager.getContractCode(uniswapV2Router);
console.log(`Uniswap Router code size: ${routerCode.length} bytes`);
// 3. Read contract storage
const slot0 = await vm.stateManager.getContractStorage(
uniswapV2Router,
hexToBytes(
"0x0000000000000000000000000000000000000000000000000000000000000000",
),
);
console.log(`Storage slot 0: ${slot0}`);
// 4. Execute view function via low-level call
const result = await vm.evm.runCall({
to: uniswapV2Router,
data: hexToBytes("0xc45a0155"), // factory() function selector
gasLimit: 100000n,
});
console.log("Factory address:", result.execResult.returnValue);
Modifying State
Make changes to the forked state:
import { createTevmNode, http } from "tevm";
import { createAddress } from "tevm/address";
import { parseEther, formatEther } 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 account balance
const testAddress = createAddress("0x1234567890123456789012345678901234567890");
let account = await vm.stateManager.getAccount(testAddress);
// If account doesn't exist yet, create it
if (!account) {
account = {
nonce: 0n,
balance: 0n,
storageRoot:
"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
codeHash:
"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
};
}
// Add 100 ETH to the account
account.balance += parseEther("100");
await vm.stateManager.putAccount(testAddress, account);
// Verify the change
const updatedAccount = await vm.stateManager.getAccount(testAddress);
console.log(`New balance: ${formatEther(updatedAccount.balance)} ETH`);
// 2. Impersonate an account
node.setImpersonatedAccount("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"); // Vitalik
// Now you can send transactions as this account
const txResult = await vm.runTx({
tx: {
from: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
to: testAddress,
value: parseEther("1"),
gasLimit: 21000n,
},
});
console.log(`Transaction success: ${!txResult.execResult.exceptionError}`);
console.log(`Gas used: ${txResult.gasUsed}`);
// 3. Modify contract storage directly
const uniswapV3Factory = createAddress(
"0x1F98431c8aD98523631AE4a59f267346ea31F984",
);
await vm.stateManager.putContractStorage(
uniswapV3Factory,
hexToBytes(
"0x0000000000000000000000000000000000000000000000000000000000000000",
),
hexToBytes(
"0x0000000000000000000000001234567890123456789012345678901234567890",
),
);
Advanced Use Cases
- 🔄 DeFi Protocol Testing - Test complex DeFi interactions like flash loans, liquidations, and arbitrage
- 🔒 Vulnerability Analysis - Analyze smart contract security by manipulating state to trigger edge cases
- 🗳️ Governance Simulation - Simulate DAO votes and governance proposals
- 💰 MEV Strategy Testing - Develop and test MEV strategies without risking real capital
DeFi Protocol Example
Simulate a complex DeFi interaction:
import { createTevmNode, http } from "tevm";
import { createAddress } from "tevm/address";
import { hexToBytes, parseEther } from "viem";
async function simulateFlashLoan() {
const node = createTevmNode({
fork: {
transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
},
});
await node.ready();
// Setup our test account
const trader = createAddress("0x1234567890123456789012345678901234567890");
const vm = await node.getVm();
// Give account some ETH
await vm.stateManager.putAccount(trader, {
nonce: 0n,
balance: parseEther("10"),
storageRoot:
"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
codeHash:
"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
});
// AAVE V2 lending pool address
const aaveLendingPool = createAddress(
"0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9",
);
// Encode a flashLoan call (simplified)
// In reality you'd need to encode the parameters correctly
const flashLoanCalldata = hexToBytes("0xab9c4b5d...");
// Execute the flash loan
const result = await vm.runTx({
tx: {
from: trader,
to: aaveLendingPool,
data: flashLoanCalldata,
gasLimit: 5000000n,
},
});
console.log("Flash loan simulation result:");
console.log(`Success: ${!result.execResult.exceptionError}`);
if (result.execResult.exceptionError) {
console.log(`Error: ${result.execResult.exceptionError}`);
} else {
console.log(`Gas used: ${result.gasUsed}`);
}
// Check ending balance
const finalAccount = await vm.stateManager.getAccount(trader);
console.log(`Final balance: ${finalAccount.balance}`);
}
// Run the simulation
simulateFlashLoan();
Performance Optimization
Efficient State Access
import { createTevmNode, http } from "tevm";
import { createAddress } from "tevm/address";
// Create a forked node
const node = createTevmNode({
fork: {
transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
// Specify a block to avoid moving target issues
blockTag: 17_000_000n,
},
});
await node.ready();
// Access pattern optimization
async function optimizedStateAccess() {
const vm = await node.getVm();
const stateManager = vm.stateManager;
// ✅ Batch related state requests
const addresses = [
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
"0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT
"0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI
].map(createAddress);
// Fetch all in parallel
const accounts = await Promise.all(
addresses.map((addr) => stateManager.getAccount(addr)),
);
// ✅ Reuse state manager for multiple operations
const usdcAddress = addresses[0];
// Access code and storage from the same contract
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
For some tests, you might not need the entire state of mainnet:
// Specify chainId for minimal setup when you don't need a full fork
const lightNode = createTevmNode({
// No fork specified - creates a minimal local node
common: { chainId: 1 },
});
// Only fork when you need production data
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
For critical paths, pre-warm the cache:
// Pre-warm the cache for frequently accessed contracts
async function warmCache(node) {
const vm = await node.getVm();
const contracts = [
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", // UniswapV2Router
].map(createAddress);
console.log("Warming cache...");
await Promise.all(
contracts.map((addr) =>
vm.stateManager
.getAccount(addr)
.then((account) => vm.stateManager.getContractCode(addr)),
),
);
console.log("Cache warmed!");
}
Best Practices
1. RPC Provider Setup
// ✅ Always call http transport with empty object
const node = createTevmNode({
fork: {
transport: http("https://ethereum.quicknode.com/YOUR-API-KEY")({}),
},
});
// ✅ Specify a block number for deterministic tests
const node = createTevmNode({
fork: {
transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
blockTag: 17_000_000n, // Fixed block for reproducibility
},
});
2. Error Handling
// ✅ Add proper error handling for RPC failures
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. Consider using a different provider.",
);
} else if (error.message.includes("network")) {
console.error("Network error. Check your internet connection.");
} else {
console.error("Fork initialization failed:", error);
}
}
3. State Handling
// ✅ Use proper null checks
const account = await vm.stateManager.getAccount(address);
if (account) {
// Account exists, safe to use
account.balance += parseEther("1");
await vm.stateManager.putAccount(address, account);
}
// ✅ Handle potential RPC failures in state access
try {
const code = await vm.stateManager.getContractCode(contractAddress);
// Use code
} catch (error) {
console.error("Failed to fetch contract code:", error);
// Implement fallback or retry logic
}
4. Performance Considerations
// ✅ Choose the right block for your needs
const node = createTevmNode({
fork: {
transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
// For recent transactions, use 'latest'
// For reproducible tests, use a specific number
// For historical analysis, use a past block
blockTag: process.env.NODE_ENV === "test" ? 17_000_000n : "latest",
},
});
// ✅ Remember that first access is slower (RPC call)
// but subsequent accesses are fast (from cache)
5. Testing Setup
// ✅ For test isolation, use a fresh fork for each test
beforeEach(async () => {
node = createTevmNode({
fork: {
transport: http("https://mainnet.infura.io/v3/YOUR-KEY")({}),
blockTag: 17_000_000n,
},
});
await node.ready();
});
// ✅ For test efficiency, reuse the same fork but reset state
let baseNode;
before(async () => {
baseNode = createTevmNode({
fork: {
/* ... */
},
});
await baseNode.ready();
});
beforeEach(async () => {
// Clone the base node for each test
node = await baseNode.deepCopy();
});
Next Steps
- State Management - Learn more about manipulating blockchain state
- Mining Modes - Configure how transactions are mined in your forked node
- Transaction Pool - Understand how to work with pending transactions
- Forking Example - See a complete example of forking mainnet