Part III · Chapter 17 of 43 · Istanbul

EIP-1344: CHAINID — replay protection for contract signatures

EIP-155 bound transactions to their chain. But contracts increasingly verify their own signed messages — gasless approvals, meta-transactions, off-chain orders — and those had no chain identity, so a signature valid against a contract on one chain replayed against the same contract on another. CHAINID exposes the current chain's id to the EVM, so a contract can bind its signatures to the chain it's actually running on.

Updated Jun 24, 2026 · 9 min
Assumed
  • EIP-155 replay protection
  • signatures & ecrecover

Istanbul, December 2019. EIP-155 solved cross-chain replay for transactions by folding a chainId into what you sign. But by 2019 a whole world of signatures had grown up inside contracts — gasless token approvals (permit), meta-transactions, off-chain DEX orders — where a user signs a message and a contract verifies it with ecrecover. Those signatures had exactly the problem 155 fixed, one layer up: no chain identity, so a signature valid against a contract on one chain was valid against the same contract everywhere. The catch is that a contract can’t just know its chain — unless the protocol tells it. EIP-1344 is the opcode that tells it. Let’s derive it.

1 step Signatures with no chain

The problem: a contract’s signatures replay everywhere

A contract that accepts signed messages verifies them with ecrecover The EVM precompile that recovers the signer's address from a message hash and a signature (v, r, s). A contract compares the recovered address to who it expects — the basis of permit, meta-transactions, and off-chain orders. : recover the signer from (hash, v, r, s) and check it’s who it should be. But if the hash the user signed says nothing about which chain the contract is on, then the same signature recovers the same signer against the same contract address on any chain — and contract addresses are often identical across chains (especially after a fork, or via CREATE2). So a permit you signed on mainnet can be replayed against the twin contract on another chain.

a contract verifies a signed message — but on which chain? signed permit ecrecover(v,r,s) Chain A · 0xTkn ✓ approved Chain B · 0xTkn ✓ replayed! the same signature approves on every chain that shares the contract address
A user signs a permit; the contract verifies it with ecrecover. Because the signed message carries no chain identity, the identical signature is accepted against the same contract address on every chain — a cross-chain replay, exactly the EIP-155 problem but for contract-level signatures.

→ Step 2: give the EVM a way to read the chain.

2 step Read the chain at runtime

CHAINID: the current chain’s id, at runtime

Hardcoding the id at deploy time is tempting but wrong — we’ll see why in a moment — so the real fix makes the chain id readable while the contract runs. CHAINID The Istanbul opcode 0x46. It pushes the id of the chain the transaction is executing on onto the stack — the same value EIP-155 uses — so contract code can read its own chain at runtime. (opcode 0x46) pushes the current chain’s id onto the stack. Now a contract can fold that id into the domain separator An EIP-712 value that scopes a signature to a specific app and deployment: it hashes together the contract name, version, the chainId, and the verifying contract's address. Signatures are made over (domainSeparator, message), so they only validate in that exact domain. — the EIP-712 value that scopes a signature to this app, this contract, this chain. The message a user signs is made over that domain, so a signature is only valid where the domain matches.

bind the signature to the chain: read CHAINID at runtime CHAINID 0x46 → 1 domain separator chainId: 1 contract: 0xTkn the message is signed over this domain now the signature is valid only on chain 1, and only for this contract
CHAINID (0x46) returns the id of the chain the code is running on. The contract folds it into its EIP-712 domain separator (chainId + verifying contract), and the user's message is signed over that domain — so the signature is valid only on chain 1, and only for this contract.
Formula
domainSeparator = keccak256( name, version, CHAINID 1 , address(this) 2 )
  1. 1 read at runtime (0x46) — so it reflects the chain the contract is actually on
  2. 2 scopes it to this exact contract, not just this chain
Sign over this domain and the signature is welded to (this chain, this contract) — a contract-level version of EIP-155's trick.

→ Step 3: the split is the whole point.

3 step Survive a chain split

Why it must be read live: the chain split

Picture a hard fork that splits one chain into two — the exact scenario EIP-155 was born from. Your contract now exists, byte-for-byte, on both chains at the same address. If it had hardcoded its chain id at deploy, both copies would carry the same constant, and a signature would still validate on both — the hole never closes. Because CHAINID is read at runtime, the two copies now return different ids: on the original chain it’s 1, on the new one it’s, say, 61. The domain separator differs, so a signature made for chain 1 recomputes to a domain that doesn’t match on chain 61, and it’s rejected. The protection tracks the split automatically.

on a split, CHAINID differs — the signature can't cross the same sig Chain A · CHAINID 1 domain uses 1 ✓ matches Chain B · CHAINID 61 domain uses 61 ✗ rejected recompute the domain when CHAINID changes — or the cache reopens the hole
After a split, the same contract runs on both chains but CHAINID returns a different id on each. A signature made for chain 1 matches the domain there (✓) but not on chain 61, where the domain now uses 61 (✗). The catch: a contract that cached its domain separator at deploy must recompute it when CHAINID changes, or the cache reopens the hole.
04 Go Deeper Where to take it from here