This ERC defines a standard approach for creating and verifying cross-chain signatures using EIP-712. By omitting the chainId field from the EIP-712 domain (which is optional per the standard), a signature can be valid across multiple chains. Chain-specific operations are encoded as an array of structured messages, where each chain receives the array of message hashes and only the full message data relevant to that chain. This enables efficient cross-chain intents, multi-chain DAO voting, and unified account management using standard EIP-712 encoding without requiring special wallet support.
Motivation
Current account abstraction solutions require separate signatures for each blockchain network. This creates poor user experience for cross-chain operations such as:
Cross-chain intents: Users wanting to trade assets across multiple chains atomically
Multi-chain DAO governance: Voting on proposals that affect protocol instances across different networks
Unified account management: Managing the same account deployed on multiple chains
Cross-chain social recovery: Recovery processes that span multiple networks
Existing proposals either require complex Merkle tree constructions (which need wallet-specific UI to verify all leaves) or non-standard encoding schemes that lack wallet adoption. This ERC provides a simpler approach using only standard EIP-712 encoding with array types, enabling cross-chain signatures with minimal on-chain overhead while maintaining full transparency in standard wallet signing interfaces.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Cross-Chain Domain Semantics
A cross-chain EIP-712 signature MUST omit the chainId field from the EIP712Domain type and domain object. Since chainId is optional per EIP-712, omitting it signals that the signature is intended for cross-chain validity. The domain’s verifyingContract field MAY be set to the account address that controls the operations across chains if it is deterministically deployed at the same address across all target chains.
Message Array Encoding
Cross-chain operations MUST be encoded as an array of chain-specific message structs. Each struct in the array SHOULD include a nested domain struct identifying both the target chain and verifying contract. This follows EIP-712’s recursive encoding pattern and ensures proper binding to chain-specific contracts.
The nested domain struct SHOULD be named to avoid collision with EIP712Domain (e.g., EIP712ChainDomain) and SHOULD include at minimum the chainId field. In case the root EIP712Domain does not include the verifyingContract field, the nested domain struct MUST include it.
Per EIP-712, arrays are encoded as keccak256(encodeData(element[0]) ‖ encodeData(element[1]) ‖ ... ‖ encodeData(element[n])), where each element is a struct that is recursively encoded as hashStruct(element[i]), and nested structs are also recursively hashed.
On-Chain Verification
When verifying a cross-chain signature on a specific chain, contracts MUST:
Accept an array of message hashes corresponding to all chains in the signed message array
Accept the full message data for the current chain only
Compute hashStruct(currentChainMessage) and verify it matches the hash at the expected position in the provided array
Reconstruct the full EIP-712 signature hash using the array of hashes
Verify the signature against the reconstructed hash
This approach minimizes on-chain calldata and computation costs since only one full message is passed to each chain, while the other chains’ operations are represented as 32-byte hashes.
Wallet Display
Standard EIP-712 wallets will automatically display the array of chain-specific messages in a readable format. Wallets MAY enhance the display by grouping operations by chain and showing chain names instead of chain IDs for better user experience.
Rationale
Standard EIP-712 Compatibility
This ERC uses only standard EIP-712 encoding without any extensions or special constructs. All fields in the EIP712Domain are optional per the standard, so omitting chainId is perfectly valid. This means:
No Special Wallet Support Required: Any wallet that supports EIP-712 can already sign these messages
Readable by Default: Wallets automatically display the array of chain-specific operations
Proven Security: Relies on well-tested EIP-712 encoding rather than custom schemes
Array Encoding vs Merkle Trees
Alternative approaches use Merkle trees to commit to cross-chain operations. While Merkle trees can reduce on-chain overhead for many chains, they have a critical drawback:
Wallet Verification Complexity: Standard EIP-712 wallets cannot display Merkle tree leaves. Users signing a Merkle root have no way to verify all operations in their wallet UI. Wallets would need to implement custom logic to:
Request all leaves from the application
Verify the Merkle tree construction
Display all operations across all chains
Ensure no malicious operations are hidden
This breaks the principle of trustless signing, users must trust the application to correctly provide all leaves, and wallet developers must implement and maintain custom verification logic.
The array-based approach provides full transparency using standard EIP-712. Users see all chain-specific operations in any compliant wallet without custom support. No hidden operations are possible since all array elements are displayed as part of the standard EIP-712 message structure.
On-chain overhead comparison: For reasonable cross-chain operations, the overhead difference is minimal.
With the array approach, each chain receives all N operation hashes (N × 32 bytes). With Merkle trees (assuming a binary tree), each chain receives a Merkle proof of size ceil(log₂(N)) × 32 bytes. Both approaches reconstruct the root/array hash on-chain from the provided data and verify it against the signature, no additional calldata for the root is needed. While Merkle proofs grow logarithmically vs. the array’s linear growth, the practical savings are small for reasonable use cases:
Chains
Array (N × 32)
Merkle (⌈log₂(N)⌉ × 32)
Savings
2
64 bytes (2 × 32)
32 bytes (1 × 32)
32 bytes (1 hash)
3
96 bytes (3 × 32)
64 bytes (2 × 32)
32 bytes (1 hash)
4
128 bytes (4 × 32)
64 bytes (2 × 32)
64 bytes (2 hashes)
5
160 bytes (5 × 32)
96 bytes (3 × 32)
64 bytes (2 hashes)
8
256 bytes (8 × 32)
96 bytes (3 × 32)
160 bytes (5 hashes)
For 2-5 chains (the reasonable case for cross-chain operations), Merkle trees save only 32-64 bytes (1-2 hashes) per transaction. This minimal savings doesn’t justify the complexity and loss of transparency. Merkle trees only become significantly more efficient at 8+ chains, which is an uncommon use case and may indicate the operation should be split into multiple signatures for better user comprehension and failure isolation.
Account-Centric Design
By setting verifyingContract to the user’s account address, this design enables:
Signature Binding: Each signature is bound to a specific account, preventing unauthorized reuse
Counterfactual Accounts: Works with accounts that haven’t been deployed yet
Replay Protection: Applications can implement nonces or other replay protection at the account level
Omitting chainId vs Using Zero
Omitting chainId entirely is cleaner than using a special value like 0:
Semantic Clarity: Absence explicitly signals “no specific chain”
Standards Compliance: All EIP-712 domain fields are optional
No Special Cases: No need to treat 0 as a magic value
Backwards Compatibility
This ERC uses standard EIP-712 without modifications. Existing wallets and applications that support EIP-712 can immediately work with cross-chain signatures without any changes, though they may not recognize the cross-chain semantics.
Applications that verify signatures with a domain that includes a specific chainId will reject cross-chain signatures (where chainId is omitted), providing safe failure by default. Applications that wish to support cross-chain signatures must explicitly implement the verification pattern described in this ERC.
Reference Implementation
A collection of examples of how to use this ERC to fulfill the Motivation use cases.
Cross-Chain Intent Example
A user wants to execute a cross-chain trade: sell USDC on Ethereum, receive ETH on Arbitrum:
{types:{EIP712Domain:[{name:"name",type:"string"},{name:"version",type:"string"},{name:"verifyingContract",type:"address"}// Deterministically deployed at the same address across all target chains// Note: chainId is omitted for cross-chain validity],CrossChainIntent:[{name:"operations",type:"ChainOperation[]"},{name:"nonce",type:"uint256"},{name:"deadline",type:"uint256"}],ChainOperation:[{name:"domain",type:"EIP712ChainDomain"},{name:"target",type:"address"},{name:"value",type:"uint256"},{name:"data",type:"bytes"}],EIP712ChainDomain:[{name:"chainId",type:"uint256"}// Note: verifyingContract is omitted since it's specified in the root EIP712Domain]},primaryType:"CrossChainIntent",domain:{name:"CrossChainDEX",version:"1",verifyingContract:"0x123321..."// User's account},message:{operations:[{domain:{chainId:1},target:"0xA0b86a33E6776885F5Db...",// USDC contractvalue:0,data:"0xa9059cbb..."// transfer(settler, 1000 USDC)},{domain:{chainId:42161},target:"0xArbitrumSettler...",value:0,data:"0x3ccfd60b..."// claim(0.5 ETH min)}],nonce:42,deadline:1704067200}}
On-chain verification on Ethereum:
functionexecuteIntent(bytes32[]calldataoperationHashes,// [hash(op[0]), hash(op[1])]
ChainOperationcalldatacurrentOp,// Full data for op[0]
uint256currentIndex,// 0 (Ethereum is first)
uint256nonce,uint256deadline,bytescalldatasignature)external{// Verify the current operation hash matches
require(hashStruct(currentOp)==operationHashes[currentIndex],"Invalid operation");require(currentOp.domain.chainId==block.chainid,"Wrong chain");// Note: verifyingContract validation is omitted because it's already part of the root EIP712Domain
// Reconstruct the full intent hash
bytes32operationsHash=keccak256(abi.encodePacked(operationHashes));bytes32intentHash=hashStruct(CrossChainIntent({operations:operationsHash,// Array is encoded as hash of elements
nonce:nonce,deadline:deadline}));// Verify signature
bytes32digest=keccak256(abi.encodePacked("\x19\x01",domainSeparator,intentHash));require(account.isValidSignature(digest,signature)==ERC1271_MAGIC_VALUE);// Execute the operation
(boolsuccess,)=currentOp.target.call{value:currentOp.value}(currentOp.data);require(success,"Execution failed");}
The Arbitrum settler would use the same signature with currentIndex: 1 and the full data for op[1]. Each chain only receives the full calldata for its own operation, while other operations are represented as 32-byte hashes.
Multi-Chain Governance Example
A DAO member votes on a proposal affecting all chain deployments:
{types:{EIP712Domain:[{name:"name",type:"string"},{name:"version",type:"string"}// Note: verifyingContract is omitted since the governor is deployed on different addresses across chains for this example],MultiChainVote:[{name:"votes",type:"ChainVote[]"},{name:"nonce",type:"uint256"}],ChainVote:[{name:"domain",type:"EIP712ChainDomain"},{name:"proposalId",type:"uint256"},{name:"support",type:"uint8"},{name:"reason",type:"string"}],EIP712ChainDomain:[{name:"chainId",type:"uint256"},{name:"verifyingContract",type:"address"}]},primaryType:"MultiChainVote",domain:{name:"MultiChainDAO",version:"1"},message:{votes:[{domain:{chainId:1,verifyingContract:"0x123321..."// DAO contract on Ethereum},proposalId:42,support:1,// Forreason:"This upgrade improves security"},{domain:{chainId:137,verifyingContract:"0x321321..."// DAO contract on Polygon},proposalId:42,support:1,// Forreason:"This upgrade improves security"}],nonce:7}}
This vote signature can be submitted to DAO contracts on both Ethereum and Polygon, enabling coordinated multi-chain governance decisions.
Unified Account Management Example
A user wants to add a new signer to their multisig account deployed across multiple chains:
{types:{EIP712Domain:[{name:"name",type:"string"},{name:"version",type:"string"},{name:"verifyingContract",type:"address"}// Deterministically deployed at the same address across all target chains],MultiChainAccountUpdate:[{name:"updates",type:"AccountUpdate[]"},{name:"nonce",type:"uint256"}],AccountUpdate:[{name:"domain",type:"EIP712ChainDomain"},{name:"operation",type:"uint8"},// 0=addSigner, 1=removeSigner, 2=changeThreshold{name:"signerData",type:"bytes"},{name:"threshold",type:"uint256"}],EIP712ChainDomain:[{name:"chainId",type:"uint256"},]},primaryType:"MultiChainAccountUpdate",domain:{name:"MultiChainMultisig",version:"1",verifyingContract:"0x123321..."// Initiating account},message:{updates:[{domain:{chainId:1,},operation:0,// addSignersignerData:"0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",threshold:3},{domain:{chainId:137,},operation:0,// addSignersignerData:"0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",threshold:3},{domain:{chainId:42161,},operation:0,// addSignersignerData:"0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",threshold:3}],nonce:42}}
On-chain execution:
functionupdateAccountWithSignature(bytes32[]calldataupdateHashes,AccountUpdatecalldatacurrentUpdate,uint256currentIndex,uint256nonce,bytescalldatasignature)external{require(hashStruct(currentUpdate)==updateHashes[currentIndex]);require(currentUpdate.domain.chainId==block.chainid);// Note: verifyingContract validation is omitted because it's already part of the root EIP712Domain
bytes32updatesHash=keccak256(abi.encodePacked(updateHashes));bytes32digest=hashTypedData(MultiChainAccountUpdate({updates:updatesHash,nonce:nonce}));require(isValidSignature(digest,signature)==ERC1271_MAGIC_VALUE);if(currentUpdate.operation==0){_addSigner(currentUpdate.signerData,currentUpdate.threshold);}// ... other operations
}
This signature enables the multisig owners to add a new signer and update the threshold across all chain deployments simultaneously. The same account address exists on Ethereum, Polygon, and Arbitrum, and this single signature authorizes the updates on all three networks.
Cross-Chain Social Recovery Example
A user has lost access to their account and guardians need to initiate recovery across multiple networks:
{types:{EIP712Domain:[{name:"name",type:"string"},{name:"version",type:"string"}// Note: verifyingContract is omitted since the recovery module is deployed on different addresses across chains for this example],MultiChainRecovery:[{name:"recoveries",type:"ChainRecovery[]"},{name:"nonce",type:"uint256"}],ChainRecovery:[{name:"domain",type:"EIP712ChainDomain"},{name:"newOwner",type:"address"}],EIP712ChainDomain:[{name:"chainId",type:"uint256"},{name:"verifyingContract",type:"address"}]},primaryType:"MultiChainRecovery",domain:{name:"CrossChainSocialRecovery",version:"1"},message:{recoveries:[{domain:{chainId:1,verifyingContract:"0x123321..."// Recovery module on Ethereum},newOwner:"0x9999999999999999999999999999999999999999"},{domain:{chainId:137,verifyingContract:"0x321321..."// Recovery module on Polygon},newOwner:"0x9999999999999999999999999999999999999999"},{domain:{chainId:42161,verifyingContract:"0x432432..."// Recovery module on Arbitrum},newOwner:"0x9999999999999999999999999999999999999999"}],nonce:1}}
This signature enables guardians to schedule a recovery operation across all chains. The process includes:
Guardian Signatures: Multiple guardians sign the same cross-chain recovery message (3-of-5 threshold)
Schedule Phase: Once sufficient guardians sign, the recovery is scheduled on all networks with a delay
Security Window: Delay period where malicious recovery attempts can be detected and canceled
Execution Phase: After the delay, the recovery replaces the account’s owner
Cross-Chain Consistency: The same recovery operation is scheduled simultaneously on Ethereum, Polygon, and Arbitrum
Security Considerations
Cross-Chain Replay
This ERC intentionally enables replay across chains, the same signature is designed to be used on multiple chains. The verifyingContract field (set to the user’s account address) binds the signature to a specific account, preventing unauthorized use. However, applications MUST implement their own replay protection mechanisms:
Nonces: Include chain-specific or global nonces in the message
Deadlines: Include expiration timestamps
Execution Tracking: Mark operations as executed to prevent re-execution
Account Validation
Applications verifying signatures SHOULD check that the signing account exists on the current chain. An account that exists on Ethereum but not Polygon should not have signatures accepted on Polygon. For counterfactual accounts that have not been deployed yet, applications should follow ERC-6492 to validate signatures.
Code and State Differences
Contract code and state may differ across chains at the same address. Signatures that pass isValidSignature() on one chain may fail on another due to:
Different Implementations: The account contract may have different code on different chains
State Divergence: Nonces, signers, or other state may differ
Chain-Specific Logic: Some accounts may implement chain-specific validation rules
Applications MUST handle signature validation failures gracefully and should not assume uniform behavior across chains.
Partial Execution Risk
Cross-chain signatures do not guarantee atomic execution. A signature may be:
Successfully executed on some chains but not others
Front-run or censored on specific chains
Invalid on some chains due to state differences
This can result in:
Incomplete Intents: User operations partially fulfilled
Locked Funds: Assets stuck on intermediate chains
Loss of Value: MEV or unfavorable execution
Applications SHOULD implement:
Refund Mechanisms: Allow users to recover from failed partial executions
Execution Coordination: Use solvers or bridges that ensure atomic settlement
Monitoring: Track execution status across all target chains
Message Array Integrity
The message array defines operations for all chains. Applications MUST:
Validate Array Position: Verify the provided message hash appears at the expected index
Verify Chain ID: Confirm the message’s chainId matches the current chain
Prevent Substitution: Ensure the full message data matches the hash
Failing to validate these can allow:
Execution of operations intended for other chains
Replay of operations across chains in unintended order
Substitution attacks where an attacker provides wrong message data
Signature Expiration
Signatures without explicit expiration remain valid indefinitely. If an operation is:
Not executed on some chains
Delayed due to network congestion
Temporarily invalid due to state changes
The signature can be executed later, potentially with unexpected consequences. Applications MUST include deadline or validUntil fields in messages to prevent stale signature execution.
Wallet Display Considerations
Since this ERC relies on standard EIP-712 wallet display, users depend on their wallet to correctly show all cross-chain operations. Users should:
Review All Operations: Check every element in the message array
Verify Chain IDs: Ensure operations target the expected chains
Check Amounts: Verify asset amounts and addresses on each chain
Understand Atomicity: Know that operations may execute independently
Wallet developers SHOULD enhance displays to highlight cross-chain nature and show warnings about partial execution risks.