Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x46 |
| Mnemonic | CHAINID |
| Gas | 2 |
| Stack Input | (none) |
| Stack Output | chain_id (uint256) |
| Behavior | Pushes the current chain’s EIP-155 identifier onto the stack as a 256-bit value. Introduced in the Istanbul hard fork (EIP-1344) to give smart contracts runtime access to the chain ID, primarily for replay protection in signature schemes. On Ethereum mainnet the value is 1, on Optimism it is 10, on Base it is 8453, etc. The value is read from the chain configuration and is constant for all transactions within a given chain — it only changes if the network undergoes a hard fork that explicitly reassigns the chain ID. |
Threat Surface
CHAINID is the EVM’s on-chain identity primitive for the network itself. It answers “which chain am I executing on?” and is the foundation of cross-chain replay protection. Every EIP-155 transaction, every EIP-712 typed data signature, and every ERC-2612 permit includes the chain ID to bind a signature to one specific chain. When this binding is missing, broken, or stale, signatures become portable weapons that attackers replay across chains, forks, and L2s.
The threat surface centers on three properties:
-
CHAINID is the sole on-chain mechanism for cross-chain replay protection. EIP-155 encodes the chain ID into the transaction signature’s
vparameter (v = {0,1} + CHAIN_ID * 2 + 35), making transactions chain-specific. EIP-712 domain separators includechainIdto bind typed data signatures to a single chain. If a contract computes a domain separator withoutblock.chainid, or if a wallet signs a message without including the chain ID, the resulting signature is valid on every EVM chain where the contract is deployed at the same address. The CHAINID opcode (EIP-1344) was introduced precisely to let contracts read this value at runtime instead of hardcoding it. -
Hardcoded chain IDs break during hard forks. Before EIP-1344 (Istanbul, 2019), contracts had to embed the chain ID as a compile-time constant. If the chain forks and the new chain assigns a different chain ID, contracts with hardcoded values either (a) reject valid signatures on the new chain or (b) accept signatures from the old chain. The ETH/ETC split in 2016 demonstrated the catastrophic consequences of missing chain ID protection: every transaction on one chain was replayable on the other until EIP-155 was activated at the Spurious Dragon fork.
-
L2 proliferation multiplies the replay surface. With hundreds of EVM-compatible L2s (Optimism, Base, Arbitrum, zkSync, Polygon, etc.), every contract deployed to multiple chains creates a potential replay target. Each chain has a unique chain ID, but if a contract’s signature verification omits
block.chainidfrom the signed payload, a signature obtained on any one chain is valid on all others. The explosion of multi-chain deployments (often via deterministic CREATE2 to the same address) has made cross-chain replay the dominant chain-ID-related vulnerability class.
Smart Contract Threats
T1: Cross-Chain Signature Replay — Missing Chain ID in Signed Messages (Critical)
When a contract verifies off-chain signatures (for meta-transactions, permits, governance votes, order books, etc.) without including block.chainid in the signed payload, the signature is valid on every chain where the contract exists at the same address. This is the highest-impact CHAINID vulnerability:
-
EIP-712 domain separator without chainId. The EIP-712 standard defines an optional
chainIdfield in the domain separator. Contracts that omit it produce domain separators that are identical across chains. An attacker who obtains a valid signature on chain A can replay it on chains B, C, D, etc. Because multi-chain deployment via CREATE2 produces identical contract addresses, theverifyingContractfield alone does not prevent replay. -
Custom signature schemes without chain binding. Many contracts implement their own off-chain signature verification (e.g.,
ecrecover(keccak256(abi.encodePacked(sender, amount, nonce)))) without including the chain ID. These signatures are inherently cross-chain replayable. An attacker monitors mempool or past transactions on one chain and replays the signature on another. -
Gasless/meta-transaction relayers. Relayers that accept signed messages and submit them on-chain often operate across multiple chains. If the signed message lacks chain ID, a relayer (or any observer) can submit the same signature on a different chain. The legitimate user’s nonce may not have been consumed on the target chain, so the replayed transaction succeeds.
Why it matters: Cross-chain replay is the canonical CHAINID attack. It affects any multi-chain protocol that uses off-chain signatures — DEXs, bridges, lending protocols, NFT marketplaces, and governance systems.
T2: Hardcoded Chain ID Breaks During Hard Forks (High)
Contracts that store the chain ID as an immutable constant (set at construction time) rather than reading block.chainid at runtime create a time bomb that detonates during hard forks:
-
Cached domain separator without revalidation. A common pattern is to compute the EIP-712 domain separator once in the constructor and store it as an immutable value. If the chain forks and the new chain adopts a different chain ID, the cached domain separator still contains the old chain ID. Signatures for the old chain are now valid on the new chain (and vice versa), because the contract’s domain separator matches the old chain’s ID.
-
Immutable vs. dynamic chain ID. OpenZeppelin’s EIP712 implementation mitigates this by caching both the domain separator and the chain ID, then recomputing the separator at runtime if
block.chainid != cachedChainId. Contracts that implement their own caching without this revalidation check are vulnerable. -
Constructor-time chain ID in upgradeable contracts. Proxy contracts that store the chain ID in an initializer may upgrade the implementation but not re-initialize the chain ID. If a fork occurs between deployment and upgrade, the proxy carries a stale chain ID.
Why it matters: Hard forks are rare but high-impact. The ETH/ETC split proved that chain splits happen on live networks with billions in value. Contracts with hardcoded chain IDs cannot adapt.
T3: Missing Chain ID in EIP-712 Domain Separator (High)
EIP-712 defines the domain separator as hashStruct(EIP712Domain(...)), where chainId is one of the fields. While the specification makes chainId optional, omitting it is a critical mistake for any contract deployed on multiple chains:
-
Permit signatures replayable across chains. ERC-2612
permit()uses EIP-712 to sign token approvals off-chain. If the domain separator omitschainId, a permit signed on Ethereum mainnet can be replayed on Optimism, Base, Arbitrum, etc. The attacker gains an approval on every chain where the token contract exists, then drains the victim’s balances. -
Governance vote replay. Off-chain governance systems (e.g., Snapshot-style signed votes relayed on-chain) that omit
chainIdfrom the signed payload allow votes to be replayed across chains. An attacker can duplicate a whale’s vote on a governance fork or sidechain deployment. -
Wallet-level failures. In April 2023, Coinspect discovered that over 40 major wallet vendors (including Coinbase, Exodus, Phantom, Rabby) had EIP-712 implementation issues related to chain ID handling. Wallets that don’t enforce chain ID in the signing flow can produce signatures that users believe are chain-specific but are actually chain-agnostic.
Why it matters: EIP-712 is the backbone of gasless approvals, meta-transactions, and off-chain order books. Permit/Permit2 authorization phishing accounted for 38% of thefts exceeding $1M in 2025, and missing chain ID protection is a root cause.
T4: L2 Chain ID Confusion and Multi-Chain Deployment Risks (Medium)
The proliferation of L2 chains creates an environment where chain ID management is operationally complex and error-prone:
-
Deploying the same contract to wrong chain ID. Developers who deploy via scripts may hardcode chain IDs in configuration files. Deploying to a testnet (chain ID 11155111 for Sepolia) and then forgetting to update before mainnet deployment leaves the contract checking the wrong chain ID.
-
L2 chain ID reuse and collisions. While IANA-style registration exists (chainlist.org), nothing prevents a new L2 from reusing an existing chain ID. If two chains share a chain ID, EIP-155 replay protection breaks entirely — transactions on one chain are valid on the other by construction.
-
Bridge contracts relying on self-reported chain ID. Cross-chain bridges that use
block.chainidto determine the source chain trust the execution environment. If a bridge contract is deployed on a malicious chain that spoofs a chain ID (e.g., claims to be chain 1), the bridge may accept messages as if they originated from Ethereum mainnet.
Why it matters: Cross-chain deployment is now standard practice. The average DeFi protocol deploys to 5+ chains. Every deployment is a potential replay target if chain ID handling is incorrect.
T5: Permit and Signature Replay Across Forks and Chains (Critical)
ERC-2612 permits and ERC-20 approval signatures are the most actively exploited CHAINID vulnerability class:
-
Permit replay across chain forks. When a chain forks (e.g., a contentious upgrade), both forks initially share transaction history. Outstanding permit signatures that included the pre-fork chain ID are valid on whichever fork retains that chain ID. On the fork that changes its chain ID, contracts with dynamic chain ID handling correctly reject old signatures, but contracts with cached/hardcoded values accept them.
-
Permit2 universal approval risks. Uniswap’s Permit2 contract centralizes approval management. A Permit2 signature that omits or incorrectly binds the chain ID could grant approvals across all chains where Permit2 is deployed (same address via CREATE2). The blast radius of a single compromised signature is multiplied by the number of chains.
-
Signature phishing with chain ID mismatch. An attacker creates a phishing site that requests a permit signature on a low-value testnet or sidechain. If the victim’s wallet doesn’t clearly display the target chain, and the contract’s domain separator omits chain ID, the signature works on mainnet where real assets exist.
Why it matters: Permit phishing is the fastest-growing attack vector in DeFi. A single signature can drain a victim’s entire token balance on every chain where the token contract is deployed.
Protocol-Level Threats
P1: Chain Fork Implications — EIP-155 Replay Protection (High)
EIP-155 binds transaction signatures to a specific chain ID by including it in the signing hash. This is Ethereum’s foundational replay protection mechanism. The protocol-level risks include:
-
Pre-EIP-155 transactions remain replayable. Transactions signed before the Spurious Dragon fork (block 2,675,000) use the legacy signature scheme (
v = 27 or 28) without chain ID. These transactions are valid on both ETH and ETC (and any other fork that accepts legacy signatures). While increasingly rare, legacy transactions are still accepted by the protocol for backwards compatibility. -
Contentious forks create dual-chain replay windows. When a chain splits, both forks initially share the same chain ID. Until one fork changes its chain ID, every new transaction signed on one fork is replayable on the other. The ETH/ETC split had an extended replay window that caused significant losses before EIP-155 was widely adopted.
-
Chain ID governance is informal. There is no on-chain registry or protocol-level enforcement of chain ID uniqueness. New chains self-assign IDs via chainlist.org (a community resource). Accidental or malicious chain ID collisions would break EIP-155 protection between the colliding chains.
P2: CHAINID Opcode Semantics Across EVM Versions (Low)
The CHAINID opcode (0x46) was introduced in Istanbul (EIP-1344, October 2019). Before Istanbul, there was no way to read the chain ID on-chain — contracts had to hardcode it. Protocol-level considerations include:
-
Pre-Istanbul contracts cannot access chain ID. Contracts deployed before Istanbul that need chain ID must receive it as a function parameter or store it as an immutable. These contracts cannot self-validate their chain context.
-
CHAINID is deterministic and non-manipulable. Unlike COINBASE or TIMESTAMP, the chain ID cannot be influenced by validators or MEV searchers. It is set in the chain configuration and is identical for all transactions in all blocks. This makes it a reliable identity primitive at the protocol level.
-
Gas cost is negligible. At 2 gas (Gbase), CHAINID cannot be used for gas griefing. There is no performance-related threat.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
| Chain ID during a hard fork | Both forks initially share the same chain ID until one explicitly changes it | Replay window: transactions on one fork are valid on the other until chain IDs diverge |
| L2 chain IDs | Each L2 has a unique chain ID (Optimism: 10, Base: 8453, Arbitrum One: 42161) | Contracts deployed to multiple L2s must use block.chainid dynamically; hardcoded values fail on all other chains |
| Chain ID = 0 | Technically valid in the EVM; block.chainid returns 0 | Pre-EIP-155 transactions don’t include chain ID (equivalent to chain ID 0); chain ID 0 signatures may pass verification on chains that accept legacy transactions |
| Immutable chain ID in contracts | Stored at construction, never updated | Breaks after any fork that changes the chain ID; replays from the old chain are accepted |
| Dynamic chain ID (OpenZeppelin pattern) | Constructor caches block.chainid; runtime compares and recomputes if changed | Correctly handles forks; the domain separator updates automatically when the chain ID changes |
| CHAINID in DELEGATECALL | Returns the same block.chainid as the parent context | No call-level variation; CHAINID is a chain property, not a message property |
| CHAINID in STATICCALL | Returns the same block.chainid (read-only, no state change) | No special behavior; safe to use in view functions |
| Testnets vs. mainnet chain IDs | Testnets have different chain IDs (Sepolia: 11155111, Holesky: 17000) | Signatures from testnets cannot be replayed on mainnet (and vice versa) as long as chain ID is included in the signed payload |
| CREATE2 same-address deployments | Contract deployed to same address on multiple chains via CREATE2 | verifyingContract in domain separator is identical across chains; chainId is the only differentiator for replay protection |
Real-World Exploits
Exploit 1: Optimism-Wintermute Cross-Chain Replay — 20M OP Tokens Stolen ($15M, June 2022)
Root cause: Non-EIP-155 deployment transactions (lacking chain ID protection) allowed an attacker to replay Gnosis Safe factory deployment transactions from Ethereum mainnet onto Optimism, gaining control of a multisig wallet holding 20 million OP tokens.
Details: In May 2022, the Optimism Foundation allocated 20 million OP tokens to Wintermute for liquidity provisioning. Wintermute provided their Ethereum L1 Gnosis Safe multisig address, but had not yet deployed that Safe contract on Optimism (L2). The tokens were sent to the L1 address on Optimism’s network, sitting in a contract address with no code.
The attacker exploited a critical chain ID omission: Wintermute’s Gnosis Safe had been deployed on mainnet using Proxy Factory v1.1.1 (from 2019), which used non-EIP-155-compliant deployment transactions — the transactions did not include the chain ID in their signatures. Because Optimism accepted legacy (non-EIP-155) transactions, the attacker replayed the exact Proxy Factory deployment transaction on Optimism, deploying the factory at the same address.
Using CREATE (which derives addresses from factory address + nonce), the attacker needed to reach nonce 8,884 to produce Wintermute’s Safe address. They deployed 162 dummy Safes per transaction across 62 transactions to reach the target nonce, then deployed a Safe they controlled at the target address. With the Safe under their control, they claimed all 20 million OP tokens.
CHAINID’s role: The entire attack was possible because the original Gnosis Safe deployment transactions lacked chain ID (EIP-155) protection. Had those deployment transactions included CHAIN_ID = 1 in their signatures, they would have been invalid on Optimism (chain ID 10). The CHAINID opcode didn’t exist when the factory was deployed (2019, pre-Istanbul on some tooling), and the deployment tools did not enforce EIP-155.
Impact: 20 million OP tokens (~$15M) stolen. Wintermute acknowledged it was “100% their fault.” The Optimism Foundation issued a replacement grant.
References:
- banteg: How Did a Hacker Steal OP?
- Inspex: How 20 Million OP Was Stolen
- CoinDesk: $15M of Optimism Tokens Stolen
Exploit 2: ETH/ETC Chain Split — Massive Cross-Chain Transaction Replay (July 2016)
Root cause: The Ethereum hard fork that created Ethereum Classic (ETC) did not include replay protection. Both chains shared chain ID 1, and every transaction signed on one chain was valid on the other.
Details: On July 20, 2016, the Ethereum network hard-forked to reverse the DAO hack. The minority chain that rejected the fork continued as Ethereum Classic. Because EIP-155 had not yet been implemented, both chains used identical transaction formats with no chain ID binding. Any transaction broadcast on ETH could be replayed on ETC (and vice versa) by simply rebroadcasting it to the other chain’s network.
Exchanges were particularly affected. When a user withdrew ETH, the exchange signed and broadcast a transaction on the ETH chain. Attackers (or the users themselves) could replay that exact transaction on ETC, causing the exchange to unknowingly send ETC as well. Multiple exchanges reported losses from replay attacks in the days following the fork. The Ethereum community rushed to implement EIP-155 (Simple Replay Attack Protection), which was activated at the Spurious Dragon hard fork (block 2,675,000, November 2016).
CHAINID’s role: The absence of any chain ID mechanism was the root cause. Pre-EIP-155 transactions included no chain identifier in the signed hash. EIP-155 fixed the protocol layer by encoding the chain ID into the signature’s v parameter. EIP-1344 (CHAINID opcode) later extended this to smart contracts, letting them read the chain ID at runtime for application-layer replay protection.
Impact: Unquantified but significant losses across exchanges and individual users. The incident directly motivated EIP-155 and, years later, EIP-1344.
References:
- EIP-155: Simple Replay Attack Protection
- Hacking Distributed: Cross-Chain Replay Attacks
- Ethereum Classic: Replay Attacks Explained
Exploit 3: EIP-712 Chain ID Implementation Flaw — 40+ Wallet Vendors Affected (April 2023)
Root cause: Over 40 major cryptocurrency wallets had EIP-712 implementation issues related to chain ID handling, enabling potential cross-chain signature replay.
Details: In April 2023, security firm Coinspect disclosed that more than 40 wallet vendors — including Coinbase Wallet, Exodus, Phantom, and Rabby — had flaws in how they processed EIP-712 typed data signatures with respect to chain ID. The wallets did not properly enforce or display the chainId field in the domain separator, meaning users could be tricked into signing messages that appeared chain-specific but were actually valid across multiple chains.
The vulnerability affected the signing flow itself: wallets either ignored the chainId field when presenting the signing request to the user, or failed to validate that the requested chain ID matched the wallet’s currently connected chain. An attacker could construct an EIP-712 signing request with a different chain ID (or no chain ID at all), and the wallet would present it as a legitimate request on the user’s current chain.
CHAINID’s role: The CHAINID opcode correctly provides the chain ID to on-chain verification. The vulnerability was in the off-chain signing flow — wallets did not use the chain ID to validate or restrict what users were signing. This demonstrates that CHAINID alone is not sufficient; the entire signing pipeline (wallet UI, domain separator construction, on-chain verification) must consistently enforce chain ID binding.
Impact: No confirmed exploits in the wild, but the vulnerability window affected millions of wallet users across 40+ products. The disclosure prompted coordinated patching across the wallet ecosystem.
References:
Exploit 4: Permit Signature Phishing — Cross-Chain Drain via Missing Chain ID (2024-2025, Recurring)
Root cause: ERC-2612 permit signatures obtained via phishing are replayed across multiple chains where the victim holds tokens, especially when domain separators lack or mishandle chain ID.
Details: Permit/Permit2 authorization phishing emerged as the dominant high-value attack vector in 2024-2025. Attackers create phishing sites that request ERC-2612 permit signatures from victims. When the target token contract’s domain separator properly includes chainId, the signature is limited to one chain. However, many token contracts (particularly early implementations or forks of non-standard tokens) either omit chainId from the domain separator or cache it immutably.
In the worst case, a single permit signature grants the attacker an unlimited approval on every chain where the token contract is deployed. The attacker signs the victim’s permit on a low-activity chain (where the transaction is less likely to be noticed), then replays it on mainnet and every L2 where the victim holds balances.
CHAINID’s role: Proper use of block.chainid in the domain separator is the primary defense. Contracts that compute the domain separator dynamically (checking block.chainid at runtime) limit each permit to one chain. Contracts that hardcode or cache the chain ID without revalidation leave the door open for cross-chain replay.
Impact: Permit/Permit2 phishing accounted for 38% of thefts exceeding 6.5M. Cumulative losses across the 2024-2025 period are estimated in the hundreds of millions.
References:
- 7BlockLabs: Identifying and Fixing Permit Signature Vulnerabilities
- ChainScore Labs: ERC-20 Permit Security Risks
Attack Scenarios
Scenario A: Cross-Chain Permit Replay (Missing Chain ID in Domain Separator)
// Vulnerable: Domain separator omits chainId
contract VulnerableToken is ERC20 {
bytes32 public immutable DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
constructor(string memory name) ERC20(name, "VTK") {
// BUG: No chainId in domain separator -- identical on every chain
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes("1")),
address(this) // same address on all chains via CREATE2
));
}
function permit(
address owner, address spender, uint256 value,
uint256 deadline, uint8 v, bytes32 r, bytes32 s
) external {
require(block.timestamp <= deadline, "expired");
bytes32 structHash = keccak256(abi.encode(
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
owner, spender, value, nonces[owner]++, deadline
));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
require(ecrecover(digest, v, r, s) == owner, "invalid sig");
_approve(owner, spender, value);
}
}
// Attack flow:
// 1. Attacker phishes a permit signature from victim on Optimism (chain 10)
// 2. Since DOMAIN_SEPARATOR has no chainId, the same signature is valid on
// mainnet (chain 1), Base (8453), Arbitrum (42161), etc.
// 3. Attacker calls permit() on every chain where the token is deployed
// 4. Attacker calls transferFrom() to drain victim's balance on all chainsScenario B: Hardcoded Chain ID Breaks After Fork
// Vulnerable: Chain ID cached at construction, never revalidated
contract FragileToken is ERC20 {
bytes32 private immutable _DOMAIN_SEPARATOR;
// No cached chain ID check -- once set, never changes
constructor() ERC20("Fragile", "FRG") {
_DOMAIN_SEPARATOR = _buildDomainSeparator();
}
function _buildDomainSeparator() private view returns (bytes32) {
return keccak256(abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256("Fragile"),
keccak256("1"),
block.chainid, // Captured once at construction
address(this)
));
}
function DOMAIN_SEPARATOR() public view returns (bytes32) {
// BUG: Always returns cached value, even after fork changes chain ID
return _DOMAIN_SEPARATOR;
}
// After a chain fork:
// - Original chain keeps chain ID 1, cached DOMAIN_SEPARATOR is correct
// - New fork assigns chain ID 9999, but _DOMAIN_SEPARATOR still contains 1
// - Permits signed for chain 1 are now valid on the fork (chain 9999)
// because the contract checks against the stale domain separator
}
// Safe pattern (OpenZeppelin):
// function DOMAIN_SEPARATOR() public view returns (bytes32) {
// if (block.chainid == _cachedChainId) return _cachedDomainSeparator;
// return _buildDomainSeparator(); // Recompute with current chain ID
// }Scenario C: Cross-Chain Deployment Replay via Non-EIP-155 Transactions
// This scenario mirrors the Optimism-Wintermute exploit.
// Factory deployed via legacy (non-EIP-155) transaction on mainnet.
// The same transaction bytes can be broadcast on any chain that accepts
// legacy transactions, deploying the factory at the same address.
// Step 1: Original factory deployment on mainnet (2019, pre-EIP-155 enforcement)
// Raw transaction: no chain ID in v value (v = 27 or 28)
// Factory deployed at: 0xABCD...1234
// Step 2: Attacker replays the exact raw transaction on Optimism
// Optimism accepts legacy transactions -> factory appears at 0xABCD...1234
// Attacker now controls the factory's nonce on Optimism
// Step 3: Attacker uses the factory to CREATE contracts,
// incrementing nonce until it produces the target address
// where tokens are sitting
// Step 4: Attacker deploys their own contract at the target address,
// gaining control of all assets sent there
// Defense: Always use EIP-155 transactions (include chain ID in v).
// Modern wallets and libraries enforce this by default.
// Verify that deployment tools produce EIP-155-compliant transactions.Scenario D: Governance Vote Replay Across Chains
contract VulnerableGovernor {
mapping(bytes32 => bool) public executed;
struct Proposal {
uint256 id;
address target;
bytes data;
}
// Vulnerable: signed vote doesn't include chain ID
function executeWithSignatures(
Proposal calldata proposal,
bytes[] calldata signatures
) external {
bytes32 proposalHash = keccak256(abi.encode(
proposal.id, proposal.target, proposal.data
// BUG: no block.chainid in hash
));
require(!executed[proposalHash], "already executed");
uint256 validSigs = 0;
for (uint256 i = 0; i < signatures.length; i++) {
address signer = recoverSigner(proposalHash, signatures[i]);
if (isGovernor(signer)) validSigs++;
}
require(validSigs >= quorum, "no quorum");
executed[proposalHash] = true;
(bool ok,) = proposal.target.call(proposal.data);
require(ok);
}
// Attack: Governance passes a proposal on mainnet.
// Attacker replays the same proposal + signatures on L2
// where the same governor contract is deployed.
// The proposal executes with the same quorum on L2,
// potentially draining a different treasury.
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Cross-chain signature replay | Always include block.chainid in every signed payload | Add chainId to all EIP-712 domain separators; include chain ID in custom signature schemes |
| T2: Hardcoded chain ID after fork | Use dynamic chain ID with revalidation | OpenZeppelin’s EIP712._domainSeparatorV4() pattern: cache domain separator + chain ID, recompute if block.chainid changes |
| T3: Missing chain ID in EIP-712 | Include all five domain separator fields | EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) — never omit chainId |
| T4: L2 chain ID confusion | Validate chain ID in deployment scripts and on-chain | Add require(block.chainid == EXPECTED_CHAIN_ID) in initializers for chain-specific contracts; use chain-aware deployment tooling |
| T5: Permit replay across chains | Dynamic domain separator + nonce management | Use OpenZeppelin’s ERC20Permit (dynamic chain ID revalidation); ensure nonce is consumed atomically; set short deadlines |
| P1: Pre-EIP-155 legacy transactions | Enforce EIP-155 in all deployment and operational transactions | Configure wallets and deployment tools to always produce EIP-155 transactions; reject legacy (v=27/28) transactions where possible |
| General: Wallet-level chain ID enforcement | Validate chain ID in the signing UI | Wallets should display and validate the chainId in EIP-712 requests against the currently connected chain; reject mismatches |
| General: Multi-chain deployment safety | Audit signature schemes for each chain | Before deploying to a new chain, verify that all signature verification includes block.chainid and that domain separators are chain-specific |
Compiler/EIP-Based Protections
- EIP-155 (Spurious Dragon, 2016): Encodes chain ID into transaction signatures (
v = {0,1} + CHAIN_ID * 2 + 35). The foundational protocol-level replay protection. All modern wallets produce EIP-155 transactions by default. - EIP-1344 (Istanbul, 2019): Introduces the
CHAINIDopcode (0x46), giving smart contracts runtime access to the chain ID. Eliminates the need to hardcode chain IDs at compile time. - EIP-712 (2017): Defines typed structured data hashing with domain separators that should include
chainId. Not enforced at the protocol level, but the standard is widely adopted. - ERC-2612 (Permit): Standardizes off-chain approval via EIP-712 signatures. OpenZeppelin’s implementation includes dynamic chain ID revalidation.
- OpenZeppelin EIP712 (>= v4.1): Caches
block.chainidat construction and recomputes the domain separator at runtime if the chain ID changes. This is the industry-standard pattern for fork-safe signature verification.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | High | Optimism-Wintermute ($15M), ETH/ETC replay (2016), multi-chain replay (ongoing) |
| T2 | Smart Contract | High | Low | Theoretical during forks; mitigated by OpenZeppelin’s dynamic pattern |
| T3 | Smart Contract | High | High | 40+ wallet vendors affected (2023), permit phishing ($6.5M single incident) |
| T4 | Smart Contract | Medium | Medium | L2 deployment misconfigurations, EVM-inequivalent code smells (~17.7% prevalence) |
| T5 | Smart Contract | Critical | High | Permit phishing = 38% of >$1M thefts in 2025; hundreds of millions in cumulative losses |
| P1 | Protocol | High | Low | ETH/ETC split (2016); rare but catastrophic when forks occur |
| P2 | Protocol | Low | N/A | No known exploits; CHAINID semantics are simple and deterministic |
Related Opcodes
| Opcode | Relationship |
|---|---|
| ADDRESS (0x30) | Returns the current contract’s address. Combined with CHAINID, these two values uniquely identify a contract across all EVM chains — both are required in EIP-712 domain separators (verifyingContract + chainId). |
| ORIGIN (0x32) | Returns tx.origin (the original EOA). Like CHAINID, ORIGIN is a transaction-level context value. Some legacy contracts use tx.origin checks instead of proper signature verification with chain ID binding. |
| CALLER (0x33) | Returns msg.sender (the immediate caller). CALLER provides identity within a call chain; CHAINID provides identity of the chain itself. Both are needed for complete replay protection — CALLER tells you who is calling, CHAINID tells you where. |