Qubit Finance QBridge Zero-Value Deposit Event Spoof / Unbacked qXETH Mint (2022-01-27)

  • Name: Qubit Finance QBridge zero-value deposit event spoof / unbacked qXETH mint
  • Date: 2022-01-27
  • Protocol: Qubit Finance / QBridge
  • Chain: Ethereum + BNB Chain
  • Category: bridge verification failure / event-forgery / blind-relayer trust / unbacked wrapped-asset mint
  • Loss: about 206,809 BNB, roughly $80M, after the attacker minted 77,162 qXETH that the protocol treated as valid collateral
  • Summary: Qubit’s Ethereum↔BNB bridge let the attacker mint bridge-backed collateral on BNB Chain without locking any ETH on Ethereum because the bridge no longer kept who may author ETH deposit truth tied to who actually received ETH. The attacker called the generic deposit() path on Ethereum with the ETH resourceID, sent 0 ETH, and supplied crafted calldata that still passed the handler checks. Qubit’s own incident report later said this generic deposit() function was a path that should no longer have been used once depositETH() was introduced, but it remained reachable and still emitted the same relay-authoritative Deposit event surface. That worked because an owner-controlled resource mapping had been changed to address(0), the zero address was whitelisted for ETH handling, and safeTransferFrom() against an EOA / zero-address target did not revert. The contract therefore emitted the same Deposit event used for real ETH bridge deposits even though no asset transfer occurred. Qubit’s relayer treated that event as proof of locked value, called voteProposal() on BNB Chain, minted qXETH, and the attacker borrowed out the rest of the lending market against that counterfeit collateral.
  • Trigger / entry point:
    • QBridge exposed both depositETH() for native ETH and deposit() for ERC-20-style deposits.
    • Both paths emitted the same Deposit event shape that the offchain relayer used as its bridge trigger.
    • For the ETH resourceID, the handler-side token mapping had been changed to address(0) through an owner-only setResource(...) call.
    • The attacker invoked the generic deposit() function instead of depositETH(), attached no ETH, and supplied data that encoded a large nominal ETH amount anyway.
  • Root cause:
    • Qubit’s authenticity boundary was not a cryptographic proof or signer quorum; it was the relayer’s interpretation of a Deposit event.
    • The bridge no longer enforced that only the canonical ETH custody path could author that event. Once depositETH() became the real ETH intake lane, the older generic deposit() path should have lost authority to produce ETH-looking deposit truth or moved onto a meaningfully different event surface.
    • The shared event schema therefore did not distinguish real ETH was locked through depositETH() from generic deposit() emitted the same log with zero value.
    • The bridge handler also treated the zero address as a valid ETH placeholder and then relied on safeTransferFrom() semantics that silently succeeded against a no-code address instead of proving that value actually moved.
    • An owner-controlled, untimelocked resource remap turned what had previously pointed at WETH into address(0), so the bridge’s proof-of-lock assumption drifted away from actual asset movement without any matching relayer-side change in what counted as authentic ETH lock evidence.
  • Exploit path:
    1. On 2021-12-13, an owner-only setResource(handlerAddress, resourceID, tokenAddress) transaction remapped the ETH resourceID from a real token address to address(0) on the Ethereum-side bridge stack.
    2. On 2022-01-27, the attacker called Ethereum-side QBridge.deposit(uint8 destinationDomainID, bytes32 resourceID, bytes data) multiple times using the ETH resourceID but with msg.value == 0. The later technical analysis cataloged a cluster of these zero-value source-side calls across Ethereum blocks 14090201, 14090210, 14090216, 14090223, 14090230 (twice), and 14090234, which matters because it shows the bridge-truth forgery was a repeatable source-side mint loop rather than one odd relayer-side anomaly.
    3. Inside the handler, tokenAddress = _resourceIDToTokenContractAddress[resourceID] resolved to address(0).
    4. The zero address still passed the whitelist logic because it doubled as Qubit’s ETH placeholder, and the attacker-picked amount passed the minimum-amount checks.
    5. The handler then executed ERC20Interface(tokenAddress).safeTransferFrom(depositer, address(this), amount) against address(0). Because there was no contract code there, the low-level call returned success instead of proving a real transfer.
    6. The generic deposit() path completed and emitted the same Deposit event that a legitimate ETH bridge deposit would have emitted.
    7. Qubit’s relayer watched the event stream, accepted the forged deposit as genuine, and submitted voteProposal() transactions on BNB Chain.
    8. Those relayer calls minted qXETH / xETH representations on BNB Chain even though no ETH had been locked on Ethereum.
    9. The attacker repeated the flow across 16 deposit / relayer rounds, accumulated 77,162 qXETH as apparently valid collateral, then borrowed WETH, BTC-B, stablecoins, CAKE, BUNNY, and MDX before consolidating much of the haul into BNB.
  • What the attacker needed to be true:
    • The relayer had to treat a Deposit event as sufficient proof that value had been locked, without re-verifying call path, msg.value, or actual token balance changes.
    • deposit() and depositETH() had to share an indistinguishable event surface.
    • The ETH resourceID had to resolve to address(0) and the zero address had to remain whitelisted.
    • The handler’s transfer logic had to accept silent success from an EOA / zero-address call instead of requiring code existence or balance delta confirmation.
    • qXETH minted by the bridge had to be accepted by Qubit’s money market as normal collateral.
  • Technical anatomy worth remembering:
    • This was a bridge-verification failure even though no multisig keys were stolen and no signature verifier was bypassed. The broken verifier was the combined event schema plus relayer trust model.
    • The critical mistake was equating event emitted with asset locked. Once those came apart, the bridge minted authenticity from logs rather than value movement.
    • The sharper design failure was that Qubit no longer had one canonical source-side artifact for ETH deposits. depositETH() was the actual custody-changing path, but deposit() still retained equal authority to publish ETH-looking deposit truth.
    • The exploit also shows how support for native assets can poison an ERC-20 bridge path when both flows reuse placeholders, whitelists, and log formats without keeping their safety assumptions separate.
    • It was also a function-surface deauthorization failure: once depositETH() became the intended ETH path, the older generic deposit() path still remained live, still inherited the zero-address placeholder assumptions, and still authored the same relay-authoritative Deposit event.
    • The owner-only resource remap mattered because it changed the semantics of an existing bridge path without changing the relayer’s proof model.
    • The zero-address transfer quirk was not obscure EVM trivia discovered for the first time in 2022; public prior-art writeups already documented that low-level calls into no-code addresses can look successful unless the caller proves code existence or a real balance delta. Qubit still let that semantic gap sit directly on the bridge-authenticity boundary.
    • By the time the attacker reached borrow(), the fraud had already happened at the bridge boundary. The lending market was honoring counterfeit but onchain-looking collateral.

