All incidents

Qubit QBridge xETH Unbacked Mint ACT Opportunity

Share
Jan 27, 2022 21:19 UTCAttackLoss: 216,960.2 qXETHManually checked3 exploit txWindow: 14m 20s
Estimated Impact
216,960.2 qXETH
Label
Attack
Exploit Tx
3
Addresses
4
Attack Window
14m 20s
Jan 27, 2022 21:19 UTC → Jan 27, 2022 21:34 UTC

Exploit Transactions

TX 1Ethereum
0x7f537463d055f32b49050a7e2179461666a9494deb7045d7825d72c210168298
Jan 27, 2022 21:19 UTCExplorer
TX 2Ethereum
0xac7292e7d0ec8ebe1c94203d190874b2aab30592327b6cc875d00f18de6f3133
Jan 27, 2022 21:34 UTCExplorer
TX 3BSC
0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc
Jan 27, 2022 21:22 UTCExplorer

Victim Addresses

0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6Ethereum
0x80d1486ef600cc56d4df9ed33baf53c60d5a629bEthereum
0x4d8ae68fcae98bf93299548545933c0d273ba23aBSC
0xfd7a5506f434f5334c100efb765025243c39137cBSC

Loss Breakdown

216,960.2qXETH

Similar Incidents

Root Cause Analysis

Qubit QBridge xETH Unbacked Mint ACT Opportunity

1. Incident Overview TL;DR

In January 2022, Qubit Finance’s cross-chain QBridge allowed an attacker to mint a large quantity of Qubit xETH (qXETH) on BSC without locking or burning any backing xETH representation on Ethereum. Using only an unprivileged externally owned account (EOA), the adversary called the Ethereum QBridge proxy with depositETH and then deposit for a burnList‑configured xETH resourceID, while the corresponding handler on Ethereum performed only value and whitelist checks and never burned or locked the backing QBridgeToken. On BSC, honest relayers later processed the resulting proposals via voteProposal, causing the BSC QBridge handler to execute the burnList path and mint qXETH to the attacker from the zero address. This broke the bridge’s conservation‑of‑value invariant and created 216960.19999999998 unbacked qXETH, constituting an Anyone‑Can‑Take (ACT) opportunity for any similarly situated unprivileged actor.

2. Key Background

QBridge is Qubit Finance’s cross‑chain bridge between Ethereum and BSC. On each chain, a QBridge contract maintains deposit nonces and proposal state, while a QBridgeHandler contract performs the asset‑level logic for deposits and proposal execution. For tokens bridged as wrapped representations (such as xETH/qXETH), the system uses a QBridgeToken contract that can be minted and burned by designated minters.

On Ethereum, the relevant contracts and addresses are:

  • QBridge implementation: 0x99309d2e7265528dc7c3067004cc4a90d37b7cc3
  • QBridge TransparentUpgradeableProxy: 0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6
  • QBridgeHandler: 0x80d1486ef600cc56d4df9ed33baf53c60d5a629b
  • QBridgeToken (Ethereum xETH representation): 0x17b7163cf1dbd286e262ddc68b553d899b93f526

On BSC, the primary addresses are:

  • QBridge proxy: 0x4d8ae68fcae98bf93299548545933c0d273ba23a
  • Qubit xETH (qXETH) proxy: 0xfd7a5506f434f5334c100efb765025243c39137c
  • qXETH implementation (from proxy metadata): 0x2dfab0eebec0d3deb37c72f3c2ec62116178040e

The bridge uses a resourceID to tie together handler logic and token contracts across chains. On Ethereum, QBridgeHandler.setResource sets resourceIDToTokenContractAddress[resourceID] and contractWhitelist[token]. The handler also maintains a burnList mapping: if a token is burnable, executing a proposal on the destination chain mints fresh wrapped tokens and deposits on the source chain must burn or lock the backing.

The relevant parts of QBridgeHandler (Ethereum) are:

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");
}

