Skip to content
← Mastering Foundry — Solidity testing discipline for engineers who already think in Rust
CLI & state-aware testing
Lesson 6 of 7·CONTENT35 min70 XP
Course
Mastering Foundry — Solidity testing discipline for engineers who already think in Rust
Lesson role
CONTENT
Sequence
6 / 7

Lesson 5 — anvil + cheatcodes — local development with mainnet state

Question

anvil is Foundry's local node + mainnet fork; cheatcodes are calls to a fixed cheatcode address that Foundry's test EVM intercepts to change EVM state from inside a test. vm.deal / vm.warp / vm.prank + anvil --fork-url lets you state-aware-test against real mainnet state. The structural difference from Hardhat's evm_snapshot is that cheatcodes are in-EVM calls, not JSON-RPC requests.

Principle (minimum model)

  • anvil --fork-url <mainnet RPC> forks mainnet. Real mainnet state, writeable locally, --fork-block-number pins to a fixed block for determinism.
  • Cheatcodes are intercepted calls, not chain precompiles. Address 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D (VM_ADDRESS in forge-std). Foundry runs upstream revm (from crates.io, not a fork) with its own cheatcode inspector, which catches calls to that address and handles them itself. Same process, no IPC, no JSON-RPC.
  • vm.deal(addr, balance). Rewrite any address's ETH balance. No faucet needed — useful for pre-funding test accounts.
  • vm.warp(timestamp). Set the block timestamp to anything you want. Essential for time-dependent logic (vesting / auctions).
  • vm.prank(addr). Make the next external call appear to come from addr as msg.sender. Test onlyOwner from any EOA. vm.startPrank / vm.stopPrank cover sequences.
  • Compose them. vm.deal + vm.warp + vm.prank lets you reproduce any mainnet state against the real mainnet protocols (Uniswap / Aave) — high-fidelity E2E integration tests.
  • vm.snapshotState / vm.revertToState. State checkpoint + rollback (the older names vm.snapshot / vm.revertTo are deprecated). Snapshot at the start of a sequence, revert between test variants for isolation.
  • Contrast with Hardhat's evm_snapshot JSON-RPC. Hardhat = a JSON-RPC request from the test to the node for each state change. Foundry = a call from inside the EVM that the test EVM intercepts. Not a chain precompile, but similar in spirit to openhl's precompiles course (Stage 9): an address whose calls the execution environment handles specially.

Worked example + steps

Lesson 5 — anvil + cheatcodes — local development with mainnet state

Goal

Concepts you'll grasp in this lesson:

  • anvil --fork-url <URL> gives you a personal mainnet at localhost:8545. Anvil is an in-process REVM that, when started with --fork-url, lazily fetches state from a remote node and serves it locally as if it were the canonical chain. Block N's state, contract storage, account balances — all readable from anvil as they exist on mainnet at the fork block, all modifiable without touching real mainnet. You can deploy contracts that read from Uniswap's actual pools, simulate liquidation cascades against real Aave positions, test governance proposals against the actual DAO state — and reset everything with a Ctrl-C. The forked anvil is the closest thing to a personal mainnet clone you can spin up in 2 seconds.
  • anvil_* RPC methods are the CLI surface for the same machinery vm.* cheatcodes expose inside tests. When you wrote vm.prank(0xWhale) in a Lesson 1–Lesson 3 test, the test contract made a call to the cheatcode address 0x7109...; Foundry's cheatcode inspector caught it and made the next call's msg.sender be 0xWhale (the two-argument vm.prank(sender, origin) also sets tx.origin). When you run cast rpc anvil_impersonateAccount 0xWhale against a forked anvil, you're sending an anvil_* JSON-RPC request that adds 0xWhale to anvil's set of impersonated accounts, so anvil will accept unsigned transactions from it. Two surfaces, similar effect, separate implementations. Cheatcodes inside tests, anvil_* RPC outside tests: both built on revm, but one changes the state of forge's in-process test EVM and the other changes the state of a running anvil node.
  • The 10 deterministic accounts anvil seeds are a feature, not a curiosity. Anvil uses a fixed BIP-39 mnemonic (test test test ... junk) and derives 10 accounts at standard derivation paths, each pre-funded with 10,000 ETH. The deterministic seed means every developer's accounts have the same addresses; the same --private-key works in any anvil instance globally. This enables reproducible tutorials, shareable scripts, and CI determinism — at the cost of obviously being completely insecure (you must never use these keys against any real network). Determinism over secrecy is a deliberate trade-off; anvil is for development, never deployment.
  • The forking-then-impersonating pattern unlocks tests against any production state. A typical Lesson 5 workflow: fork mainnet at block N → impersonate a USDC whale → call transfer from the whale to your test address → use the USDC in subsequent calls to test your contract against real-balance positions. No need to write a fixture that mints synthetic tokens; you're using the actual USDC. Same trick works for any account: governance contracts, multisigs, deployers — impersonate and act as them. This is the production-debug pattern that, before Foundry, required a hand-rolled local-node fork and custom RPC handlers. Foundry compressed it into 3 cast rpc calls. Forked-anvil impersonation is the closest thing to "edit-production-state" you can responsibly do.

