# Meter Bridge && Qubit Bridge

By [bugWriter](https://paragraph.com/@bugwriter) · 2022-02-18

---

交易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](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](https://github.com/meterio/chainbridge-solidity-v1.0.0-eth/tree/master/deployed_0421/merged)

[Breaking down the Meter hack](https://medium.com/chainsafe-systems/breaking-down-the-meter-io-hack-a46a389e7ae4)

错误原因：

产生错误的根本原因是：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](https://storage.googleapis.com/papyrus_images/b2bd9451d95db0b6c21b1b5888065cf041e92bf788144178b1b767c96aed4ddf.png)

resource ID

然后构造上述的一个交易数据即可。

💡 Qubit

参考链接：

[https://twitter.com/peckshield/status/1486841239450255362](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](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 ID](https://storage.googleapis.com/papyrus_images/4c0f82634d33eb04b8732d433081b77559543a78d72bb5d70b912ece57bf4a39.png)

resource ID

quibit被盗的根本原因其实在于：

他没有使用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);
        }
    

进一步思考

---

*Originally published on [bugWriter](https://paragraph.com/@bugwriter/meter-bridge-qubit-bridge)*
