On Ethereum block 25118335 at 2026-05-17T23:55:23Z, attacker EOA 0x5abb91b9c01a5ed3ae762d32b236595b459d5777 called bridge dispatcher 0x71518580f36feceffe0721f06ba4703218cd7f63 and drained bridge-held assets to drainer 0x65cb8b128bf6e690761044cceca422bb239c25f9. The trace shows a BTC-import and proof-processing flow followed immediately by bridge payouts, which is consistent with a logic error in the bridge’s import/proof path rather than a flash-loan or reentrancy pattern. Exact losses were 1,625.36688649 ETH, 103.56766017 tBTC, and 147,658.836798 USDC; local artifacts support a minimum USD loss of $147,658.84 from the USDC leg alone, but they do not include a trusted contemporaneous valuation for the ETH and tBTC legs.

Root Cause

Vulnerable Contract

Suspected Bridge Dispatcher at 0x71518580f36feceffe0721f06ba4703218cd7f63.

The dispatcher is a delegatecall-based bridge router. In this local run, recovered pseudocode now exists for the dispatcher and the three trace-visible modules behind it: entry module 0xa045cf963b79833faf445f555ee1a6812d6fc87f, verification module 0x5e8060ecbf415aa25f12c1d67fde832bd87dcfa1, and payout module 0x08f0fbcc068c70a29326094110769ee5f1d0107d. These files are decompiler-style recovered approximations, not verified Solidity, so they are used only to express the same control flow already proven by the trace.

Vulnerable Function

Trace-visible exploit path:

  • External entry selector 0x8c49b257 on dispatcher 0x71518580f36feceffe0721f06ba4703218cd7f63
  • Delegatecall to _createImports(bytes) (0x2babda4c) on 0xa045cf963b79833faf445f555ee1a6812d6fc87f
  • Delegatecall to proveImports(bytes) (0xa34ccc20) on 0x5e8060ecbf415aa25f12c1d67fde832bd87dcfa1
  • Delegatecall to processTransactions(bytes,uint256) (0xf419ee83) on 0x08f0fbcc068c70a29326094110769ee5f1d0107d

The payout is executed inside processTransactions(bytes,uint256), but the trace only proves that the bug sits earlier in the same entry flow: attacker-supplied import/proof material progressed into payout instead of being rejected before bridge funds were released. The exact failed predicate inside that verification path is still not source-validated in this local run.

Vulnerable Code

The following recovered snippets are not verified source. They are narrow pseudocode approximations derived from the trace and the decompiler artifacts in this analysis directory, and they preserve the call sequence that the validator can cross-check on-chain.

// Recovered by Decompiler Agent — NOT verified source code
function route_0x8c49b257(bytes calldata payload) external {
    bytes memory imports = IEntryModule(ENTRY_MODULE)._createImports(payload);
    bytes memory proved = IVerificationModule(VERIFICATION_MODULE).proveImports(imports);
    IPayoutModule(PAYOUT_MODULE).processTransactions(proved, 3);
}
// Recovered by Decompiler Agent — NOT verified source code
function proveImports(bytes memory imports) external pure returns (bytes memory proved) {
    // recovered approximation: helper loops over attacker data and returns a payout payload.
    // the exact failed predicate is unresolved in this local recovery.
    IHashHelper(HASH_HELPER).createHash(imports);
    IMmrHelper(MMR_HELPER).GetMMRProofIndex(0, 0, 0);
    proved = imports;
}

function processTransactions(bytes memory proved, uint256 count) external {
    TransferItem[] memory payouts = deserializeTransfers(proved, uint8(count));
    _payNative(payouts[0].to, payouts[0].amount);
    _payToken(payouts[1].token, payouts[1].to, payouts[1].amount);
    _payToken(payouts[2].token, payouts[2].to, payouts[2].amount);
}

Why It’s Vulnerable

Expected behavior: a bridge import path should only reach payout after it proves that the imported message is authentic, uniquely spendable, and mapped to a legitimate bridge transfer set. Before any ETH or ERC-20 transfer is executed, the bridge should reject duplicated proof elements, replayed imports, malformed transfer lists, and any payout not tied to an authorized inbound message.

Actual behavior: the trace shows the dispatcher accepts a large attacker-supplied payload, runs the entry and proof modules, and then enters processTransactions(bytes,uint256). Inside that function, the recovered payout logic and the trace both show three outbound transfers to the same drainer: 1,625.36688649 ETH, 103.56766017 tBTC, and 147,658.836798 USDC. There is no trace-visible rejection, rollback, or secondary authorization step between transfer decoding and payout execution.

What matters is the gap between those two states. The local trace supports a logic_error in the import/proof path because the bridge demonstrably treated attacker-submitted import material as spendable and converted it into outbound payouts from its own inventory. However, this local run still does not recover enough verified source detail to distinguish missing proof-spend validation from replay-accounting failure, malformed-message acceptance, transfer-list deserialization abuse, or another verification omission inside the same path.

Attack Execution

High-Level Flow

  1. The attacker EOA submitted a single call to the bridge dispatcher with a large serialized payload.
  2. The dispatcher routed that payload into the bridge’s BTC import path.
  3. The verification module performed repeated hashing and proof-index helper calls over the submitted data.
  4. After the verification phase completed, the bridge entered its payout module and decoded three transfers from the proved payload.
  5. The bridge sent native ETH directly to the drainer wallet.
  6. The bridge then transferred tBTC from bridge custody to the same drainer wallet.
  7. The bridge finally transferred USDC from bridge custody to the same drainer wallet.

