Everything marked [verified] below we checked directly against Ethereum mainnet; anyone can reproduce it from the addresses and hashes in the appendix. Items marked [reported] or [inferred] come from public reporting or from reading the code, and are flagged individually. We do not present them as proven fact.
TL;DR
On 2026-07-23 the Verus↔Ethereum bridge was drained for the second time, for roughly $7.54M. On the Ethereum side the attacker spent 0 ETH (gas only) and passed the bridge’s verification with a forged cross-chain import — because the bridge only checked that the payout list was intact (the hash matched), and never that the withdrawal was backed by equal value locked on Verus. Committing that hash on the Verus side costs almost nothing (~$0.01). The same class of flaw was exploited two months earlier, in May.
1. The incident at a glance
| Field | Value |
|---|---|
| Time | 2026-07-23, ~03:45 UTC (second drain; first was May 2026, ~$11.58M) |
| Loss | ~$7.54M (ETH, tBTC, USDC, USDT, MKR, scrvUSD, EURC) |
| Chain | Ethereum mainnet |
| Bridge (proxy) | 0x71518580f36feceffe0721f06ba4703218cd7f63 |
| Implementation | 0x27c76df6912698e6d4c55aaa87cf88c30db90cf7 (SubmitImports, source-verified, solc 0.8.9) |
| Flaw location | VerusProof (0x54e03a1682fd0bb065b669f6296f97028dcfd4ce) .checkExportAndTransfers |
| Attack tx | 0xa1f1e65c1cea4dba4ae439cd4dcdba6cc2dbda0ed1228e61f29ae9c9324eb099 |
| Attacker | initiator 0xbda71b58cec0b1c20a8f87ccd52fa0679747855c; funds pooled at 0xCFd0A20703cD11E0b9f665e1C3F1Ef989C142D54 |
| Ethereum-side cost | value = 0 ETH |
All of the above is [verified], except “first drain, May, ~$11.58M”, which is [reported].
2. How the bridge is supposed to work
Verus↔Ethereum uses an export / import model:
- On the Verus chain, an export is created — the value to move across is locked / accounted for.
- Verus notaries commit that export to Ethereum (a hash plus a state root).
- On the Ethereum side,
SubmitImports._createImportsreceives the import, verifies the proof, and hands off toTokenManager.processTransactions, which pays out from the bridge’s reserves according to the transfer list inside the import.
The load-bearing detail: the payout list serializedTransfers is attacker-controlled input. The bridge’s one job is to guarantee that whatever the list pays out is actually backed by equal value locked on the Verus side.
3. Where the flaw is (down to the function)
All of the gatekeeping before payout lives in VerusProof.proveImports → checkExportAndTransfers. For this import, that function’s entire validation is:
// VerusProof.checkExportAndTransfers (approx. L412-417)
if (!(cce.hashReserveTransfers == hashedTransfers && // the payout list's hash matches (integrity)
cce.sourceSystemID == VERUS &&
cce.destSystemID == VETH &&
cce.destCurrencyID == VETH)) { // source chain / dest chain / currency match
revert("CCE information does not checkout");
}
It checks: the payout-list hash is consistent, and the source chain, destination chain, and currency match.
What it never checks: how much value was actually reserved on the Verus side behind this payout. The function reads cce.numInputs (the count of transfers) but never compares cce.totalamounts (the declared reserve total) against the amount being paid out, and imposes no upper bound on the amount whatsoever.
In one line: the bridge verified that the data wasn’t tampered with (integrity), but never that the withdrawal had real value behind it (value conservation) — and committing that hash on Verus is nearly free (~$0.01). The two were conflated.
The vulnerable function source is [verified]: the implementation is source-verified on Ethereum and can be read directly.
4. The attack, step by step
-
[reported — Verus side] The attacker creates an export worth ~0.01–0.02 VRSC (≈ $0.01) whose payout list — a “pay me $7.54M” list — is committed as a hash.
-
[mechanism] Verus notaries routinely attest Verus chain state (which now includes that $0.01 export) to Ethereum. Note: the notaries only prove “this is Verus chain state”; they do not approve the payout. The attacker corrupts no notary — they abuse the bridge’s normal habit of attesting the whole chain.
-
[verified] From
0xbda71b…855c, the attacker sends tx0xa1f1e65c…24eb099,to= bridge,value = 0 ETH, carrying 4,324 bytes of forged calldata into the import function (selector0x8c49b257). -
[verified + mechanism]
checkExportAndTransferscompares only the hash and the chain/currency IDs → the hash of the attacker’s payout list matches the hash they committed on Verus → the check passes. -
[verified]
TokenManager.processTransactionspays out the list. The ERC-20 legs, which we reconciled line by line:Asset Amount tBTC 71.5045915 USDC 149,275.07 USDT 78,300.54 MKR 59.43 scrvUSD 92,784.36 EURC 31,475.66 ETH also flowed out via internal transactions (not priced per-leg here); security firms put the total at ~$7.54M. Funds landed at
0xCFd0A2…D54. -
[verified] The attacker then deposited the proceeds into a Tornado Cash router (
0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b) in fixed denominations of 0.1 / 1 / 10 ETH to mix them.
5. Why the same hole was exploited twice in two months
After the May incident, the bridge shipped a “security upgrade.” But the check in checkExportAndTransfers requiring that inputs must spend from the previous CCE transaction (TX_PREVOUTSEQ / "Input does not spend from last CCE tx") is very likely the May patch — it closes the specific vector of forged / replayed inputs, but it still never adds an amount check.
The patch sealed one concrete gap; it never fixed the root seam of “trust the hash, don’t verify the value” — so the same class of flaw recurred within two months. ([inferred] from code structure and public attribution; the security firm Blockaid also states this incident shares the May vulnerability class.)
6. The fix
There is only one real remedy: conservation of value.
- The core
require(minimal fix): before paying out, enforce that the total to be paid ≤ the value that is provably reserved / locked on the Verus side. Bringcce.totalamountsinto the check, and require every transfer amount to fall within proven-covered fields — not merely matchhashReserveTransfers. - Separate integrity from validity: a matching hash only proves “this data was committed”; you must independently prove “this data is backed by equal value.” The two are not the same thing.
- Withdrawal caps / rate limits (defense in depth): put hard ceilings on per-transaction and per-window payouts. Even if the logic is bypassed, the blast radius is capped.
- An assertion-level regression test for “tiny input → huge output”: encode “value must be conserved” as a red line that can never be deleted. Then any patch that fails to reach the root gets caught on the spot, before shipping — which is exactly what could have prevented the second drain.
7. Our take: this was catchable in advance
This is an omission-class flaw — not a wrong line, but a missing one. Flaws like this emit no positive signal you can spot; you cannot catch them by “reading the code cleverly.” The only thing that makes them visible is running a fixed checklist of invariants that should hold against the code, and doing the subtraction: should-have minus does-have.
Near the top of any cross-chain bridge’s required invariants is value conservation: value paid out ≤ value provably locked. Run that one down the Verus payout path, all the way to checkExportAndTransfers, and you find that coverage of the “amount conserved” class is zero — the path carries only integrity checks and weak-identity checks. The checklist reaches this point and prints a blank:
payout sink : processTransactions(serializedTransfers) ← amounts attacker-controlled
path checks : [ hashReserveTransfers match (integrity), chain/currency IDs (weak identity) ]
missing class : value conservation ← the hole
That red line isn’t because we’re smarter than the attacker — it’s that “value conservation” was already on the checklist, so it lights up when the path reaches it. Different auditor, different level of fatigue, same line still lights up red. That is how we audit: we don’t bet on inspiration, we bet on the checklist.
Appendix — evidence, verify it yourself
Every row below is on Etherscan; check them line by line.
| Object | Address / hash |
|---|---|
| Bridge (proxy) | 0x71518580f36feceffe0721f06ba4703218cd7f63 |
Implementation SubmitImports |
0x27c76df6912698e6d4c55aaa87cf88c30db90cf7 (source-verified) |
VerusProof (flaw location) |
0x54e03a1682fd0bb065b669f6296f97028dcfd4ce |
TokenManager (payout) |
0xc45eedc99a98bc2e5ce717bb3ba16bde1725730a |
| Attack tx | 0xa1f1e65c1cea4dba4ae439cd4dcdba6cc2dbda0ed1228e61f29ae9c9324eb099 |
| Attacker (initiator) | 0xbda71b58cec0b1c20a8f87ccd52fa0679747855c |
| Funds pool | 0xCFd0A20703cD11E0b9f665e1C3F1Ef989C142D54 |
| Tornado Cash router | 0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b |
Honesty statement
- [verified] items were checked by us directly on Ethereum mainnet: the vulnerable contract source, the attack transaction, each ERC-20 outflow amount, the mixing route, and the sub-contract registry. Anyone can reproduce them from the table above.
- [reported / inferred] items — the ~$0.01 Verus-side commitment, the May loss figure, the “same vulnerability class” attribution, the precise ETH-leg pricing — come from public reporting and from reading the code. They are flagged individually and are not presented as proven fact.
- This is a security technical analysis, not investment advice. It states only verifiable on-chain and code facts, and makes no claim about the identity or intent of any party.
SIRENBOW audits cross-chain and DeFi systems against a fixed invariant checklist — the same method that lights up omissions like this one. If you’re shipping a bridge, talk to us.