Three bridge-truth promotions this incident collapsed

  1. A bridge-looking ETH deposit artifact existed at all
    • Qubit’s generic deposit() path could still emit the same Deposit event shape used for real ETH bridge intake, and the ETH resourceID still resolved through handler logic that looked bridge-valid enough to continue.
    • At this stage the strongest true fact was only a QBridge deposit-shaped log was emitted for the ETH resource, not ETH actually entered bridge custody through the canonical native-asset path.
  2. That artifact was promoted into canonical truth that this exact ETH amount had been locked on Ethereum
    • This was the decisive failure. The relayer treated the shared Deposit event as if it already proved depositETH()-style custody movement, even though the attacker had used the stale generic deposit() route with msg.value == 0 and a zero-address placeholder target.
    • The bridge therefore collapsed a deposit-looking signal exists and the canonical ETH custody path really locked value for this transfer into the same verifier meaning, so a fake source-side event became accepted bridge truth.
  3. That accepted bridge truth still deserved immediate mint and collateral authority on BNB Chain
    • Once the relayer voted the proposal through, the route still let the counterfeit deposit mint 77,162 qXETH and treated that inventory as ordinary bridge-backed collateral.
    • The system then preserved almost no blast-radius separation between one fake ETH deposit signal was honored and protocol-wide borrow power now exists against that lie, which is why the attacker could borrow out a large share of the lending market instead of being trapped inside a narrowly capped bridge lane.
  • Representative transactions and contracts:
    • Attacker address: 0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7
    • Ethereum-side QBridge contract: 0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6
    • Owner-only setResource(...) transaction that remapped the ETH resourceID to address(0): 0xe3da555d506638bd7b697c0bdf7920be8defc9a175cd35bf72fb10bc77167b66
    • Representative attacker-side Ethereum deposit() round with 0 ETH value: block 14090216, 2022-01-27 21:45:32 UTC, on the same Ethereum-side QBridge contract; the translated technical analysis identifies this as one of several zero-value deposit calls that still emitted relay-authoritative Deposit logs before the BNB-side mints.
    • Representative BNB Chain relayer voteProposal(...) transaction minting bridge assets from a fake deposit: 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc
    • Legitimate pre-change deposit() example cited in later technical analysis, showing the same resource path once moved real WETH before the zero-address remap: 0xc85df3fbfee7a8541e9fd354e98df78dca21ac6be40b929ff5da52ea8eab80de
  • Response / mitigation:
    • Qubit disabled supply, redeem, borrow, repay, bridge, and bridge-redemption functions while investigating.
    • The team tracked the exploiter, coordinated with partners including Binance, and publicly offered a large bug-bounty / return arrangement.
    • User-loss lookup tooling and incident communications were published after the exploit.

