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.
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
Entries in the SAFE database that describe this failure. These share its failure class.
Every figure on this page comes from the post-mortems above, not from us. Losses are US dollars at the time of the incident.
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.