Cobo Security Team: Hidden Risks and Arbitrage Opportunities in the ETH Hard Fork
TechFlow Selected TechFlow Selected
Cobo Security Team: Hidden Risks and Arbitrage Opportunities in the ETH Hard Fork
This article will use these two incidents as case studies to analyze the impact of replay attacks on forked chains and how protocols should defend against such attacks.
This article is contributed by the Cobo Blockchain Security Team. The team members come from well-known blockchain security firms and have extensive experience in smart contract audits, having discovered high-severity vulnerabilities in multiple DeFi projects. The team currently focuses on smart contract security and DeFi security, researching and sharing cutting-edge blockchain security technologies.
We also welcome lifelong learners with a research-oriented mindset and scientific methodology in the field of cryptocurrency to join us and contribute insights and research perspectives to the industry!
This is the 16th article published by Cobo Global.
Introduction
With ETH's upgrade to the PoS consensus system, the original PoW-based ETH chain successfully hard-forked under support from certain community factions (hereinafter referred to as ETHW). However, because some on-chain protocols were not designed with potential hard forks in mind, corresponding protocols on the ETHW fork chain face certain security risks—among which replay attacks pose the most serious threat.
After the hard fork was completed, at least two replay attacks occurred on the ETHW mainnet: one targeting OmniBridge and another targeting Polygon Bridge. This article analyzes these two incidents as case studies, examining how replay attacks impact forked chains and how protocols should defend against such threats.
Types of Replay Attacks
First, before diving into analysis, we need a basic understanding of replay attack types. Generally speaking, there are two categories: transaction replay and signed message replay. Below, we explain the differences between these two mechanisms.
Transaction Replay
Transaction replay refers to copying a transaction from the original chain directly onto the target chain—an operation that occurs at the transaction level. After replaying, the transaction can still execute normally and pass validation. The most famous example is the Wintermute attack on Optimism, which led directly to the loss of over 20 million OP tokens. However, after EIP-155 was implemented, transactions include a chainId parameter (a unique identifier distinguishing one chain from its forks) within their signatures. As a result, if the target chain has a different chainId, the replayed transaction will fail validation and cannot be executed.
Signed Message Replay
Unlike transaction replay, signed message replay targets messages signed with a private key (e.g., "Cobo is the best"). In this type of attack, the attacker does not replay an entire transaction but instead reuses only the signed message. For instance, consider the message "Cobo is the best"—since it contains no chain-specific parameters, the signature would theoretically remain valid across any forked chain and could pass verification. To prevent such cross-chain reuse, developers can embed the chainId into the message content, for example: "Cobo is the best + chainId()". By including a chain-specific identifier, the signed message becomes unique to each chain, preventing direct replay across forks.
Attack Mechanisms Behind OmniBridge and Polygon Bridge
Next, we analyze the attack principles behind OmniBridge and Polygon Bridge. First, let’s state the conclusion: neither incident involved transaction replay attacks. This is because ETHW uses a different chainId than the Ethereum mainnet, making direct transaction replay impossible due to failed signature validation. That leaves only message replay as a viable vector. Let’s now examine how each bridge was exploited via message replay on the ETHW fork chain.
OmniBridge
OmniBridge is a cross-chain bridge used for transferring assets between xDAI and the Ethereum mainnet, relying primarily on designated validators to submit cross-chain messages for asset transfers. In OmniBridge, the logic for validator-submitted verification messages is as follows:
function executeSignatures(bytes _data, bytes _signatures) public { _allowMessageExecution(_data, _signatures); bytes32 msgId; address sender; address executor; uint32 gasLimit; uint8 dataType; uint256[2] memory chainIds; bytes memory data; (msgId, sender, executor, gasLimit, dataType, chainIds, data) = ArbitraryMessage.unpackData(_data); _executeMessage(msgId, sender, executor, gasLimit, dataType, chainIds, data); }
In this function, line #L2 first checks whether the submitted signature was generated by an authorized validator. Then, on line #L11, the data message is decoded. Notably, the decoded output includes a chainId field—does this mean signed message replay is prevented? Let’s continue analyzing.
function _executeMessage( bytes32 msgId, address sender, address executor, uint32 gasLimit, uint8 dataType, uint256[2] memory chainIds, bytes memory data ) internal { require(_isMessageVersionValid(msgId)); require(_isDestinationChainIdValid(chainIds[1])); require(!relayedMessages(msgId)); setRelayedMessages(msgId, true); processMessage(sender, executor, msgId, gasLimit, dataType, chainIds[0], data); }
Tracing into the _executeMessage function reveals that on line #L11, a validity check is performed on chainId:
function _isDestinationChainIdValid(uint256 _chainId) internal returns (bool res) { return _chainId == sourceChainId(); } function sourceChainId() public view returns (uint256) { return uintStorage[SOURCE_CHAIN_ID]; }
Further analysis shows that this chainId check does not use the EVM's native chainid opcode to retrieve the current chain's ID. Instead, it relies on a value stored in the uintStorage variable—an administrator-configurable setting. Therefore, the message itself does not inherently contain a cryptographic binding to the chain, meaning it remains vulnerable to signed message replay in theory.
During the hard fork, all pre-fork states are preserved identically on both chains. Unless the xDAI team takes additional action afterward, the OmniBridge contract state—including the set of validators—remains unchanged on both ETHW and the Ethereum mainnet post-fork. Given this, validator signatures made on the mainnet can also be validated on ETHW. Since the signed messages do not include chainId, attackers can exploit this by replaying validator signatures to withdraw assets from the same contract on ETHW.
Polygon Bridge
Like OmniBridge, Polygon Bridge facilitates asset transfers between Polygon and the Ethereum mainnet. However, unlike OmniBridge, Polygon Bridge relies on block proofs for withdrawals. Its logic is shown below:
function exit(bytes calldata inputData) external override { //...omitted non-critical logic // verify receipt inclusion require( MerklePatriciaProof.verify( receipt.toBytes(), branchMaskBytes, payload.getReceiptProof(), payload.getReceiptRoot() ), "RootChainManager: INVALID_PROOF" ); // verify checkpoint inclusion _checkBlockMembershipInCheckpoint( payload.getBlockNumber(), payload.getBlockTime(), payload.getTxRoot(), payload.getReceiptRoot(), payload.getHeaderNumber(), payload.getBlockProof() ); ITokenPredicate(predicateAddress).exitTokens( _msgSender(), rootToken, log.toRlpBytes() ); }
From the function logic, we see the contract performs two checks to validate the message: verifying the transactionRoot and BlockNumber to confirm the transaction genuinely occurred on the child chain (Polygon Chain). The first check can be bypassed since anyone can construct their own transactionRoot from transaction data. However, the second check cannot be easily circumvented. Examining the _checkBlockMembershipInCheckpoint function reveals:
function _checkBlockMembershipInCheckpoint( uint256 blockNumber, uint256 blockTime, bytes32 txRoot, bytes32 receiptRoot, uint256 headerNumber, bytes memory blockProof ) private view returns (uint256) { ( bytes32 headerRoot, uint256 startBlock, , uint256 createdAt, ) = _checkpointManager.headerBlocks(headerNumber); require( keccak256( abi.encodePacked(blockNumber, blockTime, txRoot, receiptRoot) ) .checkMembership( blockNumber.sub(startBlock), headerRoot, blockProof ), "RootChainManager: INVALID_HEADER" ); return createdAt; }
The headerRoot is retrieved from the _checkpointManager contract. Following this logic, we examine where _checkpointManager sets the headerRoot:
function submitCheckpoint(bytes calldata data, uint[3][] calldata sigs) external { (address proposer, uint256 start, uint256 end, bytes32 rootHash, bytes32 accountHash, uint256 _borChainID) = abi .decode(data, (address, uint256, uint256, bytes32, bytes32, uint256)); require(CHAINID == _borChainID, "Invalid bor chain id"); require(_buildHeaderBlock(proposer, start, end, rootHash), "INCORRECT_HEADER_DATA"); // check if it is better to keep it in local storage instead IStakeManager stakeManager = IStakeManager(registry.getStakeManagerAddress()); uint256 _reward = stakeManager.checkSignatures( end.sub(start).add(1), /** prefix 01 to data 01 represents positive vote on data and 00 is negative vote malicious validator can try to send 2/3 on negative vote so 01 is appended */ keccak256(abi.encodePacked(bytes(hex"01"), data)), accountHash, proposer, sigs ); //....remaining logic omitted
As seen in line #L2, the signature data only verifies borChainId, without checking the actual chainId of the chain itself. Since this message is signed by a contract-designated proposer, attackers can theoretically replay the proposer’s signed message on the forked chain to submit a valid headerRoot. Subsequently, they can call the exit function on Polygon Bridge on the ETHW chain and provide the corresponding Merkle proof of the transaction to successfully withdraw funds while passing the headerRoot validation.
Take address 0x7dbf18f679fa07d943613193e347ca72ef4642b9 as an example. This address successfully profited from the ETHW chain through the following steps:
-
First, deposit funds on the mainnet using centralized exchange withdrawal capabilities.
-
Deposit funds on the Polygon chain via Polygon Bridge’s depositFor function;
-
Withdraw funds on the Ethereum mainnet by calling Polygon Bridge’s exit function;
-
Copy the headerRoot submitted by the proposer on the Ethereum mainnet;
-
Replay the proposer's signed message extracted in the previous step on ETHW;
-
Call exit on Polygon Bridge on ETHW to withdraw funds.
Why Did This Happen?
From the two examples analyzed above, it is clear that both protocols suffered replay attacks on ETHW due to insufficient replay protection mechanisms, allowing their assets to be drained on the forked chain. However, since neither bridge officially supports the ETHW fork chain, users did not suffer any direct losses. But why didn’t these bridges implement replay protection during design? The reason is simple: both OmniBridge and Polygon Bridge were designed for highly specific use cases—transferring assets solely between their designated chains—with no plans for multi-chain deployment. Hence, lacking replay protection had no immediate security implications for the protocol under normal usage scenarios.
Conversely, for users on ETHW, given that these bridges don't support multi-chain operations, performing actions on the ETHW fork may expose them to message replay attacks on the Ethereum mainnet.
Take UniswapV2 as an example. Currently, the UniswapV2 pool contract includes a permit function containing the variable PERMIT_TYPEHASH, which incorporates DOMAIN_SEPARATOR.
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { require(deadline >= block.timestamp, 'UniswapV2: EXPIRED'); bytes32 digest = keccak256( abi.encodePacked( '\x19\x01', DOMAIN_SEPARATOR, keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) ) ); address recoveredAddress = ecrecover(digest, v, r, s); require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE'); _approve(owner, spender, value); }
This variable was first defined in EIP-712 and includes chainId, explicitly designed from the outset to prevent replay across chains. However, according to the UniswapV2 pool contract logic:
constructor() public { uint chainId; assembly { chainId := chainid } DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), keccak256(bytes(name)), keccak256(bytes('1')), chainId, address(this) ) ); }
The DOMAIN_SEPARATOR is fixed during contract construction. This means that even after a hard fork changes the actual chainId, the pool contract cannot update the DOMAIN_SEPARATOR accordingly. If users later perform authorizations on ETHW, those permit signatures could be replayed back onto the Ethereum mainnet. Beyond Uniswap, many similar protocols exist—such as certain versions of Yearn vault contracts—that also use a static DOMAIN_SEPARATOR. Users must therefore remain cautious about replay risks when interacting with such protocols on ETHW.
Preventive Measures During Protocol Design
For developers designing message-signing mechanisms, future multi-chain scenarios should be considered. If there is any possibility of multi-chain deployment in the roadmap, chainId should be included as a dynamic variable within the signed message. Additionally, during signature verification, since hard forks preserve all pre-fork states unchanged, the chainId used for validating signed messages should not be stored as a contract variable. Instead, it should be freshly retrieved before each verification to ensure security.
Impact
Impact on Users
Ordinary users should avoid performing any operations on unsupported forked chains, to prevent associated signed messages from being replayed on the main chain and causing asset losses on the primary network.
Impact on Exchanges and Custodians
Since many exchanges support ETHW tokens, the tokens withdrawn via such attacks may end up being deposited and sold on exchanges. However, it should be noted that these attacks do not stem from flaws in the chain’s consensus mechanism or involve malicious minting. Therefore, exchanges generally do not require special countermeasures against such events.
Conclusion
As multi-chain ecosystems evolve, replay attacks are transitioning from theoretical concerns to mainstream exploitation vectors. Developers must carefully evaluate protocol designs and incorporate factors like chainId into message signatures wherever possible, adhering to established best practices to prevent user asset losses.
Cobo is the largest cryptocurrency custodian in the Asia-Pacific region. Since its inception, Cobo has provided exceptional service to over 500 leading industry institutions and high-net-worth individuals, ensuring secure storage of digital assets while delivering consistent yield generation. Trusted globally, Cobo focuses on building scalable infrastructure, offering institutions comprehensive solutions including secure custody, asset growth, on-chain interactions, and cross-chain/cross-layer capabilities—empowering organizations in their transition to Web 3.0. Cobo's business units include Cobo Custody, Cobo DaaS, Cobo MaaS, Cobo StaaS, Cobo Ventures, and Cobo DeFi Yield Fund, meeting diverse client needs.
Join TechFlow official community to stay tuned
Telegram:https://t.me/TechFlowDaily
X (Twitter):https://x.com/TechFlowPost
X (Twitter) EN:https://x.com/BlockFlow_News