Pattern tags

  • deposit-signal-locked-value-mismatch
  • bridge-verification-failure
  • Native-asset intake-path deauthorization failure / stale generic route retained authority to publish ETH deposit truth after depositETH() became the canonical custody lane
  • Configuration drift at the bridge-authenticity boundary / owner remap silently changed what the ETH resourceID meant to relayers

Why the exploit worked

  • Qubit split ETH bridge semantics across two unequal paths but let both keep authoring the same relay-authoritative Deposit fact. depositETH() was the real custody lane for ETH, but the stale generic deposit() path could still emit the same bridge truth while enforcing no matching msg.value transfer.
  • The address(0) ETH placeholder stopped being a harmless implementation detail once it remained valid on the generic path. After the owner-only setResource(...) remap pointed the ETH resourceID at the zero address, the handler still accepted that path as whitelisted bridge input and then treated low-level call success as if a real asset transfer had occurred.
  • The relayer completed the second half of the failure by trusting the shared Deposit event rather than the stronger claim ETH arrived through the canonical ETH custody path. Once the fake event existed, the destination bridge honestly executed a lie.
  • The silent semantics change from WETH-backed deposit() history to zero-address ETH-placeholder semantics mattered operationally, not just administratively. Qubit changed what counted as authentic ETH deposit evidence without changing the event surface or the relayer’s proof model.
  • The downstream lending loss was therefore a consequence, not the root exploit. By the time borrow() ran on BNB Chain, bridge-minted qXETH already looked canonically backed because the bridge itself had authenticated the wrong source-side artifact.

Detection and audit pivots

  • Enumerate every function that can emit a relay-visible deposit artifact for a given bridged asset, especially native assets. If depositETH() is the real ETH custody lane, ask which other routes can still emit the same event or reach the same handler semantics.
  • Review all address(0), pseudo-native placeholders, resource mappings, and handler-specific whitelists as bridge-authentication inputs, not plumbing. A sentinel that is safe on one path can become counterfeit deposit authority on another.
  • Require measured custody proof at the accounting boundary itself: msg.value checks on native paths, balance-delta checks on token paths, and code-existence / post-transfer checks before a deposit record is emitted.
  • Diff historic and current meaning of each resourceID. If an owner action can silently change a resource from real token contract to sentinel placeholder without forcing relayer-side decoder changes, the bridge has a configuration-grade authenticity bug.
  • Compare source-side deposit semantics with downstream monetization reach. If one counterfeit bridge artifact can instantly become broadly borrowable collateral across the money market, ask what per-route caps, collateral quarantines, or bridge-health gates should have narrowed the blast radius.

Failure-prevention checklist

  • Let exactly one ETH custody path author ETH bridge truth. Once depositETH() is the canonical native-asset intake lane, generic ERC-20-style deposit() paths should be unable to emit the same relay-authoritative Deposit fact for the ETH resourceID.
  • Derive credited bridge amounts from measured custody change, not from calldata shape plus handler control flow. Native paths should require matching msg.value; token paths should require code existence plus post-transfer balance movement before any deposit record is published.
  • Keep sentinel values lane-local. If address(0) is only meaningful as a native-asset placeholder, generic token handlers and generic resource mappings should reject it outright instead of treating it as a valid token contract.
  • Make resourceID semantics high-friction and explicit. A remap from WETH-style ERC-20 handling to zero-address ETH-placeholder handling should require review, delay, and relayer-side schema changes because it redefines what counts as authentic backing evidence.
  • Split event schemas when custody assumptions differ. Relayers should never need operational folklore to distinguish real ETH entered bridge custody from generic compatibility path ran without value movement.
  • Reconcile destination liabilities against source-side locked assets quickly enough to catch counterfeit qXETH before it can become money-market collateral at size.
  • Put route-local blast-radius limits under bridge-minted collateral: per-route mint caps, collateral quarantine for fresh bridge inventory, and bridge-health gates that stop one bad bridge artifact from immediately opening protocol-wide borrow power.
  • Regression-test no-code-address and stale-path cases directly: resourceID -> address(0) remaps, generic-path ETH deposits with msg.value == 0, and end-to-end fake Deposit event -> relayer voteProposal() -> collateral mint simulations.

