Skip to content

sim:fork

Fork the blockchain and execute commands in a simulation.

sim:fork <block>
NameTypeDescription
blockblockCommands to execute in the fork
NameTypeDescription
--block-numbernumberBlock number to fork from
--fromaddressDefault sender address
--auth-tokenstringRPC provider authentication token
--usingsimulation-modeSimulation backend (anvil, hardhat, tenderly, tenderly-multichain, ethereumjs, revm)
# Fork and set account balance
sim:fork --using anvil (
sim:set-balance @me 100e18
)
  • Supported backends: anvil, hardhat, tenderly, tenderly-multichain, ethereumjs (default), revm
  • The ethereumjs and revm backends run entirely in the browser — no external node needed
  • revm executes on the Rust EVM compiled to WebAssembly: much faster opcode execution than ethereumjs on compute-heavy simulations, with identical semantics (state still streams lazily from the upstream RPC)
  • All commands inside the fork block execute against the simulated state
  • Changes do not affect the real chain
  • batch (...) inside a fork simulates an EIP-7702 batch: if the sender EOA has no delegation yet, the fork installs a delegation to MetaMask's EIP7702StatelessDeleGator (0x63c0c19a282a1B52b07dD5a65b58948A07DAE32B) and executes all batched calls atomically in a single self-call transaction. An existing delegation on the EOA is reused as-is.

A fork is multichain: switch inside a fork block moves between one fork per chain instead of failing, and each chain keeps its own state, so switching back and forth is safe.

load sim
load bridges
sim:fork --using anvil (
bridges:bridge 100e6 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 to base --using CCTPv2
switch base
set $balance @get(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 "balanceOf(address)(uint256)" @me)
sim:expect @bool($balance > 0)
)

Bridge transfers are auto-relayed. When a bridge transaction executes, the fork scans its receipt for the source event; switching to the destination chain then executes the destination leg there — a mocked Circle attestation driving the real receiveMessage mint for CCTP, an impersonated relayer fill for Across, an impersonated endpoint calling lzReceive for LayerZero, a replayed deposit for the canonical bridges. bridges:claim is therefore unnecessary inside a fork. Transfers whose destination chain the script never switches to are reported when the block ends.

Each backend hosts the extra chains differently:

BackendHow it goes multichain
ethereumjs (default)One in-memory fork per chain, created on the first switch
revmSame as ethereumjs: one in-memory (WASM) fork per chain
tenderlyOne Virtual TestNet per chain
tenderly-multichainA single multichain Virtual Environment holding every chain the script switches to (one dashboard, one teardown). Its networks are attached when the environment is created, so switch targets must be literal
anvil / hardhatThe single local node is re-forked on each switch, saving and restoring each chain's state (needs dumpState/loadState, which Anvil supports)

Limitations: secondary chains fork at their latest block (--block-number pins only the starting chain), each fork keeps its own clock (wait advances the active one), and a delivered destination leg is not itself relayed onward.