Opcode Summary

PropertyValue
Opcode0x46
MnemonicCHAINID
Gas2
Stack Input(none)
Stack Outputchain_id (uint256)
BehaviorPushes 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:

  1. CHAINID is the sole on-chain mechanism for cross-chain replay protection. EIP-155 encodes the chain ID into the transaction signature’s v parameter (v = {0,1} + CHAIN_ID * 2 + 35), making transactions chain-specific. EIP-712 domain separators include chainId to bind typed data signatures to a single chain. If a contract computes a domain separator without block.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.

  2. 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.

  3. 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.chainid from 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 chainId field 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, the verifyingContract field 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 omits chainId, 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 chainId from 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.chainid to 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 CaseBehaviorSecurity Implication
Chain ID during a hard forkBoth forks initially share the same chain ID until one explicitly changes itReplay window: transactions on one fork are valid on the other until chain IDs diverge
L2 chain IDsEach 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 = 0Technically valid in the EVM; block.chainid returns 0Pre-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 contractsStored at construction, never updatedBreaks 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 changedCorrectly handles forks; the domain separator updates automatically when the chain ID changes
CHAINID in DELEGATECALLReturns the same block.chainid as the parent contextNo call-level variation; CHAINID is a chain property, not a message property
CHAINID in STATICCALLReturns the same block.chainid (read-only, no state change)No special behavior; safe to use in view functions
Testnets vs. mainnet chain IDsTestnets 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 deploymentsContract deployed to same address on multiple chains via CREATE2verifyingContract 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:


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:


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:


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 chains

Scenario 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

ThreatMitigationImplementation
T1: Cross-chain signature replayAlways include block.chainid in every signed payloadAdd chainId to all EIP-712 domain separators; include chain ID in custom signature schemes
T2: Hardcoded chain ID after forkUse dynamic chain ID with revalidationOpenZeppelin’s EIP712._domainSeparatorV4() pattern: cache domain separator + chain ID, recompute if block.chainid changes
T3: Missing chain ID in EIP-712Include all five domain separator fieldsEIP712Domain(string name,string version,uint256 chainId,address verifyingContract) — never omit chainId
T4: L2 chain ID confusionValidate chain ID in deployment scripts and on-chainAdd require(block.chainid == EXPECTED_CHAIN_ID) in initializers for chain-specific contracts; use chain-aware deployment tooling
T5: Permit replay across chainsDynamic domain separator + nonce managementUse OpenZeppelin’s ERC20Permit (dynamic chain ID revalidation); ensure nonce is consumed atomically; set short deadlines
P1: Pre-EIP-155 legacy transactionsEnforce EIP-155 in all deployment and operational transactionsConfigure wallets and deployment tools to always produce EIP-155 transactions; reject legacy (v=27/28) transactions where possible
General: Wallet-level chain ID enforcementValidate chain ID in the signing UIWallets should display and validate the chainId in EIP-712 requests against the currently connected chain; reject mismatches
General: Multi-chain deployment safetyAudit signature schemes for each chainBefore 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 CHAINID opcode (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.chainid at 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 IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalHighOptimism-Wintermute ($15M), ETH/ETC replay (2016), multi-chain replay (ongoing)
T2Smart ContractHighLowTheoretical during forks; mitigated by OpenZeppelin’s dynamic pattern
T3Smart ContractHighHigh40+ wallet vendors affected (2023), permit phishing ($6.5M single incident)
T4Smart ContractMediumMediumL2 deployment misconfigurations, EVM-inequivalent code smells (~17.7% prevalence)
T5Smart ContractCriticalHighPermit phishing = 38% of >$1M thefts in 2025; hundreds of millions in cumulative losses
P1ProtocolHighLowETH/ETC split (2016); rare but catastrophic when forks occur
P2ProtocolLowN/ANo known exploits; CHAINID semantics are simple and deterministic

OpcodeRelationship
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.