Meter Bridge && Qubit Bridge

  • bixia1994
  • 更新于 2022-02-18 11:25
  • 阅读 3078

qubit

Meter Bridge && Qubit Bridge

<aside> 💡 Meter bridge

</aside>

交易hash

参考链接:

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: 20220218112240.png

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

<aside> 💡 Qubit

</aside>

参考链接: https://twitter.com/peckshield/status/1486841239450255362

tx

tx2

错误原因:

用户→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!

20220218112324.png

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

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);
    }
点赞 1
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

4 条评论

请先 登录 后评论
bixia1994
bixia1994
0x92Fb...C666
learn to code