For burnList tokens, deposit burns QBridgeToken from the depositor. By contrast, depositETH only checks msg.value, whitelist, and minAmounts and never burns or locks the backing ERC‑20. On the destination chain, executeProposal mints:

function executeProposal(bytes32 resourceID, bytes calldata data) external override onlyBridge {
    uint option;
    uint amount;
    address recipientAddress;
    (option, amount, recipientAddress) = abi.decode(data, (uint, uint, address));

    address tokenAddress = resourceIDToTokenContractAddress[resourceID];

    require(contractWhitelist[tokenAddress], "provided tokenAddress is not whitelisted");

    if (burnList[tokenAddress]) {
        address delegatorAddress = delegators[option];
        if (delegatorAddress == address(0)) {
            QBridgeToken(tokenAddress).mint(recipientAddress, amount);
        } else {
            QBridgeToken(tokenAddress).mint(delegatorAddress, amount);
            IQBridgeDelegator(delegatorAddress).delegate(tokenAddress, recipientAddress, option, amount);
        }
    } else if (tokenAddress == ETH) {
        SafeToken.safeTransferETH(recipientAddress, amount.sub(withdrawalFees[resourceID]));
    } else {
        tokenAddress.safeTransfer(recipientAddress, amount.sub(withdrawalFees[resourceID]));
    }
}

The expected invariant is that for any burnList resourceID, destination‑chain minted supply must be backed by source‑chain burns/locks. In the incident configuration, the exploited xETH resourceID is in the burnList on BSC, but the attacker’s deposits on Ethereum use the depositETH path that does not burn QBridgeToken.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is a cross‑chain invariant violation caused by inconsistent use of burn semantics for a burnList‑configured resourceID. On the destination chain (BSC), the qXETH handler treats the xETH resourceID as burnable and mints qXETH when proposals are executed, assuming that backing has already been burned or locked on the source chain. On the source chain (Ethereum), the same resourceID is wired so that the attacker’s deposits go through QBridge.depositETHQBridgeHandler.depositETH, which performs only ETH value and minimum‑amount checks and never calls QBridgeToken.burnFrom or transfers any backing tokens.

As a result, each cross‑chain deposit for this resourceID increases the attacker’s qXETH balance on BSC without decreasing any xETH representation on Ethereum. Honest BSC relayers, acting under standard voteProposal/executeProposal logic, unknowingly mint unbacked qXETH to the attacker. This misconfiguration and handler/API mismatch form an Anyone‑Can‑Take ACT opportunity: any unprivileged EOA that can call QBridge on Ethereum and receive qXETH on BSC can reproduce the exploit.

4. Detailed Root Cause Analysis

4.1 Pre‑state and configuration

At Ethereum block 14090105 and BSC block 14741733, the following conditions hold (see seed/index.json, storage layout snapshots, and tokentx logs):

  • QBridge/QBridgeHandler on Ethereum are deployed and configured with a resourceID that maps to a token contract 0x2f422fe9ea622049d6f73f81a906b9b8cff03b7f, and burnList[tokenAddress] is enabled in the relevant configuration on BSC.
  • On BSC, the qXETH proxy 0xfd7a5506f434f5334c100efb765025243c39137c is deployed and wired to a TransparentUpgradeableProxy implementation (0x2dfab0...), which in turn delegates to a token implementation that exposes mint and Transfer semantics.
  • Attacker EOA 0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7 is funded and has no prior qXETH mint events in the observed window.
  • Ethereum QBridgeToken 0x17b7163cf1dbd286e262ddc68b553d899b93f526 shows no ERC‑20 transfers or burns in the block range 14090000–14090300, confirming that the bridge does not manipulate this token during the attack window:
{
  "status": "0",
  "message": "No transactions found",
  "result": []
}

4.2 Source‑chain deposit path without burn

