qubit
<aside> 💡 Meter bridge
</aside>
参考链接:
错误原因:
产生错误的根本原因是: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:
然后构造上述的一个交易数据即可。
<aside> 💡 Qubit
</aside>
参考链接: 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
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);
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!