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.
- 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.
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.
→ Step 2: give the EVM a way to read the chain.
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.
- 1 read at runtime (0x46) — so it reflects the chain the contract is actually on
- 2 scopes it to this exact contract, not just this chain
→ Step 3: the split is the whole point.
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.