The attacker begins by calling QBridge.depositETH on Ethereum via the proxy 0x20e5e35b...:

  • Tx 0x7f537463d055f32b49050a7e2179461666a9494deb7045d7825d72c210168298 (block 14090105)
    Function: depositETH(uint8 destinationChainID, bytes32 resourceID, bytes data)
    Value: 0.1 ETH (msg.value = 0.1 ETH)
    DestinationChainID: 56 (BSC)
    ResourceID: embeds token 0x2f422fe9ea622049d6f73f81a906b9b8cff03b7f
    Data: ABI‑encoded (option, amount) with amount = 0.1 ETH.

From attacker_to_qbridge_summary.json, this is the first of the attacker’s deposits. Within QBridge.sol, the call path is:

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);
}

The handler then executes depositETH as shown in Section 2: it only validates msg.value, whitelist, and minAmounts for the resourceID/option pair. It does not:

  • Burn any QBridgeToken from the depositor.
  • Transfer any ERC‑20 representing xETH into escrow.

The attack later repeats this pattern with QBridge.deposit:

  • Seed tx 0xac7292e7d0ec8ebe1c94203d190874b2aab30592327b6cc875d00f18de6f3133 (block 14090170) and 15 similar deposit calls, all from the attacker and all using the same resourceID and destinationDomainID with varying amounts. These are enumerated in attacker_to_qbridge_summary.json.

For these deposit calls, the code path would normally enforce a burn for burnList tokens, but the misconfiguration and cross‑chain resourceID wiring cause the exploited xETH resourceID to be handled inconsistently: the BSC side treats it as burnList while the attacker’s Ethereum path does not actually burn or lock backing for the minted qXETH.

4.3 Destination‑chain proposal execution with mint

On BSC, the honest relayer 0xeb645b4c35cf160e47f0a49c03db087c421ab545 monitors QBridge Deposit events and submits voteProposal calls on the QBridge proxy 0x4d8ae68f.... A representative transaction is:

  • Tx 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc (block 14741733)
    Function: voteProposal(uint8 chainID, uint64 depositNonce, bytes32 resourceID, bytes data)
    chainID = 1 (Ethereum), resourceID matches the xETH resourceID, depositNonce ties back to the attacker’s Ethereum deposit.

The BSC callTracer trace for this transaction (bsc_tx_trace/.../tx_trace_callTracer.json) shows:

  • A call into the qXETH proxy 0xfd7a5506... that forwards via delegatecall to implementation 0x2dfab0....
  • Within the implementation, a call with selector 0x40c10f19 (standard ERC‑20 mint(address,uint256)) to a token contract 0xbf8169c5... (underlying implementation), with arguments:
{
  "from": "0x2f422fe9ea622049d6f73f81a906b9b8cff03b7f",
  "to": "0xbf8169c537eb6861c62beaacb9403ac37b4c8c7f",
  "type": "DELEGATECALL",
  "input": "0x40c10f19 ... 0000000000000000016345785d8a0000"
}

This results in a qXETH Transfer event from the zero address to the attacker. The Etherscan‑style token log summary (attacker_tokentx_xeth_summary.json) confirms that for this and four other sampled mints:

{
  "contractAddress": "0xfd7a5506f434f5334c100efb765025243c39137c",
  "from": "0x0000000000000000000000000000000000000000",
  "to": "0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7",
  "tokenName": "Qubit xETH",
  "tokenSymbol": "qXETH",
  "methodId": "0xc0331b3e",
  "functionName": "voteProposal(uint8 chainID, uint64 depositNonce, bytes32 resourceID, bytes data)"
}

Across the 18 qXETH mint events tied to voteProposal calls, the total amount minted to the attacker is 216960.19999999998 qXETH, as computed in root_cause.json from attacker_tokentx_raw.json.

4.4 Invariant and breakpoint

The core invariant for burnList‑style bridge tokens is:

  • For any resourceID configured so that the destination‑chain handler executes the burnList branch and mints a wrapped token (qXETH) to the recipient, the total minted supply on the destination chain must not exceed the total amount of backing tokens that have been irreversibly burned or locked on the source chain by QBridgeHandler in response to corresponding deposits.

