本文作者:bixia1994[1]
💡 Meter bridge
交易 hash[2]
参考链接:
chainbridge-solidity-v1.0.0-eth/deployed_0421/merged at master · meterio/chainbridge-solidity-v1.0.0-eth[3]
Breaking down the Meter hack[4]
错误原因:
产生错误的根本原因是: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
//destinationChainID
//resourceID
//offset
//len
//amount
//addr len 0x14=20
//receipient addr
首先在 Handler 中(0xde4fC7C3C5E7bE3F16506FcC790a8D93f8Ca0b40),根据 wtokenAddress 查找到对应的 resouceID:
然后构造上述的一个交易数据即可。
💡 Qubit
参考链接:https://twitter.com/peckshield/status/1486841239450255362[5]
tx[6]
tx2[7]
错误原因:
用户 →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[8]
攻击者的调用参数为:
Function: deposit(uint8 destinationDomainID, bytes32 resourceID, bytes data)
MethodID: 0x05e2ca17
//destination
//resource
//offset
//len
//option
//amount
//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]bixia1994: https://learnblockchain.cn/people/3295
[2]交易hash: https://etherscan.io/tx/0x2d3987963b77159cfe4f820532d729b0364c7f05511f23547765c75b110b629c
[3]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
[4]Breaking down the Meter hack: https://medium.com/chainsafe-systems/breaking-down-the-meter-io-hack-a46a389e7ae4
[5]https://twitter.com/peckshield/status/1486841239450255362: https://twitter.com/peckshield/status/1486841239450255362
[6]tx: https://etherscan.io/address/0x80d1486ef600cc56d4df9ed33baf53c60d5a629b#code
[7]tx2: https://etherscan.io/address/0x99309d2e7265528dc7c3067004cc4a90d37b7cc3#code
[8]https://etherscan.io/tx/0x3dfa33b5c6150bf3d64f49cb97eba351f99e4dff7119ef458e40f51160bf77ec/advanced: https://etherscan.io/tx/0x3dfa33b5c6150bf3d64f49cb97eba351f99e4dff7119ef458e40f51160bf77ec/advanced