We do not have a reliable USD price for the recorded assets yet.
0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6Ethereum0x80d1486ef600cc56d4df9ed33baf53c60d5a629bEthereum0x4d8ae68fcae98bf93299548545933c0d273ba23aBSC0xfd7a5506f434f5334c100efb765025243c39137cBSCIn 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.
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:
0x99309d2e7265528dc7c3067004cc4a90d37b7cc30x20e5e35ba29dc3b540a1aee781d0814d5c77bce60x80d1486ef600cc56d4df9ed33baf53c60d5a629b0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc0x17b7163cf1dbd286e262ddc68b553d899b93f526On BSC, the primary addresses are:
0x4d8ae68fcae98bf93299548545933c0d273ba23a0xfd7a5506f434f5334c100efb765025243c39137c0x2dfab0eebec0d3deb37c72f3c2ec62116178040eThe 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.
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.depositETH → QBridgeHandler.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.
At Ethereum block 14090105 and BSC block 14741733, the following conditions hold (see seed/index.json, storage layout snapshots, and tokentx logs):
0x2f422fe9ea622049d6f73f81a906b9b8cff03b7f, and burnList[tokenAddress] is enabled in the relevant configuration on BSC.0xfd7a5506f434f5334c100efb765025243c39137c is deployed and wired to a TransparentUpgradeableProxy implementation (0x2dfab0...), which in turn delegates to a token implementation that exposes mint and Transfer semantics.0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7 is funded and has no prior qXETH mint events in the observed window.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": []
}
The attacker begins by calling QBridge.depositETH on Ethereum via the proxy 0x20e5e35b...:
0x7f537463d055f32b49050a7e2179461666a9494deb7045d7825d72c210168298 (block 14090105)depositETH(uint8 destinationChainID, bytes32 resourceID, bytes data)0.1 ETH (msg.value = 0.1 ETH)56 (BSC)0x2f422fe9ea622049d6f73f81a906b9b8cff03b7f(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:
The attack later repeats this pattern with QBridge.deposit:
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.
On BSC, the honest relayer 0xeb645b4c35cf160e47f0a49c03db087c421ab545 monitors QBridge Deposit events and submits voteProposal calls on the QBridge proxy 0x4d8ae68f.... A representative transaction is:
0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc (block 14741733)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:
0xfd7a5506... that forwards via delegatecall to implementation 0x2dfab0....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.
The core invariant for burnList‑style bridge tokens is:
QBridgeHandler in response to corresponding deposits.The concrete breakpoint is:
QBridgeHandler.depositETH, which:
amount == msg.value.amount >= minAmounts[resourceID][option].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:
Transfer events from the zero address to the attacker on BSC.This behavior is entirely driven by public contract logic and configuration and does not rely on private keys beyond the attacker’s unprivileged EOA.
The adversary‑related cluster consists of a single EOA:
0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7
attacker_to_qbridge_summary.json).attacker_tokentx_raw.json and attacker_tokentx_xeth_summary.json).Victim contracts are:
Ethereum QBridge deposits for xETH resourceID
0x7f537463d055f32b49050a7e2179461666a9494deb7045d7825d72c210168298 (block 14090105, depositETH with 0.1 ETH).0xff6fa990d4f6cf85592f7fb32840539e6dad20fda4e547acb1a4bbb969d99ba9 (block 14090126, depositETH with 0.1 ETH).0xac7292e7d0ec8ebe1c94203d190874b2aab30592327b6cc875d00f18de6f3133 and 15 other deposit calls (blocks 14090170–14090234) with larger amounts.Deposit events while not burning or locking any QBridgeToken on Ethereum.BSC voteProposal executions minting qXETH
0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc (block 14741733).0x881a68c9..., 0x61ca8bc2..., 0xf6008ab4..., 0xd8bba155... and others in the same series.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.Downstream collateralization and borrowing (out of scope for invariant)
The exploit satisfies the ACT (Anyone‑Can‑Take) criteria:
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.From the bridge’s perspective:
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.
Ethereum contracts and code
QBridge.sol): 0x99309d2e7265528dc7c3067004cc4a90d37b7cc3
artifacts/root_cause/data_collector/iter_1/contract/1/0x99309d2e7265528dc7c3067004cc4a90d37b7cc3/source/src/bridge/QBridge.solQBridgeHandler.sol): 0x80d1486ef600cc56d4df9ed33baf53c60d5a629b
artifacts/root_cause/data_collector/iter_1/contract/1/0x80d1486ef600cc56d4df9ed33baf53c60d5a629b/source/src/bridge/QBridgeHandler.solQBridgeToken.sol): 0x17b7163cf1dbd286e262ddc68b553d899b93f526
artifacts/root_cause/data_collector/iter_1/contract/1/0x80d1486ef600cc56d4df9ed33baf53c60d5a629b/source/src/bridge/QBridgeToken.solBSC contracts and code
0x4d8ae68fcae98bf93299548545933c0d273ba23a0xfd7a5506f434f5334c100efb765025243c39137c
artifacts/root_cause/data_collector/iter_4/bsc/bsc_qxeth_getsourcecode.jsonKey transactions
artifacts/root_cause/data_collector/iter_3/tx/1/attacker_to_qbridge_summary.json, including seed tx 0xac7292e7d0ec8ebe1c94203d190874b2aab30592327b6cc875d00f18de6f3133 and initial depositETH tx 0x7f537463d055f32b49050a7e2179461666a9494deb7045d7825d72c210168298.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
cast run -vvvvv) for 0xac7292e7d0ec8ebe1c94203d190874b2aab30592327b6cc875d00f18de6f3133: artifacts/root_cause/seed/1/0xac7292e7d0ec8ebe1c94203d190874b2aab30592327b6cc875d00f18de6f3133/trace.cast.log.0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc: artifacts/root_cause/data_collector/iter_5/bsc_tx_trace/0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc/tx_trace_callTracer.json.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
artifacts/root_cause/data_collector/iter_2/other/qbridge_known_incidents.json.