Skip to content
LogoLogo

Forking a network

A forked provider lazily fetches state (accounts, storage slots, bytecode, blocks) from an upstream RPC the first time you touch it, and keeps every write local. It is the fastest way to run app code or tests against real chain state.

Basic fork

import {  } from '@tevm/ethers'
import {  } from 'viem'
 
const  = await .createMemoryProvider({
	: { : ('https://mainnet.optimism.io')({}) },
})
 
const  = await .getBlockNumber()
.() // the block height the fork was pinned to

fork.transport is a viem transport instance, not a URL — note the ({}) that instantiates the transport returned by http(...).

Pinning to a block

Always pin a block in tests. An unpinned fork follows the chain tip, so results change from run to run and the upstream cache is useless.

import {  } from '@tevm/ethers'
import {  } from 'viem'
 
const  = await .createMemoryProvider({
	: {
		: ('https://mainnet.optimism.io')({}),
		: 154_847_000n,
	},
})
 
.(await .getBlockNumber()) // 154847000

Reading forked contracts with ethers

Once forked, any contract on the upstream chain is readable through an ordinary ethers Contract. Use the typed Contract export to get inferred types for free.

import { ,  } from '@tevm/ethers'
import {  } from 'viem'
 
const  = [
	{
		: 'function',
		: 'name',
		: 'view',
		: [],
		: [{ : '', : 'string' }],
	},
	{
		: 'function',
		: 'balanceOf',
		: 'view',
		: [{ : 'account', : 'address' }],
		: [{ : '', : 'uint256' }],
	},
] as 
 
const  = await .createMemoryProvider({
	: {
		: ('https://mainnet.optimism.io')({}),
		: 154_847_000n,
	},
})
 
// USDC on OP Mainnet
const  = new ('0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', , )
 
.(await .name()) // 'USD Coin'
 
const  = await .balanceOf('0x0000000000000000000000000000000000000001')
.(typeof ) // 'bigint'

Overwriting forked state

Local writes never touch the upstream chain. This is what makes forks useful for "what if" scenarios: give yourself tokens, replace bytecode, or impersonate a whale.

import {  } from '@tevm/ethers'
import {  } from 'viem'
import {  } from 'ethers'
 
const  = await .createMemoryProvider({
	: {
		: ('https://mainnet.optimism.io')({}),
		: 154_847_000n,
	},
	: { : 'auto' },
})
 
const  = '0x0000000000000000000000000000000000000001'
 
// Give the address 100 ETH locally.
await .tevm.setAccount({ : , : ('100') })
.(await .getBalance()) // 100000000000000000000n
 
// Give it an ERC20 balance by writing the token's storage slot for you.
await .tevm.deal({
	: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85',
	: ,
	: 1_000_000n,
})

deal locates the balance slot of the token and writes it, so it works on tokens that have no mint function.

Snapshotting a fork

dumpState / loadState let you pay the fork's RPC cost once and restore that state instantly — useful as a test fixture.

import {  } from '@tevm/ethers'
import {  } from 'viem'
import {  } from 'ethers'
 
const  = await .createMemoryProvider({
	: {
		: ('https://mainnet.optimism.io')({}),
		: 154_847_000n,
	},
})
 
await .tevm.setAccount({ : `0x${'42'.(20)}`, : ('100') })
 
const  = await .tevm.dumpState()
 
// Later — or in another test — restore without any network access.
const  = await .createMemoryProvider({})
await .tevm.loadState({ : .state })
 
.(await .getBalance(`0x${'42'.(20)}`)) // 100000000000000000000n

Caveats

  • The first read of each account or storage slot costs an upstream RPC round trip; subsequent reads are cached in memory.
  • A public RPC will rate limit a large test suite. Use your own endpoint for CI.
  • blockTag accepts a bigint block number or a tag such as 'latest'.