Detailed Call Trace

The flow below is derived from trace_callTracer.json and selector-checked against cast sig results.

depth 0  CALL
  0x5abb91b9c01a5ed3ae762d32b236595b459d5777
    -> 0x71518580f36feceffe0721f06ba4703218cd7f63
       selector 0x8c49b257 (unresolved), value 0

depth 1  DELEGATECALL
  0x71518580f36feceffe0721f06ba4703218cd7f63
    -> 0xa045cf963b79833faf445f555ee1a6812d6fc87f
       selector 0x2babda4c `_createImports(bytes)`

depth 2  DELEGATECALL
  0x71518580f36feceffe0721f06ba4703218cd7f63
    -> 0x5e8060ecbf415aa25f12c1d67fde832bd87dcfa1
       selector 0xa34ccc20 `proveImports(bytes)`

depth 3/4  repeated verification helpers
  0x71518580f36feceffe0721f06ba4703218cd7f63
    -> 0x40ec84ca82fbf1b4d6a8edb02bffcf3eb0400aae
       selector 0x2cd0cff2 `createHash(bytes)` (21 delegatecalls total)
  0x71518580f36feceffe0721f06ba4703218cd7f63
    -> 0x918d6f7efe5a1707b83a2f0cf016cc5cf7983ffb
       selector 0x6d48bb8d `GetMMRProofIndex(uint64,uint64,uint8)` (5 delegatecalls total)

depth 2  DELEGATECALL
  0x71518580f36feceffe0721f06ba4703218cd7f63
    -> 0x08f0fbcc068c70a29326094110769ee5f1d0107d
       selector 0xf419ee83 `processTransactions(bytes,uint256)`

depth 3  STATICCALL
  0x71518580f36feceffe0721f06ba4703218cd7f63
    -> 0x796f4236c96e222b727df27978b3a77020356b88
       selector 0x9d56cf89 `deserializeTransfers(bytes,uint8)`

depth 3  CALL
  0x71518580f36feceffe0721f06ba4703218cd7f63
    -> 0x65cb8b128bf6e690761044cceca422bb239c25f9
       value 0x581c7f2bb3271b0400 = 1,625.36688649 ETH

depth 3  CALL
  0x71518580f36feceffe0721f06ba4703218cd7f63
    -> 0x18084fba666a33d37592fa2633fd49a74dd93a88
       selector 0xa9059cbb `transfer(address,uint256)`
       args: to `0x65cb8b128bf6e690761044cceca422bb239c25f9`, amount `103567660170000000000`

depth 3  CALL
  0x71518580f36feceffe0721f06ba4703218cd7f63
    -> 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
       selector 0xa9059cbb `transfer(address,uint256)`
       args: to `0x65cb8b128bf6e690761044cceca422bb239c25f9`, amount `147658836798`

Financial Impact

The bridge lost three assets in the exploit transaction:

  • 1,625.36688649 ETH
  • 103.56766017 tBTC
  • 147,658.836798 USDC

funds_flow.json correctly captured the ETH and tBTC legs, but its USDC decimal metadata is wrong because local token metadata lookup failed; the raw transfer amount 147658836798 must be interpreted with USDC’s canonical 6 decimals, not 18. The direct economic floor is therefore $147,658.84 from the USDC leg alone, with the true total materially higher once the ETH and tBTC legs are valued. The funds moved from bridge custody at 0x71518580f36feceffe0721f06ba4703218cd7f63 to drainer wallet 0x65cb8b128bf6e690761044cceca422bb239c25f9, so the loss appears to have hit protocol-held bridge liquidity rather than an intermediate AMM or lending pool.

No flash loan or repayment path appears anywhere in the trace. The bridge remained callable after the transaction, but this path drained a meaningful portion of immediately spendable bridge inventory and demonstrates that the proof-to-payout control boundary was broken.

Evidence

  • Transaction hash: 0x6990f01720f57fc515d0e976a0c4f8157e0a9529194c4c15d190e98d087eb321
  • Chain: Ethereum mainnet
  • Block number: 25118335
  • Block timestamp: 2026-05-17T23:55:23Z
  • Receipt status: 0x1
  • Attacker EOA: 0x5abb91b9c01a5ed3ae762d32b236595b459d5777
  • Vulnerable bridge dispatcher: 0x71518580f36feceffe0721f06ba4703218cd7f63
  • Entry module: 0xa045cf963b79833faf445f555ee1a6812d6fc87f
  • Verification module: 0x5e8060ecbf415aa25f12c1d67fde832bd87dcfa1
  • Payout module: 0x08f0fbcc068c70a29326094110769ee5f1d0107d
  • Hash helper: 0x40ec84ca82fbf1b4d6a8edb02bffcf3eb0400aae
  • Proof-index helper: 0x918d6f7efe5a1707b83a2f0cf016cc5cf7983ffb
  • Drainer wallet: 0x65cb8b128bf6e690761044cceca422bb239c25f9
  • ERC-20 transfer 1: tBTC token 0x18084fba666a33d37592fa2633fd49a74dd93a88, amount 103567660170000000000
  • ERC-20 transfer 2: USDC token 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48, amount 147658836798
  • Native ETH payout is visible in the call trace as a direct CALL with value 0x581c7f2bb3271b0400
  • Selector verification completed locally for _createImports(bytes), proveImports(bytes), processTransactions(bytes,uint256), deserializeTransfers(bytes,uint8), GetMMRProofIndex(uint64,uint64,uint8), createHash(bytes), transfer(address,uint256), and decimals()