The concrete breakpoint is:

  • On Ethereum, for the exploited xETH resourceID, the attacker’s deposits are processed by QBridgeHandler.depositETH, which:
    • Checks that amount == msg.value.
    • Checks that the token is whitelisted and amount >= minAmounts[resourceID][option].
    • Does not burn or lock any QBridgeToken backing for the xETH representation.
  • On BSC, the same resourceID is configured as a burnList token, and executeProposal mints qXETH to the attacker under the burnList branch.

Because there is no burn or lock on Ethereum, each cross‑chain deposit yields net positive qXETH supply on BSC, directly violating the invariant. On‑chain evidence shows:

  • 18 qXETH Transfer events from the zero address to the attacker on BSC.
  • No QBridgeToken transfers or burns for the same window on Ethereum.

This behavior is entirely driven by public contract logic and configuration and does not rely on private keys beyond the attacker’s unprivileged EOA.

5. Adversary Flow Analysis

5.1 Adversary‑related cluster

The adversary‑related cluster consists of a single EOA:

  • Attacker EOA: 0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7
    • Sends all QBridge.depositETH and QBridge.deposit calls in the exploit window on Ethereum (see attacker_to_qbridge_summary.json).
    • Receives all 18 qXETH mint events on BSC from the zero address (see attacker_tokentx_raw.json and attacker_tokentx_xeth_summary.json).

Victim contracts are:

  • Ethereum QBridge proxy/implementation and QBridgeHandler (bridge logic and handler).
  • Ethereum QBridgeToken (xETH representation).
  • BSC QBridge proxy.
  • BSC qXETH proxy/implementation (wrapped token representation).

5.2 Lifecycle stages

  1. Ethereum QBridge deposits for xETH resourceID

    • Tx 0x7f537463d055f32b49050a7e2179461666a9494deb7045d7825d72c210168298 (block 14090105, depositETH with 0.1 ETH).
    • Tx 0xff6fa990d4f6cf85592f7fb32840539e6dad20fda4e547acb1a4bbb969d99ba9 (block 14090126, depositETH with 0.1 ETH).
    • Tx 0xac7292e7d0ec8ebe1c94203d190874b2aab30592327b6cc875d00f18de6f3133 and 15 other deposit calls (blocks 14090170–14090234) with larger amounts.
    • Effect: increments depositNonce for the xETH resourceID for destinationChainID 56, emitting Deposit events while not burning or locking any QBridgeToken on Ethereum.
  2. BSC voteProposal executions minting qXETH

    • Representative tx 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc (block 14741733).
    • Additional txs 0x881a68c9..., 0x61ca8bc2..., 0xf6008ab4..., 0xd8bba155... and others in the same series.
    • Effect: for each proposal that reaches relayer threshold, executeProposal mints qXETH (via ERC‑20 mint) and results in a Transfer event from 0x000...000 to the attacker’s EOA. Total minted: 216960.19999999998 qXETH.
  3. Downstream collateralization and borrowing (out of scope for invariant)

    • External incident reports and BSC token transfer logs indicate that the attacker later used qXETH as collateral in Qubit’s BSC lending protocol to borrow other assets. The present root cause analysis focuses on the bridge‑level invariant break (creation of unbacked qXETH) rather than reconstructing the full lending‑pool drain.

5.3 ACT criteria

The exploit satisfies the ACT (Anyone‑Can‑Take) criteria:

  • Unprivileged adversary: The attacker uses only an EOA with no special privileges on either chain.
  • Permissionless access: QBridge.depositETH and QBridge.deposit are externally callable by any EOA; BSC relayer transactions are public and do not depend on private agreements with the attacker.
  • Public information: All required parameters (resourceID, destinationChainID, depositNonce) are visible from public logs and traces; the misconfiguration can be discovered by inspecting verified contract code and storage.
  • Repeatability: Any other unprivileged actor observing the same configuration could independently execute the same sequence of deposits and rely on the same relayers to mint unbacked qXETH to their own address.

6. Impact & Losses