Verification:

# Terminal 1: start a forked anvil
anvil --fork-url https://ethereum.reth.rs/rpc

# Terminal 2: impersonate any address and read your fresh balance
cast rpc anvil_impersonateAccount 0xF977814e90dA44bFA03b6295A0616a897441aceC \
  --rpc-url http://localhost:8545
cast balance 0xF977814e90dA44bFA03b6295A0616a897441aceC \
  --rpc-url http://localhost:8545

…spins up a forked mainnet locally, marks Binance's hot wallet address (a known whale) as impersonatable, and queries its real ETH balance via the local node. After this lesson you'll have used anvil's 5 most important RPC methods, mapped 4 of them to the vm.* cheatcodes you already used inside tests, and run a real forked-impersonation flow.

Specific changes:

  • No source-file edits. Lesson 5 is shell + RPC. You'll run anvil in one terminal and cast rpc / cast call / cast send in another.
  • Optional: set ETH_RPC_URL=http://localhost:8545 in your second-terminal session to drop --rpc-url from subsequent cast invocations.

Total: zero lines of Solidity. The pedagogical move is recognizing that the Lesson 1–Lesson 3 vm.* cheatcodes you wrote inside tests and the anvil_* RPC methods you call from the CLI are parallel ways to manipulate EVM state: one inside forge's test EVM, one against a running anvil node.

Recap

After Lesson 4:

  • cast is alloy::Provider exposed as a terminal command; subcommands map 1:1 to alloy methods.
  • cast call reads, cast send writes; --rpc-url makes the chain a per-command parameter.
  • cast abi-encode / cast abi-decode / cast 4byte cover the calldata-manipulation surface.

Lesson 4 pointed cast at real mainnet. Lesson 5 points cast at a local fork of mainnet — your machine is now a controllable mainnet clone. The vm.* cheatcodes you saw inside Foundry tests come back as anvil_* RPC methods, and the discipline-transfer story completes: revm underneath, three surfaces (Solidity vm.*, Foundry test runner, anvil JSON-RPC).

Plan

Six categories of invocation:

  1. Start a forked anvil — anvil (vanilla) vs anvil --fork-url <mainnet-rpc>. Inspect the seeded accounts.
  2. anvil_impersonateAccount — mark a real mainnet address as impersonatable, then send transactions as that address without its private key.
  3. anvil_setBalance / anvil_setStorageAt — directly mutate account balances and contract storage. The "I'm the chain god" RPC methods.
  4. anvil_mine / anvil_setNextBlockTimestamp — time-travel: mine N blocks instantly, or jump the next block's timestamp forward. Useful for testing time-dependent logic without waiting 7 days.
  5. The forked-impersonation flow — fork mainnet → impersonate USDC whale → transfer USDC to your test address → use it in a contract call. The end-to-end demo.
  6. The vm.* ↔ anvil_* mapping table — same pedagogical role as Lesson 4's cast ↔ alloy::Provider table, but for cheatcodes.

Quick check: what is the anvil equivalent of vm.deal(alice, 10 ether)?