Audit prompts

  • For the ETH resourceID, which exact source-side post-condition proves redeemable ETH entered bridge custody, and which functions can currently publish that fact?
  • Can any stale compatibility path, refund helper, or generic token route still emit the same event or reach the same handler state as the canonical native-asset path?
  • Does any bridge path accept sentinel addresses like address(0) while still behaving like an ERC-20 transfer flow? If so, where is no-code-address success prevented from impersonating real custody?
  • What owner or admin action can silently redefine what a resourceID means to relayers today, and what timelock, review, or decoder migration would force that meaning change to become explicit?
  • If the relayer sees a Deposit event, what stronger fact has it actually proven: custody changed, some bridge path ran, or just this log shape appeared?
  • Which controls stop bridge-minted assets from becoming immediately rehypothecatable collateral across the lending market before route health or backing is confirmed?
  • Are source-side liabilities and destination-side wrapped supply reconciled by route and asset quickly enough to isolate ETH route counterfeit mint before broad borrow-out loss socializes it?
  • Do tests cover the awkward but catastrophic seams: same event from two different custody paths, zero-value native deposit, no-code token target, and config remap that changes asset semantics without changing the event schema?

Lessons that should already be standard

  • Event shape is custody logic when relayers spend against it. If one log can be emitted by both a real custody path and a fake compatibility path, the bridge has already outsourced verification to guesswork.

  • Native-asset support is not a harmless extension of ERC-20 plumbing. Zero-address placeholders, shared handler code, and convenience paths routinely become counterfeit-deposit authority if they retain access to the same bridge-truth surface.

  • Configuration changes can be verifier changes in disguise. Qubit’s owner-only setResource(...) remap did not just update bookkeeping; it changed what the relayer was willing to honor as ETH deposit evidence.

  • Silent success is not proof of asset movement. Low-level call success against a no-code address is exactly the kind of implementation artifact that must never be allowed to stand in for custody.

  • Stale paths need real deauthorization, not comments and operator intent. Once depositETH() became the intended ETH lane, leaving deposit() live with equal event authority meant the bridge still had two contradictory speakers for one economic fact.

  • Bounded release applies to collateral too, not only bridge withdrawals. If fresh bridge inventory can instantly become broadly borrowable collateral, a bridge-truth bug becomes a whole-protocol solvency event instead of a route-local incident.

  • Key transaction hashes:

    • Owner-only setResource(...) remap to address(0): 0xe3da555d506638bd7b697c0bdf7920be8defc9a175cd35bf72fb10bc77167b66
    • Representative BNB Chain relayer voteProposal(...) mint from a fake deposit: 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc
    • Legitimate pre-change deposit() example on the same resource path: 0xc85df3fbfee7a8541e9fd354e98df78dca21ac6be40b929ff5da52ea8eab80de
  • Comparable exploit pattern:

Why this incident matters

  • It is a very clean case where a bridge did not fail at cryptography; it failed because its relayer accepted the wrong operational signal as proof.
  • It shows that event design is custody logic when relayers or downstream chains treat events as authoritative.
  • More specifically, it preserves the healthy design rule that only the canonical ETH intake lane should have been allowed to author ETH deposit truth. Once depositETH() was the path that actually received value, deposit() should not have remained a second way to mint the same bridge fact.
  • It is also a durable lesson in native asset support bolted onto ERC-20 bridge plumbing: zero-address sentinels, shared handlers, and ambiguous event schemas are not implementation details; they are part of the trust boundary.
  • It also preserves a quieter lifecycle lesson: when a new native-asset path replaces an older generic one, leaving the older path live without distinct events or hard deauthorization can silently widen who is allowed to author bridge truth.
  • It is also a bounded-release case study: one counterfeit ETH deposit artifact was enough to mint 77,162 qXETH, let the attacker post that fake inventory as collateral, and then borrow out a large share of Qubit’s live lending markets before anyone could contain the route.

Internal linkages

Control surface