# Meter Bridge && Qubit Bridge **Published by:** [bugWriter](https://paragraph.com/@bugwriter/) **Published on:** 2022-02-18 **URL:** https://paragraph.com/@bugwriter/meter-bridge-qubit-bridge ## Content 交易hash: 参考链接: https://twitter.com/peckshield/status/1490121762847092736?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1490121762847092736%7Ctwgr%5E%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fcdn.embedly.com%2Fwidgets%2Fmedia.html%3Ftype%3Dtext2Fhtmlkey%3Dd04bfffea46d4aeda930ec88cc64b87cschema%3Dtwitterurl%3Dhttps3A%2F%2Ftwitter.com%2Fpeckshield%2Fstatus%2F1490121762847092736image%3Dhttps3A%2F%2Fi.embed.ly%2F1%2Fimage3Furl3Dhttps253A252F252Fabs.twimg.com252Ferrors252Flogo46x38.png26key3D4fce0568f2ce49e8b54624ef71a8a5bd chainbridge-solidity-v1.0.0-eth/deployed_0421/merged at master · meterio/chainbridge-solidity-v1.0.0-eth Breaking down the Meter hack 错误原因: 产生错误的根本原因是:meter中针对deposit和depositETH,emit了相同的事件。但是在depositETH中,将ETH 包装成WETH后马上转给了handler,导致与deposit方法里对于ERC20的处理方式不一致,从而使得handler里面针对depositETH进行特殊处理。 即:跨链桥的逻辑应该是 用户→ 桥 deposit → Handler: transferFrom(burn/lock) → emit Deposit 用户→ 桥 depositETH: transfer WETH→ Handler (do nothing) → emit Deposit ⇒ 用户 → 桥 deposit → Handler: do nothing 根本逻辑错误在于:handler里 if (tokenAddress != _wtokenAddress) 导致Handler: function deposit( bytes32 resourceID, uint8 destinationChainID, uint64 depositNonce, address depositer, bytes calldata data ) external override onlyBridge { bytes memory recipientAddress; uint256 amount; uint256 lenRecipientAddress; assembly { amount := calldataload(0xC4) recipientAddress := mload(0x40) lenRecipientAddress := calldataload(0xE4) mstore(0x40, add(0x20, add(recipientAddress, lenRecipientAddress))) calldatacopy( recipientAddress, // copy to destinationRecipientAddress 0xE4, // copy from calldata @ 0x104 sub(calldatasize(), 0xE) // copy size (calldatasize - 0x104) ) } address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; require(_contractWhitelist[tokenAddress], "provided tokenAddress is not whitelisted"); // ether case, the weth already in handler, do nothing if (tokenAddress != _wtokenAddress) { if (_burnList[tokenAddress]) { burnERC20(tokenAddress, depositer, amount); } else { lockERC20(tokenAddress, depositer, address(this), amount); } } _depositRecords[destinationChainID][depositNonce] = DepositRecord( tokenAddress, uint8(lenRecipientAddress), destinationChainID, resourceID, recipientAddress, depositer, amount ); } 当用户的传入的调用参数如下时:Function: deposit(uint8 destinationChainID, bytes32 resourceID, bytes data) MethodID: 0x05e2ca17 [0]: 0000000000000000000000000000000000000000000000000000000000000001 //destinationChainID [1]: 0000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc201 //resourceID [2]: 0000000000000000000000000000000000000000000000000000000000000060 //offset [3]: 0000000000000000000000000000000000000000000000000000000000000054 //len [4]: 000000000000000000000000000000000000000000000016e77c77f5de41f3a4 //amount [5]: 0000000000000000000000000000000000000000000000000000000000000014 //addr len 0x14=20 [6]: 8d3d13cac607b7297ff61a5e1e71072758af4d01000000000000000000000000 //receipient addr 首先在Handler中(0xde4fC7C3C5E7bE3F16506FcC790a8D93f8Ca0b40),根据wtokenAddress查找到对应的resouceID:resource ID然后构造上述的一个交易数据即可。 💡 Qubit 参考链接: https://twitter.com/peckshield/status/1486841239450255362 错误原因: 用户→Bridge: deposit (resourceID → ETH) → Handler: deposit (tokenAddress = 0) 当handler中,deposit tokenAddr=0, 其调用safeTransferFrom时,其会直接调用STOP,返回true,而不是revert或者false。 即:function safeTransfer( address token, address to, uint value ) internal { // bytes4(keccak256(bytes('transfer(address,uint256)'))); (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); require(success && (data.length == 0 || abi.decode(data, (bool))), "!safeTransfer"); } 当token不是一个合约地址时,比如一个EOA地址,其调用的call仍然会成功,返回success! Bridge: function deposit(uint8 destinationDomainID, bytes32 resourceID, bytes calldata data) external payable notPaused { require(msg.value == fee, "QBridge: invalid fee"); address handler = resourceIDToHandlerAddress[resourceID]; require(handler != address(0), "QBridge: invalid resourceID"); uint64 depositNonce = ++_depositCounts[destinationDomainID]; IQBridgeHandler(handler).deposit(resourceID, msg.sender, data); emit Deposit(destinationDomainID, resourceID, depositNonce, msg.sender, data); } function depositETH(uint8 destinationDomainID, bytes32 resourceID, bytes calldata data) external payable notPaused { uint option; uint amount; (option, amount) = abi.decode(data, (uint, uint)); require(msg.value == amount.add(fee), "QBridge: invalid fee"); address handler = resourceIDToHandlerAddress[resourceID]; require(handler != address(0), "QBridge: invalid resourceID"); uint64 depositNonce = ++_depositCounts[destinationDomainID]; IQBridgeHandler(handler).depositETH{value:amount}(resourceID, msg.sender, data); emit Deposit(destinationDomainID, resourceID, depositNonce, msg.sender, data); } Handler: function deposit(bytes32 resourceID, address depositer, bytes calldata data) external override onlyBridge { uint option; uint amount; (option, amount) = abi.decode(data, (uint, uint)); address tokenAddress = resourceIDToTokenContractAddress[resourceID]; require(contractWhitelist[tokenAddress], "provided tokenAddress is not whitelisted"); if (burnList[tokenAddress]) { require(amount >= withdrawalFees[resourceID], "less than withdrawal fee"); QBridgeToken(tokenAddress).burnFrom(depositer, amount); } else { require(amount >= minAmounts[resourceID][option], "less than minimum amount"); tokenAddress.safeTransferFrom(depositer, address(this), amount); } } function depositETH(bytes32 resourceID, address depositer, bytes calldata data) external payable override onlyBridge { uint option; uint amount; (option, amount) = abi.decode(data, (uint, uint)); require(amount == msg.value); address tokenAddress = resourceIDToTokenContractAddress[resourceID]; require(contractWhitelist[tokenAddress], "provided tokenAddress is not whitelisted"); require(amount >= minAmounts[resourceID][option], "less than minimum amount"); } https://etherscan.io/tx/0x3dfa33b5c6150bf3d64f49cb97eba351f99e4dff7119ef458e40f51160bf77ec/advanced 攻击者的调用参数为:Function: deposit(uint8 destinationDomainID, bytes32 resourceID, bytes data) MethodID: 0x05e2ca17 [0]: 0000000000000000000000000000000000000000000000000000000000000001 //destination [1]: 00000000000000000000002f422fe9ea622049d6f73f81a906b9b8cff03b7f01 //resource [2]: 0000000000000000000000000000000000000000000000000000000000000060 //offset [3]: 0000000000000000000000000000000000000000000000000000000000000060 //len [4]: 0000000000000000000000000000000000000000000000000000000000000069 //option [5]: 00000000000000000000000000000000000000000000021e0c0013070adc0000 //amount [6]: 000000000000000000000000d01ae1a708614948b2b5e0b7ab5be6afa01325c7 //receipient resource IDquibit被盗的根本原因其实在于: 他没有使用Openzeppelin的safeERC20合约,而是自己实现了一个版本的safeERC20. 但是在它自己实现的safeERC20合约里面的safeTransferFrom方法里有bug,没有检查token必须是合约地址,而不是EOA。 在Openzeppelin则做了相应的检查。function safeTransferFrom( address token, address from, address to, uint value ) internal { // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); require(success && (data.length == 0 || abi.decode(data, (bool))), "!safeTransferFrom"); } function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { require(address(this).balance >= value, "Address: insufficient balance for call"); require(isContract(target), "Address: call to non-contract"); // solhint-disable-next-line avoid-low-level-calls (bool success, bytes memory returndata) = target.call{ value: value }(data); return _verifyCallResult(success, returndata, errorMessage); } 进一步思考 ## Publication Information - [bugWriter](https://paragraph.com/@bugwriter/): Publication homepage - [All Posts](https://paragraph.com/@bugwriter/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@bugwriter): Subscribe to updates