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 tofork.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()) // 154847000Reading 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)}`)) // 100000000000000000000nCaveats
- 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.
blockTagaccepts abigintblock number or a tag such as'latest'.

