Reproduced Exploit
Qubit Finance QBridge Exploit — Zero-Address Token Whitelist Bypass
QBridgeHandler.deposit (QBridgeHandler.sol:122-137) looks up the token for a resourceID:
Loss
~$80M (the largest Qubit incident; bridge minted unbacked assets on BSC)
Chain
Ethereum
Category
Logic / State
Date
Jan 2022
Source & credit. Exploit reproduction, trace data, and analysis adapted from DeFiHackLabs by SunWeb3Sec — an open registry of reproduced on-chain exploits. Standalone Foundry PoC and full write-up: 2022-01-Qubit_exp in the
evm-hack-registrymirror. Upstream DeFiHackLabs PoC:src/test/…/Qubit_exp.sol.
Vulnerability classes: vuln/bridge/missing-validation · vuln/access-control/broken-logic
Reproduction: the PoC compiles & runs in an isolated Foundry project at this project folder. Full verbose trace: output.txt. Verified vulnerable source: QBridgeHandler.sol.
Key info#
| Loss | ~$80M (the largest Qubit incident; bridge minted unbacked assets on BSC) |
| Vulnerable contract | QBridgeHandler (impl 0x80D148…a629b, behind proxy 0x17B716…f9526) |
| Bridge | QBridge (impl 0x99309d…7cc3, behind proxy 0x20E5E3…7bce6) — 0x20E5E35ba29dC3B540a1aee781D0814D5c77Bce6 |
| Attacker EOA | 0xD01Ae1A708614948B2B5e0B7AB5be6AFA01325c7 |
| Attack tx | 0xc6f4dde74fdbf9f907ca7ba5f4f5e83a2ad45d3e1f9f76e5c8e2a16a4c8b2f8a |
| Chain / block / date | Ethereum mainnet / 14,090,169 / Jan 27, 2022 |
| Bug class | Uninitialized/default mapping abuse — unknown resourceID → tokenAddress = address(0), which is wrongly whitelisted (contractWhitelist[0]==true), so a deposit records success without moving any token, letting the bridge mint unbacked funds on the destination chain. |
TL;DR#
QBridgeHandler.deposit (QBridgeHandler.sol:122-137)
looks up the token for a resourceID:
address tokenAddress = resourceIDToTokenContractAddress[resourceID]; // 0x0 for unknown resourceID
require(contractWhitelist[tokenAddress], "provided tokenAddress is not whitelisted"); // passes! 0x0 whitelisted
...
tokenAddress.safeTransferFrom(depositer, address(this), amount); // calls 0x0.transferFrom → no-op [Stop]
Two independent defects compose into a critical bug:
contractWhitelist[address(0)]wastrue. A whitelist mapping should default tofalsefor every key, but the deployment left the zero address whitelisted (a sentinel/default bug). The PoC confirms it:is 0 address whitelisted = 1.- Unknown
resourceIDmaps toaddress(0).resourceIDToTokenContractAddress[unknown]returns0x0. There is norequire(tokenAddress != address(0)).
Together, an attacker submits a deposit for an unregistered resourceID. The handler resolves
tokenAddress = 0x0, passes the whitelist check (0x0 is whitelisted), then calls
safeTransferFrom on address(0). The low-level call to a non-contract returns success with no data,
which OpenZeppelin's safeTransferFrom accepts — so no token is actually taken from the depositor.
The bridge nonetheless emits Deposit and increments its deposit nonce; the relayers then honour that
deposit on BSC and mint real tokens to the attacker. Net cost to the attacker: zero on-chain value.
Cost to the protocol: ~$80M of unbacked mint.
Root cause#
A sentinel / default-value bug plus a missing zero-address guard.
The whitelist is the handler's only trust gate before it authorises a cross-chain mint. By letting
address(0) through:
- Any
resourceIDnot in the mapping becomes a "valid, whitelisted token address." safeTransferFrom(0x0, …)is a call to an EOA — it returns(success=true, data=""), which the OZ helper treats as a successful ERC20 transfer.
So the deposit accounting proceeds as if real tokens were locked, when none were. The attacker crafted
a fresh resourceID (…2f422fe9…01), decoded data as option=105, amount=190, and the deposit
recorded param2: 86 (the nonce). The relayer bridge then minted on BSC against that record.
Why the trace proves the bug#
contractAddress = 0x0000…0000
is 0 address whitelisted = 1
QBridge.deposit(1, resourceID, data)
→ QBridgeHandler.deposit(resourceID, attacker, data) [delegatecall chain]
→ 0x0.transferFrom(attacker, handler, 190e18) → [Stop] (no revert, no state change)
→ emit Deposit(1, resourceID, 86, attacker, data)
The deposit counter moves 85 → 86 — the bridge has now recorded a legitimate-looking deposit that the
destination chain will honour.
Preconditions#
- None beyond being able to call
QBridge.deposit(domainID, resourceID, data)with an unregisteredresourceIDand arbitraryamount. No token, no allowance, no capital required.
Diagrams#
Remediation#
- Never whitelist
address(0)— and add an explicitrequire(tokenAddress != address(0), "invalid resourceID")before the whitelist check. This alone kills the bug. - Validate
resourceIDis registered. Use a separateisValidResourceID(resourceID)check rather than relying on the token mapping to implicitly validate. - Make
safeTransferFromfail on non-contracts. OZ'sAddress.isContractreturns false for0x0, but the older helper used here treated empty-return success as valid — confirm the helper reverts on non-contract targets. - Two-key cross-check: require that
tokenContractAddressToResourceID[tokenAddress] == resourceID(round-trip) so a stray/zero token can never pair with an arbitrary resourceID. - Cap and time-delay large mints on the destination so an attacker cannot extract the full amount before the off-chain relayers notice the anomaly.
How to reproduce#
_shared/run_poc.sh 2022-01-Qubit_exp --mt testExploit -vvvvv
- RPC: mainnet archive (block 14,090,169). Infura mainnet in
foundry.toml. - Result:
[PASS] testExploit()— logscontractAddress = 0x0,is 0 address whitelisted = 1, and theDepositevent fires with nonce 86.
Reference: Qubit Finance QBridge zero-address whitelist bypass, Jan 27 2022 (~$80M).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 2022-01-Qubit_exp (evm-hack-registry mirror).
- Upstream DeFiHackLabs PoC:
Qubit_exp.sol.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Qubit Finance QBridge Exploit".
- Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.