(Answer: cast rpc anvil_setBalance 0xAliceAddress 0x8AC7230489E80000 --rpc-url http://localhost:8545 — where 0x8AC7230489E80000 is hex for 10 × 10^18 wei (10 ether). The shape is identical: name an address, set its balance to a value. Anvil's RPC takes the balance as a hex-encoded uint256; the test cheatcode takes it as a Solidity uint256. Both set the account's balance — but in different places. The cheatcode changes the state of forge's in-process test EVM; the RPC method changes anvil's in-memory node state. Neither affects the other.)

How vm.* cheatcodes map to anvil_* RPC methods

The architecture in one diagram — revm underneath, three surfaces:

flowchart TD
    A["Foundry test runner<br/>vm.prank, vm.deal, vm.warp"] -->|"call to cheatcode address 0x7109...<br/>caught by the cheatcode inspector"| R
    B["forge test / forge script<br/>(test contracts, scripts)"] -->|"in-process execution"| R
    C["Anvil HTTP server<br/>anvil_impersonateAccount, anvil_setBalance"] -->|"HTTP JSON-RPC handler"| R
    R["revm execution<br/>accounts / storage / block env"]

Three different entry points into revm-based execution. They are not one shared mutation API: forge's test EVM and an anvil node each hold their own state, and the cheatcodes and the anvil_* methods are implemented separately (crates/cheatcodes/ vs crates/anvil/src/eth/api.rs in foundry-rs/foundry). (cast call / cast send are not on this diagram: by default they send JSON-RPC to whatever node --rpc-url points at.) The table below lists the cheatcode-to-RPC-method correspondences:

┌────────────────────────────────────┬──────────────────────────────────────────────┐
│  Inside Foundry tests (cheatcode)  │  Against a running anvil (RPC method)        │
├────────────────────────────────────┼──────────────────────────────────────────────┤
│  vm.prank(addr)                    │  cast rpc anvil_impersonateAccount addr      │
│  vm.deal(addr, value)              │  cast rpc anvil_setBalance addr <hex-value>  │
│  vm.warp(timestamp)                │  cast rpc anvil_setNextBlockTimestamp <ts>   │
│  vm.roll(blockNumber)              │  cast rpc anvil_mine <blockcount> (mines     │
│                                    │      blocks rather than setting the number)  │
│  vm.store(addr, slot, value)       │  cast rpc anvil_setStorageAt addr slot value │
│  vm.etch(addr, bytecode)           │  cast rpc anvil_setCode addr <bytecode>      │
│  vm.snapshotState() /              │  evm_snapshot / evm_revert (Ganache/Hardhat- │
│    vm.revertToState(id)            │      style names, also supported by Hardhat) │
├────────────────────────────────────┼──────────────────────────────────────────────┤
│  (lives inside the test contract;  │  (called over HTTP JSON-RPC from any client; │
│   call to 0x710970..., caught by   │   handled by anvil's EthApi in Rust)         │
│   forge's cheatcode inspector)     │                                              │
└────────────────────────────────────┴──────────────────────────────────────────────┘

The structural takeaway: vm.* and anvil_* are two surfaces for the same kinds of state change. Inside a Solidity test, the cheatcode goes through Foundry's cheatcode inspector and changes the test EVM's state; from a shell, the anvil_* method goes through anvil's JSON-RPC handler and changes the node's state. They are separate code paths with separate state. If you grok Lesson 1–Lesson 3's vm.*, you already know what most anvil_* methods do; you just need the RPC method name.

Walk-through

Step 1: Start anvil and inspect what you got

anvil

In one terminal. Anvil prints (abbreviated):

                              _   _
                             (_) | |
      __ _   _ __   __   __  _  | |
     / _` | | '_ \  \ \ / / | | | |
    | (_| | | | | |  \ V /  | | | |
     \__,_| |_| |_|   \_/   |_| |_|

    1.7.x ( ... )    https://github.com/foundry-rs/foundry

Available Accounts
==================

(0) 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000.000000000000000000 ETH)
(1) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000.000000000000000000 ETH)
...
(9) 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 (10000.000000000000000000 ETH)

Private Keys
==================

(0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
(1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
...

Wallet
==================
Mnemonic:          test test test test test test test test test test test junk
Derivation path:   m/44'/60'/0'/0/

Chain ID
==================
31337

Listening on 127.0.0.1:8545

Five things to notice about the startup banner:

  1. The mnemonic is test test test ... junk. Anvil uses this fixed seed phrase by default; every anvil instance on Earth, when started without --mnemonic, has the same 10 accounts. This is intentional — it lets tutorial code use known private keys without each reader having to set up their own seed. The keys are public knowledge; never use them outside local development.
  2. Account 0 is 0xf39Fd6... with private key 0xac0974.... Memorize these — they appear constantly in tutorials, Foundry's own docs, and CI configs. You can paste 0xac0974... as --private-key for any anvil-targeting cast send and it will work.
  3. Chain ID is 31337. That's the anvil default. Hardhat also defaults to 31337. If you accidentally point a tx at a real network with chain ID 31337, none will accept it — chain ID is the explicit shield against cross-chain replay. The funny number is a safety feature.
  4. The RPC listens on 127.0.0.1:8545. Standard Ethereum RPC port. Anvil binds to localhost only by default; --host 0.0.0.0 opens it to the network (don't, on shared machines).
  5. No --fork-url means anvil starts from genesis with empty state. No contracts deployed, no transactions in history. Useful for unit-testing your own contracts in isolation, useless for testing against production protocols. We'll restart with --fork-url in Step 2.

Stop this anvil (Ctrl-C) and start a forked one:

anvil --fork-url https://ethereum.reth.rs/rpc

The banner adds a Fork section:

Fork
==================
Endpoint:       https://ethereum.reth.rs/rpc
Block number:   <recent mainnet block number>
Block hash:     0x...
Chain ID:       1

Chain ID is now 1 — mainnet. The 10 deterministic accounts are still present (anvil seeds them regardless of fork status), but the chain state is now mainnet's view at the fork block. Every USDC balance, every Uniswap pool, every governance vote — readable as it exists on real Ethereum right now.

⚠️ Safety note — Chain ID 1 fork + the wrong --rpc-url. Your local fork now reports Chain ID 1, the same value real mainnet reports. The chain-ID check that protects you from cross-chain replay between, say, mainnet and Sepolia won't help here — both endpoints report 1. If a real mainnet RPC URL is sitting in another env var or in your shell history and you accidentally point cast send --private-key <REAL-KEY> at it instead of http://localhost:8545, the transaction broadcasts to real mainnet. --unlocked is harmless (no signed tx produced without a key), but --private-key is not. The defense: export ETH_RPC_URL=http://localhost:8545 explicitly in your fork-working terminal and never paste real-mainnet private keys into that shell. Discipline on --rpc-url is the only defense once you start forking.

Step 2: Read forked mainnet state from your local anvil

In a second terminal — set ETH_RPC_URL to the local anvil for the rest of the session:

export ETH_RPC_URL=http://localhost:8545

Now any cast command without --rpc-url goes to anvil. Read USDC's totalSupply:

cast call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 "totalSupply()(uint256)"

Same output as Lesson 4 against real mainnet — because anvil's forking layer transparently fetches the contract code + storage from the fork source the first time you query it, caches it locally, and serves subsequent queries instantly. Lazy state fetching: anvil only pulls state on demand, so spinning up a fork is fast (seconds), and the cached state is local for the session.

Step 3: Impersonate a real mainnet address

The killer feature. Pick any mainnet address — the Binance-labelled address 0xF977814e90dA44bFA03b6295A0616a897441aceC (a publicly-known whale; it held ~419,000 ETH at the fork block used while checking this lesson):

cast rpc anvil_impersonateAccount 0xF977814e90dA44bFA03b6295A0616a897441aceC

Output: null (success — anvil RPC methods return null for "done").

Now you can send transactions as that address without its private key. Before sending ETH to anvil's account 0 (0xf39Fd6...), check its code on the fork:

cast code 0xf39Fd6e51aad88F6F4ce6aB8827279cfFFb92266
# → 0xef01008a67b5020ee254ef48e3b6a04927f39baf7e408a  (at the block checked)

Because the anvil keys are public, on real mainnet someone has attached an EIP-7702 delegation (0xef0100 + contract address) to these accounts, and the delegated code forwards incoming ETH away. Your fork inherits that code, so an ETH transfer to account 0 succeeds but its balance stays at 10000. Clear the code on your fork first (local only):

cast rpc anvil_setCode 0xf39Fd6e51aad88F6F4ce6aB8827279cfFFb92266 0x

Then send 1 ETH from the impersonated whale:

cast send --unlocked \
  --from 0xF977814e90dA44bFA03b6295A0616a897441aceC \
  --value 1ether \
  0xf39Fd6e51aad88F6F4ce6aB8827279cfFFb92266

The --unlocked flag tells cast send to ask the node to sign (anvil handles it for impersonated accounts; no private key needed). The transaction completes and cast prints the receipt.

Verify the recipient's balance went up:

cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cfFFb92266 --ether

The starting 10000 ETH is now 10001 ETH. You just sent 1 ETH from Binance's wallet to your local test account without their private key, against a local fork of mainnet state. No real mainnet was touched.

Five things to notice:

  1. Impersonation works because anvil accepts unsigned eth_sendTransaction requests from impersonated accounts. The forked state shows Binance's address with a real ETH balance; anvil's transaction-execution path treats from = impersonated_addr as legitimate. The signature check is what private keys exist to satisfy; impersonation simply turns the check off for designated addresses.
  2. anvil_impersonateAccount is persistent until you stop impersonating. Until you call anvil_stopImpersonatingAccount, the address stays in anvil's impersonation set. Useful for multi-step tests; harmful if you forget and another test step expects normal signature enforcement.
  3. cast send --unlocked is the CLI equivalent of vm.prank inside a test. Both say "execute the next call as if it came from this address," both rely on the underlying machinery being permissive. --unlocked is the magic word that tells cast not to expect a private key.
  4. The whale's ETH balance reflects mainnet's view at fork time. When anvil first served the eth_getBalance(0xF977...) query, it fetched the real balance from https://ethereum.reth.rs/rpc, cached it, and now serves locally-modified versions of that balance. Subsequent cast send operations subtract from anvil's local cache, not from real mainnet.
  5. Real mainnet is untouched. The Binance address's actual ETH balance hasn't changed. You're sending ETH on your local fork; the global ledger doesn't know this happened.

Stop impersonating when done:

cast rpc anvil_stopImpersonatingAccount 0xF977814e90dA44bFA03b6295A0616a897441aceC

Step 4: Edit state directly with anvil_setBalance / anvil_setStorageAt

Sometimes you don't want to impersonate — you just want to give an address a balance:

# Give anvil account 0 exactly 1,000,000 ETH (10^24 wei = 0xD3C21BCECCEDA1000000)
cast rpc anvil_setBalance 0xf39Fd6e51aad88F6F4ce6aB8827279cfFFb92266 \
  0xD3C21BCECCEDA1000000

# Verify
cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cfFFb92266 --ether
# → 1000000.000000000000000000

This is the RPC equivalent of vm.deal(addr, value) from Lesson 1–Lesson 3 tests.

For ERC-20 token balances, you don't have an anvil_setTokenBalance — but you can use anvil_setStorageAt to directly write the storage slot that holds the balance:

# USDC's `_balances` mapping is at storage slot 9. The slot for balanceOf(addr)
# is keccak256(abi.encode(addr, 9)). Compute that:
SLOT=$(cast index address 0xf39Fd6e51aad88F6F4ce6aB8827279cfFFb92266 9)

# Set the balance to 1,000,000 USDC (1e12 in 6-decimal precision = 0xe8d4a51000)
cast rpc anvil_setStorageAt 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
  $SLOT \
  0x000000000000000000000000000000000000000000000000000000e8d4a51000

# → true

# Verify
cast call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
  "balanceOf(address)(uint256)" \
  0xf39Fd6e51aad88F6F4ce6aB8827279cfFFb92266
# → 1000000000000 [1e12]

You just gave yourself 1 million USDC on the local fork without buying any. Same trick works for any storage slot of any contract — change a totalSupply, flip an owner, set a price feed's last value. The only thing you need is the storage slot, which forge inspect <contract> storage reveals for any contract whose source you have in your project.

Three things to notice:

  1. cast index address <addr> <base-slot> computes the mapping slot. keccak256(abi.encode(addr, baseSlot)) is the Solidity storage layout for mapping(address => X). cast index exposes this as a CLI helper, so you don't have to compute the keccak by hand.

  2. anvil_setStorageAt is the most powerful and most dangerous of anvil's mutators. You can break contracts in interesting ways by setting storage to invalid states (e.g., set USDC's paused slot to a non-boolean). Use it for tests that verify your contract handles edge cases, not for "just making numbers match."

    [!IMPORTANT] Full 32-byte values, and packed slots EVM storage slots are 32-byte (256-bit) words. anvil_setStorageAt expects the value as a complete 32-byte hex value (0x followed by exactly 64 hex characters), padded with leading zeros (e.g., 0x000000000000000000000000000000000000000000000000000000e8d4a51000). A shorter value such as 0xe8d4a51000 is rejected with error code -32602: invalid string length (Foundry 1.7.1).

    Whatever you write replaces the whole slot. Solidity packs several small variables (e.g., an address and a uint96) into one 32-byte slot, so writing a slot without accounting for every variable in it overwrites all of them. The professional approach to updating single variables in a packed slot is to read the existing 32-byte value, apply a bitwise mask to update only the target bytes, and write the combined 32-byte word back.

  3. Real production contracts often have non-obvious storage layouts. USDC's mapping at slot 9 is correct as of this writing, but contracts upgraded via proxy patterns can have arbitrary layouts. forge inspect <contract> storage is the source of truth.

Step 5: Time travel with anvil_mine and anvil_setNextBlockTimestamp

Mine 100 blocks instantly (useful for testing time-locked withdrawals, vesting cliffs):

cast rpc anvil_mine 0x64  # 0x64 = 100
cast block-number
# → <fork_block + 100>

Jump the next block's timestamp forward by 7 days:

# Get current timestamp from latest block
CURRENT=$(cast block latest --field timestamp)
SEVEN_DAYS_LATER=$((CURRENT + 7 * 86400))

# Set next block's timestamp
cast rpc anvil_setNextBlockTimestamp $SEVEN_DAYS_LATER

# Mine one block so the timestamp takes effect
cast rpc anvil_mine 0x1

# Verify
cast block latest --field timestamp
# → <fork_timestamp + 7*86400>

This is the RPC equivalent of vm.warp + vm.roll from tests. Useful for: testing vesting that unlocks in N days, testing auction-end logic that requires N hours, testing rate-limiting that resets daily — all without waiting real time.

Step 6: The full forked-impersonation flow as a recipe

Putting it together — the workflow you'll use most:

# Terminal 1: forked anvil
anvil --fork-url https://ethereum.reth.rs/rpc

# Terminal 2:
export ETH_RPC_URL=http://localhost:8545

# 1. Find a whale of the token you want (balances change — check first:
#    cast call $TOKEN "balanceOf(address)(uint256)" $WHALE)
WHALE=0x28C6c06298d514Db089934071355E5743bf21d60  # Binance-labelled EOA; held ~52,000 USDC when checked
TOKEN=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48  # USDC
ME=0xf39Fd6e51aad88F6F4ce6aB8827279cfFFb92266     # anvil account 0

# 2. Impersonate the whale
cast rpc anvil_impersonateAccount $WHALE

# 3. Make sure the whale can pay gas (setBalance sets, it doesn't add)
cast rpc anvil_setBalance $WHALE 0x8AC7230489E80000  # 10 ETH

# 4. Whale transfers USDC to you
cast send --unlocked --from $WHALE \
  $TOKEN "transfer(address,uint256)" $ME 1000000000  # 1000 USDC

# 5. Verify your new USDC balance
cast call $TOKEN "balanceOf(address)(uint256)" $ME
# → 1000000000 [1e9]  (1000 USDC in 6-decimal precision, if $ME started at 0)

# 6. Now use this real USDC against your test contract
#    (deploy your contract via `forge create` or `cast send --create`,
#     then call it with your now-funded test account)

This short recipe replaces a mock-USDC fixture: you test against the real token contract and its real storage. (Hardhat has the same building blocks as hardhat_impersonateAccount / hardhat_setBalance; the point is the workflow, not a tool exclusive.)

Common errors

  • error: the following required arguments were not provided: --from <ADDRESS> on cast send --unlocked — --unlocked requires --from, since there's no private key to derive the sender from.
  • Error: nonce too low — you sent transactions from an account that anvil doesn't have the latest nonce for. Restart anvil (resets all nonces) or use anvil_setNonce.
  • Insufficient funds for gas * price + value — the impersonated account doesn't have ETH to pay gas. Send it some via anvil_setBalance (Step 4) before sending the transaction.
  • ETH sent to an anvil default account on a mainnet fork "disappears" — the account has an EIP-7702 delegation on mainnet that forwards ETH (see Step 3). Clear it with cast rpc anvil_setCode <addr> 0x, or use a fresh address.
  • Anvil dies silently after long-running tests — check anvil's terminal for OOM or panic messages. Long-running tests with many anvil_setStorageAt calls can grow anvil's state cache; restart between unrelated test runs.

Design retrospective

Three load-bearing decisions in anvil's design:

  1. Anvil runs on revm; it's not a separate EVM implementation. Anvil isn't a from-scratch chain client and it isn't built on Reth — it's a JSON-RPC node built from Foundry's own crates (crates/anvil, foundry-evm) on the same upstream revm crate (from crates.io) that Foundry's test runner uses. Reth also uses revm, but anvil does not depend on Reth; a new EVM feature reaches anvil when Foundry upgrades its revm dependency and wires it in, on Foundry's release cycle. One EVM library, several surfaces: Foundry tests (in-process), forge script, local node (anvil).

  2. anvil_* RPC method names are namespaced separately from eth_* standard methods. Standard JSON-RPC methods like eth_getBalance, eth_call, eth_sendTransaction work identically on anvil as on any node. Anvil-specific methods like anvil_impersonateAccount, anvil_setStorageAt use the anvil_ prefix. This is convention (also used by hardhat_* for Hardhat-specific methods), and it serves a discipline purpose: client code that calls eth_* is portable to mainnet, code that calls anvil_* is local-dev-only. Namespace separation is a deployment-safety guard at the method-name level.

  3. Forking is lazy, not eager. Anvil doesn't download the entire mainnet state on --fork-url; it downloads state on demand as queries reference it. This means startup is fast (seconds, not hours), but the first query for any uncached state has a round-trip latency. Subsequent queries for the same state are instant. Lazy forking is the trade-off that makes forking practical — eager forking would be unusably slow.

Answer key

After Lesson 5, your shell history should include something like:

# Terminal 1
anvil --fork-url https://ethereum.reth.rs/rpc

# Terminal 2 — the recipes you'll reach for most
export ETH_RPC_URL=http://localhost:8545
cast rpc anvil_impersonateAccount 0x...
cast rpc anvil_setBalance 0x... 0x...
cast rpc anvil_setStorageAt 0x... <slot> <value>
cast rpc anvil_mine 0x64
cast rpc anvil_setNextBlockTimestamp <unix-ts>
cast send --unlocked --from 0x... 0x<contract> "<sig>" <args...>

After Lesson 5 you can:

  • Spin up a forked mainnet locally in 2 seconds
  • Impersonate any account (no key required) and act as them
  • Mutate any account's balance or contract's storage directly
  • Time-travel forward by blocks or seconds for time-locked logic testing
  • Build a full "fork → impersonate whale → fund test address → call contract" flow

Q&A

Q1: How is anvil different from Hardhat Network?

Same core idea (local Ethereum node, RPC-compatible, supports forking + impersonation + state manipulation), different implementation language and ergonomics. Anvil is a single Rust binary built on revm. Current Hardhat runs its network on EDR (NomicFoundation/edr), which is also written in Rust on revm and ships as an npm package — the old JavaScript ethereumjs-vm backend is gone. Anvil's RPC methods use the anvil_* prefix; Hardhat's use hardhat_*. For most workflows they're interchangeable. Anvil fits a Foundry / no-Node.js workflow; Hardhat fits if you've already invested in its plugin ecosystem.

Q2: Can I run anvil as a long-running development node?

Yes — anvil is daemon-ready, supports background mode (&), and you can leave it running for hours. The state is in-memory only by default, so a restart loses all changes. For persistent state across restarts, use --state <file> to save/load state to disk. Note: state files can grow large for long-running forks; clean them up periodically. Anvil is fine for hour-long sessions; for longer, manage --state files.

Q3: What's the relationship between anvil and forge test?

forge test runs your tests on an in-process revm, and each test starts from the state left by setUp. anvil runs revm behind a JSON-RPC server. Same EVM library, different transport. You can also point forge test at a running anvil with --fork-url http://localhost:8545: forge then reads state from anvil (including anything you set with anvil_setBalance etc.), but writes made during tests stay inside forge — they are not sent to anvil, and each test is still isolated. Most workflows use forge's built-in EVM for tests and anvil for ad-hoc CLI work. Tests = forge's in-process EVM; CLI work = anvil.

Q4: Can I impersonate a contract address, not just an EOA?

Yes. anvil_impersonateAccount works on any address; the address doesn't need to be an EOA. Useful for testing what happens when a specific contract calls your contract — you can impersonate the Uniswap V3 router and call your contract as if a real swap was routing through you. Impersonation is address-keyed, not EOA-keyed.

Q5: What happens to the fork state when I Ctrl-C anvil?

Lost (unless you used --state <file>). All anvil_setBalance, anvil_setStorageAt, anvil_impersonateAccount mutations and any transactions you sent disappear. Next anvil --fork-url re-fetches state from the fork source. This is a feature, not a bug — fresh forks per session prevent state pollution between unrelated test runs.

Q6: Why doesn't --fork-url work with the anvil Docker image?

It does — but anvil binds to 127.0.0.1 by default, which inside a container is the container's own localhost. Start it with --host 0.0.0.0 (or set ANVIL_IP_ADDR=0.0.0.0) and publish the port with -p 8545:8545. Also remember Docker's --fork-url <host-RPC> references the container's network view; if your fork source is on the host, use host.docker.internal:<port> (Docker Desktop) or your host's LAN IP. Networking + Docker = the usual gotchas, not an anvil-specific issue.

Q7: My fork has Chain ID 1, the same as real mainnet. Doesn't this defeat the chain-ID safety check?

Yes — and this is the Lesson 5 trap to internalize.

When you fork mainnet, local anvil reports Chain ID 1. The chain-ID safety check only compares endpoint IDs, so if both endpoints report 1, it passes silently.
If a real-mainnet RPC URL is still in another env var or shell history, and you accidentally run cast send --private-key <REAL-KEY> --rpc-url $REAL_RPC instead of http://localhost:8545, the transaction will broadcast to real mainnet.

--unlocked impersonation is harmless against real mainnet (no signed tx is produced), but --private-key is not.
The defense is operational, not architectural:

  1. Explicitly set export ETH_RPC_URL=http://localhost:8545 in fork-working sessions.
  2. Never paste real-mainnet private keys into a shell that has been used for local-fork work.

Once you start forking, --rpc-url discipline is your only defense. Chain-ID checks protect between different chains, not between a fork and its source chain.

Next lesson (Lesson 6) — the capstone: porting InsuranceFund to Solidity

Lesson 6 is the capstone where everything in Lesson 0–Lesson 5 comes together. You'll take openhl-liquidation Stage 10b's InsuranceFund — the Rust implementation you wrote (or studied) in the openhl liquidation course — and port it to Solidity. The shortfall law carries over directly (amount + unfilled == shortfall → absorbed + remaining == loss); the running totals, owner and owner-only withdraw are EVM-side additions. Then you'll prove four invariants with invariant testing — one is the call-sequence form of that shortfall law, the others check the Solidity-side bookkeeping — using a Handler that runs random operation sequences, as the Rust balance_never_negative proptest from the liquidation course's withdraw_shortfall lesson does. The capstone deliverable lives in-repo at examples/foundry-capstone/:

  • examples/foundry-capstone/src/InsuranceFund.sol — the Solidity port
  • examples/foundry-capstone/test/InsuranceFundHandler.sol — Handler with wrappedDeposit / wrappedWithdraw / wrappedAbsorb and 5 ghost variables
  • examples/foundry-capstone/test/InsuranceFund.invariant.t.sol — the 4 invariant_* functions: invariant_Conservation, invariant_DepositAccounting, invariant_WithdrawAccounting, invariant_AbsorbDecomposition

By the end of Lesson 6 you'll have proven the same theorem in two languages, mechanically, against the same invariant testing engine you learned in Lesson 3. That's the discipline transfer that makes the whole curriculum click: it was never about Rust or Solidity — it was about the conservation-law discipline that survives the language boundary.

Summary (3 lines)

  • anvil --fork-url forks mainnet state locally; --fork-block-number pins for determinism. State-aware tests run against real protocols, high fidelity.
  • Cheatcodes are calls to 0x7109709E... that Foundry's cheatcode inspector (on upstream revm) intercepts; they are not chain precompiles. vm.deal / vm.warp / vm.prank rewrite state directly — no IPC, no JSON-RPC, structurally different from Hardhat's evm_snapshot.
  • vm.snapshotState / vm.revertToState isolate test variants. anvil_* methods do the same kinds of change against a running node, through a separate implementation. Next lesson: Capstone — prove the four InsuranceFund invariants in Solidity.