Skip to content
#17 largest Input validation 2022

The Qubit Finance hack — $80M lost

Loss$80M
Date28 Jan 2022
ChainBNB Chain ↔ Ethereum
Failure classInput validation
In assetsdeposited exactly zero
TargetQBridge
1

What happened

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.

2

How the attack ran

  1. Call deposit() with token 0x0And with msg.value = 0
  2. Hand-rolled safeTransferFrom runsA raw call, with no isContract check
  3. A call to an empty account succeedsIt returns (true, "") — both requires pass
  4. The Deposit event fires anyway77,162 qXETH minted against nothing — $80M
3

The code

the one line that was removed
// ❌ 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
4

What would have caught it

What an audit looks for: every re-implementation of a standard library, and every 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.
6

Sources

Every figure on this page comes from the post-mortems above, not from us. Losses are US dollars at the time of the incident.

Check your own contract for this

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.

Run the scanner See how it is attacked Read the docs