Skip to content
#03 largest Access control 2021

The Poly Network hack — $611M lost

Loss$611M
Date10 Aug 2021
ChainEthereum + BNB Chain + Polygon
Failure classAccess control
In assetslater returned by the attacker
1

What happened

The cross-chain executor EthCrossChainManager was itself the owner of the contract that stored the bridge's trusted keeper keys. Its executor function let an incoming message name any target contract and any method by string. The attacker brute-forced a method name — f1121318093 — whose first four bytes of keccak collide exactly with putCurEpochConPubKeyBytes(bytes) = 0x41973cd9, then made the bridge call its own privileged storage contract and replace every keeper with his own key. After that he simply signed his own withdrawals.

2

How the attack ran

  1. A cross-chain message is craftedTarget contract and method name are both attacker-chosen
  2. A method name is brute-forcedf1121318093, picked for its keccak prefix
  3. Selector collision0x41973cd9 ≡ putCurEpochConPubKeyBytes, and the bridge owns that contract
  4. Keepers replacedThe attacker now signs the bridge’s own withdrawals
3

The code

EthCrossChainManager.sol — untrusted input picks the selector
function _executeCrossChainTx(
    address _toContract, bytes memory _method,
    bytes memory _args, bytes memory _fromContractAddr, uint64 _fromChainId
) internal returns (bool){
    require(isContract(_toContract), "...not a contract");
    (success, returnData) = _toContract.call(
        abi.encodePacked(
            bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))),
            abi.encode(_args, _fromContractAddr, _fromChainId)
        )
    );
    ...
}

// _method = "f1121318093"
// keccak256("f1121318093(bytes,bytes,uint64)")[0:4]  ==  0x41973cd9
// keccak256("putCurEpochConPubKeyBytes(bytes)")[0:4] ==  0x41973cd9   ← same selector
// and EthCrossChainManager is the OWNER of EthCrossChainData.
→ result: the attacker's key becomes the bridge's only keeper
4

What would have caught it

What an audit looks for: never let untrusted input choose which function gets called. A four-byte selector is not a namespace — collisions are cheap to brute-force. And a privileged contract must never be reachable from a generic execute path: put an explicit deny-list of privileged targets in the executor.
6

Sources

Every figure on this page comes from the post-mortems above, not from us. Losses are US dollars at the time of the incident.

Check your own contract for this

Access control is one of the 203 classes the SaferICO scanner checks for. It will not review your signing process — but it will read your Solidity.

Run the scanner See how it is attacked Read the docs