eth_call State Overrides: Simulate Against State That Doesn't Exist
Most developers use eth_call to ask "what does this function return against current state?" But eth_call has a rarely-used third parameter that answers a much more powerful question: "what would this return if the chain looked different?" You can hand the node a set of state overrides — fake balances, fake contract code, fake storage slots — and it will run your call against that hypothetical world without touching anything on-chain.
This is one of the most useful RPC features almost nobody reaches for. It turns a read-only call into a simulator: preview a transaction that would revert today, test a contract patch without deploying it, or read what a contract would return if an account held tokens it doesn't. This post covers the mechanics, a reproducible example, and the cases where it earns its keep.
The third parameter
The full signature is:
eth_call([ txObject, blockParameter, stateOverrideSet ])
The stateOverrideSet is an object keyed by address, where each entry can override:
| Field | Effect |
|---|---|
balance |
Set the account's ETH balance (hex wei) |
nonce |
Set the account's nonce |
code |
Replace the account's bytecode with your own |
state |
Replace the account's entire storage (all other slots read as zero) |
stateDiff |
Patch specific storage slots, leaving the rest untouched |
Nothing is persisted — the override exists only for the duration of that single call. The node builds a temporary view of state, runs your call against it, and throws the view away.
A reproducible example
Here's the smallest demonstration. We call an empty address, but override its code with runtime bytecode that always returns the number 42 (0x2a). The bytecode 0x602a60005260206000f3 is just: push 0x2a, store it in memory, return 32 bytes.
curl -s -X POST "https://rpc.swiftnodes.io/rpc/eth?key=YOUR_API_KEY" \
-H 'content-type: application/json' -d '{
"jsonrpc":"2.0","id":1,"method":"eth_call","params":[
{"to":"0x0000000000000000000000000000000000000abc","data":"0x"},
"latest",
{"0x0000000000000000000000000000000000000abc":{"code":"0x602a60005260206000f3"}}
]}'
Result:
0x000000000000000000000000000000000000000000000000000000000000002a
Run the same call without the override and you get 0x — an empty account with no code returns nothing. The override materialized a contract that never existed on-chain, ran your call against it, and returned 42.
Where this is actually useful
Simulate a transaction for an account that can't afford it yet. Testing a swap or a contract interaction usually fails if the caller doesn't hold the tokens or ETH. Override the caller's balance and you can preview whether the call would succeed with funds, before the user has deposited anything. This pairs naturally with gas estimation — eth_estimateGas accepts the same override set, so you can estimate gas for a transaction that would revert against real state (see estimating gas right).
Preview a token operation without an approval. ERC-20 transfers through a router need an allowance, which is a real on-chain transaction. With a stateDiff, you can set the allowance storage slot to a large value and simulate the whole swap as if the approval already happened — useful for quoting an exact-output trade before asking the user to sign anything.
Test a contract change without deploying it. Override the code at an existing contract's address with a patched version and call it. You get to see how the new logic behaves against live storage and live surrounding contracts, with zero deployment cost. This is how a lot of "what if we fixed this bug" analysis gets done.
Read a contract under hypothetical storage. Override a storage slot with stateDiff to flip a boolean (say, a paused flag) or change an owner, then call a function gated on it. You're asking "what would this return if the contract were in that state?" without waiting for it to actually get there.
state vs stateDiff — use stateDiff
The difference between state and stateDiff trips people up and matters a lot:
statereplaces the account's entire storage. Every slot you didn't specify reads back as zero. If the contract depends on any other storage (almost all do), it will misbehave.stateDiffpatches only the slots you list and leaves everything else intact.
In practice you almost always want stateDiff. Reach for state only when you genuinely want a blank-slate storage.
Finding the right slot is the hard part. For a mapping like mapping(address => uint256) balances declared at slot p, the slot for key k is keccak256(abi.encode(k, p)). Fixed variables occupy sequential slots from 0. Getting the layout right is the main friction of storage overrides — tooling like Foundry's storage layout output helps, and the reading balances post covers why you should read storage rather than reconstruct it.
Doing it from a library
viem has first-class support via stateOverride:
const result = await client.call({
to: "0x0000000000000000000000000000000000000abc",
data: "0x",
stateOverride: [
{ address: "0x0000000000000000000000000000000000000abc", code: "0x602a60005260206000f3" },
],
});
web3.py takes a state_override argument:
result = w3.eth.call(
{"to": "0x0000000000000000000000000000000000000abc", "data": "0x"},
"latest",
state_override={"0x0000000000000000000000000000000000000abc": {"code": "0x602a60005260206000f3"}},
)
ethers v6 doesn't expose overrides on provider.call, so send the raw request — the third array element is the override set:
const result = await provider.send("eth_call", [
{ to: "0x0000000000000000000000000000000000000abc", data: "0x" },
"latest",
{ "0x0000000000000000000000000000000000000abc": { code: "0x602a60005260206000f3" } },
]);
Gotchas
- Not every endpoint supports it. State overrides are a client feature (Geth, Erigon, Reth, Nethermind implement them), but many shared public endpoints strip the third parameter or reject it. If your override seems ignored — the call returns the un-overridden result — you're likely on an endpoint that dropped it. SwiftNodes passes the override through to a supporting node; the example above runs against our Ethereum endpoint as-is.
- It's a simulation, full stop. Nothing is written. Two calls with different overrides don't interact.
- Combine with a historical block. The block parameter still applies, so you can override state and pin to an old block — "what would this have returned at block N if the account had held tokens?" — which requires an archive node (archive access via
&archive=1, and see reading historical state). - Tracing accepts overrides too.
debug_traceCalltakes the same override set, so you can get a full execution trace of your simulated call, not just its return value.
The takeaway
eth_call isn't only a way to read the chain as it is — with the third parameter it's a way to read the chain as it might be. Override a balance to preview a funded transaction, override code to test a fix, override a storage slot to explore a hypothetical, and estimate gas for calls that would revert today. It's a simulator hiding inside a method you already use.
Want a node that actually honors the third parameter? SwiftNodes forwards eth_call overrides straight through to a supporting Ethereum node — sign up for a free key and point your client at https://rpc.swiftnodes.io/rpc/eth?key=YOUR_API_KEY.
Related posts
- Is Your RPC Node Actually at the Chain Tip? How to Catch a Stale Endpoint
eth_syncing returns false when a node is fully synced — and also when it hasn't started syncing. That trap, plus how to really tell if an RPC node (your own or a provider's) is at the chain tip: compare block height to a reference, check block-timestamp freshness, and the Solana equivalents (getHealth, catchup). A liveness check is not a freshness check.
- Ankr Alternative: Public Endpoints, API Credits, and the Flat-Rate Option
Ankr's free public RPC is everywhere — until production. Here's where the public endpoints stop, how Ankr's API-credit pricing works, and when flat-rate is the better fit.
- Decoding Event Logs: Topics, Signatures, and Indexed Parameters
You can fetch logs with eth_getLogs and eth_subscribe — but the raw log is address + topics + data, not a readable event. This tutorial explains how logs are encoded (topic0 = the event signature hash, indexed params in topics, the rest in data), how to decode them in viem/ethers/web3.py, and the traps: indexed dynamic types, address padding, and anonymous events.