From the bridge’s perspective:

  • Unbacked minted supply: A total of 216960.19999999998 qXETH were minted on BSC to the attacker from the zero address without any corresponding burn or lock of QBridgeToken on Ethereum. This directly measures the magnitude of the invariant violation in the qXETH reference asset.
  • Backing deficit: After the exploit, the outstanding qXETH liabilities on BSC exceed the Ethereum‑side xETH representation by 216960.19999999998 units. Any honest user holding qXETH is exposed to under‑collateralization risk if they attempt to redeem across chains.

External reports (see qbridge_known_incidents.json) and additional BSC traces show that the attacker subsequently supplied qXETH as collateral to Qubit’s lending pools and borrowed other tokens, amplifying losses beyond the bridge itself. However, even if no downstream lending actions were taken, the bridge alone would remain critically insolvent with respect to xETH/qXETH.

7. References

  • Ethereum contracts and code

    • QBridge implementation (QBridge.sol): 0x99309d2e7265528dc7c3067004cc4a90d37b7cc3
      • Source: artifacts/root_cause/data_collector/iter_1/contract/1/0x99309d2e7265528dc7c3067004cc4a90d37b7cc3/source/src/bridge/QBridge.sol
    • QBridgeHandler (QBridgeHandler.sol): 0x80d1486ef600cc56d4df9ed33baf53c60d5a629b
      • Source: artifacts/root_cause/data_collector/iter_1/contract/1/0x80d1486ef600cc56d4df9ed33baf53c60d5a629b/source/src/bridge/QBridgeHandler.sol
    • QBridgeToken (QBridgeToken.sol): 0x17b7163cf1dbd286e262ddc68b553d899b93f526
      • Source: artifacts/root_cause/data_collector/iter_1/contract/1/0x80d1486ef600cc56d4df9ed33baf53c60d5a629b/source/src/bridge/QBridgeToken.sol
  • BSC contracts and code

    • QBridge proxy: 0x4d8ae68fcae98bf93299548545933c0d273ba23a
    • qXETH proxy: 0xfd7a5506f434f5334c100efb765025243c39137c
      • Proxy verification and implementation: artifacts/root_cause/data_collector/iter_4/bsc/bsc_qxeth_getsourcecode.json
  • Key transactions

    • Ethereum deposits (attacker → QBridge proxy): summarized in artifacts/root_cause/data_collector/iter_3/tx/1/attacker_to_qbridge_summary.json, including seed tx 0xac7292e7d0ec8ebe1c94203d190874b2aab30592327b6cc875d00f18de6f3133 and initial depositETH tx 0x7f537463d055f32b49050a7e2179461666a9494deb7045d7825d72c210168298.
    • BSC qXETH mints via voteProposal: summarized in artifacts/root_cause/data_collector/iter_4/bsc/attacker_tokentx_xeth_summary.json and attacker_tokentx_raw.json, with representative tx 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc.
  • Traces and storage

    • Seed Ethereum tx call trace (cast run -vvvvv) for 0xac7292e7d0ec8ebe1c94203d190874b2aab30592327b6cc875d00f18de6f3133: artifacts/root_cause/seed/1/0xac7292e7d0ec8ebe1c94203d190874b2aab30592327b6cc875d00f18de6f3133/trace.cast.log.
    • BSC callTracer trace for qXETH‑minting voteProposal tx 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc: artifacts/root_cause/data_collector/iter_5/bsc_tx_trace/0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc/tx_trace_callTracer.json.
    • Ethereum QBridge/QBridgeHandler storage layout and snapshot at 0xd6ffba: artifacts/root_cause/data_collector/iter_2/QBridge_storage_layout.json, artifacts/root_cause/data_collector/iter_2/QBridgeHandler_storage_layout.json, and artifacts/root_cause/data_collector/iter_2/storage/1/qbridge_and_handler_storage_0xd6ffba.json.
  • External reports

    • Aggregated public incident reports describing the broader QBridge/Qubit exploit and downstream lending‑pool losses: artifacts/root_cause/data_collector/iter_2/other/qbridge_known_incidents.json.