QBridge shipped a hand-rolled copy of safeTransferFrom built on a raw call, dropping the one line OpenZeppelin includes: the check that the target address actually contains code. Call it with tokenAddress = 0x0 and the EVM does what it always does with a call to an empty account — it succeeds and returns nothing. Zero tokens moved. The Deposit event fired anyway, the Ethereum side saw it, and minted the attacker 77,162 qXETH against a deposit that never happened.
// ❌ QBridge's own version function _callOptionalReturn(IERC20 token, bytes memory data) private { (bool success, bytes memory returndata) = address(token).call(data); require(success, "SafeERC20: low-level call failed"); if (returndata.length > 0) { require(abi.decode(returndata, (bool)), "SafeERC20: operation did not succeed"); } // call() to an address with NO CODE returns (true, ""). // success == true. returndata.length == 0. Both requires pass. } // ✅ OpenZeppelin's version function _callOptionalReturn(IERC20 token, bytes memory data) private { bytes memory returndata = address(token).functionCall(data, "..."); // functionCall() → require(isContract(target), "Address: call to non-contract") ... } deposit(tokenAddress = 0x0000…0000, amount = 77162e18, msg.value = 0) → result: 77,162 qXETH minted on BSC against nothing at all
address parameter that reaches a low-level call. Zero-address checks and isContract checks are the cheapest lines in Solidity, and they are the ones people delete to save gas.Entries in the SAFE database that describe this failure. These share its failure class.
Every figure on this page comes from the post-mortems above, not from us. Losses are US dollars at the time of the incident.
Input validation is one of the 203 classes the SaferICO scanner checks for. It will not review your signing process — but it will read your Solidity.