Reproduced Exploit

Allbridge CCTP — Phantom Deposit on Base (~$191K)

Allbridge’s new CCTP router on Base treated a Circle-attested generic MessageTransmitterV2.sendMessage as a real USDC deposit. The attacker authored a burn-shaped body declaring 1,000,000 USDC, pointed recipient at their own harness (so Circle’s TokenMessenger never minted), and set destinationCall…

Aug 2026Basebridge5 min read

Loss

~$191,156 USDC (router emptied). Attacker net ~189,751.55 USDC after 10 bp fee + Aave premium

Chain

Base

Category

bridge

Date

Aug 2026

Source

Crypto Training

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Crypto Training original detection and analysis (live Twitter/X security-alert intake — not from DeFiHackLabs). Standalone Foundry PoC, offline anvil_state.json, and full write-up: 2026-08-AllbridgeCctpPhantomDeposit_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/bridge/message-validation · vuln/bridge/false-deposit · vuln/defi/flash-loan

Reproduction: Foundry fork replay at Base block 50157344 (one before the drain). Historical harness calldata redeems a Circle-attested generic sendMessage that minted 0 USDC, books a phantom 1M credit in CCTPTokenMessenger.receivedMessages, flash-loans the shortfall from Aave V3, and drains the router via Router.receiveToken. Offline anvil_state.json + _shared/run_poc.sh [PASS]. Basis: Defimon · DefimonAlerts.


Key info#

Loss~$191,156 USDC (router emptied). Attacker net ~189,751.55 USDC after 10 bp fee + Aave premium
ChainBase (chainId 8453)
ProtocolAllbridge Next — CCTP router path (no liquidity pool)
Date2026-08-19
Attacker EOA0x2419432344b0B892E592b2601B98eaE702Ba360e
Exploit harness0xb6fBDFA5F3CBEB139D4ccE86D92F4ac8687B16c0
Logic (initcode)0xe9edf1582ed9520f7149669d9c6bf3276b02477e (created in the attack tx)
Victim Router0xaA119F7442eCC28b9a8F236707ADA8362CFF24fF
Vulnerable messenger0xf9B710E427bf4D93598E0F80A84de22C7Ad9B577CCTPTokenMessenger
USDC (Base)0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Flash lenderAave V3 Pool 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5
Attack tx0x9f906fcd…e6a8 @ block 50157345
Forged sendMessagePolygon 0x2a88d797…2140 (2026-07-25)
Alerts / analysisDefimonAlerts · Defimon writeup
Bug classBridge message validation / false depositreceiveCctpMessage credits amount - feeExecuted from an attested CCTP body into receivedMessages[hookDataHash] without checking that USDC was minted or that balances increased

TL;DR#

Allbridge’s new CCTP router on Base treated a Circle-attested generic MessageTransmitterV2.sendMessage as a real USDC deposit. The attacker authored a burn-shaped body declaring 1,000,000 USDC, pointed recipient at their own harness (so Circle’s TokenMessenger never minted), and set destinationCaller to Allbridge’s messenger. Circle attested it on Polygon on July 25; 24 days later, six seconds after a legitimate ~191k USDC inflow landed in the router, the attacker redeemed the phantom credit, flash-loaned the shortfall from Aave, called Router.receiveToken, and walked with ~189.75k USDC.

Root cause is a single missing check in CCTPTokenMessenger.receiveCctpMessage:

SOLIDITY
receivedMessages[messageHash] = receivedAmount; // no balance / mint check

Secondary analysis (@SlowMist_Team)#

Bridge message validation / phantom CCTP deposit: receiveCctpMessage credits attacker-authored amount+messageHash without requiring sender=remote TokenMessenger, recipient=Circle TokenMessengerV2, or an observed USDC mint/balance increase

Source: https://x.com/SlowMist_Team/status/2090994186346790999

The vulnerable code (verbatim)#

1. Phantom credit after Circle receiveMessage (signature / message-validation bug)#

sources/CCTPTokenMessenger/CCTPTokenMessenger.solline 182:

SOLIDITY
function receiveCctpMessage(bytes calldata message, bytes calldata attestation) external {
    require(message.length >= 408, "Message too short");
    uint32 sourceDomain = uint32(bytes4(message[4:8]));
    bytes32 destCaller  = bytes32(message[108:140]);
    uint256 amount      = uint256(bytes32(message[216:248]));   // attacker-authored
    uint256 feeExecuted = uint256(bytes32(message[312:344]));
    bytes32 sourceSender = bytes32(message[248:280]);           // attacker-authored
    bytes32 messageHash  = bytes32(message[376:408]);           // attacker-authored hookData

    require(destCaller == bytes32(uint256(uint160(address(this)))), "Invalid destination caller");
    uint32 sourceChainId = domainToChainId[sourceDomain];
    require(sourceChainId != 0, "Unknown source domain");
    require(sourceSender == remoteTokenMessengers[sourceChainId], "Invalid remote sender");

    bool success = IMessageTransmitter(MESSAGE_TRANSMITTER).receiveMessage(message, attestation);
    require(success, "CCTP receiveMessage failed");

    uint256 receivedAmount = amount - feeExecuted;
    receivedMessages[messageHash] = receivedAmount; // @audit PHANTOM CREDIT — no USDC mint/balance check
    emit MessageReceived(messageHash);
}

Circle correctly asserts the message is authentic. It does not assert that a burn/mint occurred. Generic sendMessage attests arbitrary payloads with zero USDC movement. The sourceSender == remoteTokenMessengers[...] check is attacker-satisfiable: the body field is copied from the publicly readable mapping.

2. Router pays on the phantom credit#

sources/Router/Router.solreceiveToken only requires a nonzero messenger credit for the caller-supplied hash tuple:

SOLIDITY
bytes32 messageHash =
    _calculateMessageHash(_nonce, recipient, destinationToken, normalizedAmount, sourceChain, CHAIN_ID);
require(!usedMessages[messageHash], "Message already used");
uint256 receivedAmount = ITokenMessenger(tokenMessengersAddr).receivedTokenAmount(messageHash);
require(receivedAmount > 0, "Message not received"); // solvency == accounting entry
// ... fee, then IERC20(intermediaryToken).safeTransfer(recipientAddr, amountAfterFee);

The hash (nonce=12345, recipient=harness, USDC, normalizedAmount=1e15, sourceChain=12345, CHAIN_ID=9) equals 0xe15a0288…c52e, which was embedded as hookData in the forged body months earlier.


Attack flow#

  1. Jul 25 (Polygon): MessageTransmitterV2.sendMessage with burn-shaped body, amount=1e12, recipient=harness, destinationCaller=Allbridge messenger, messageSender=remoteTokenMessengers[5]. No burn. Circle attests.
  2. Aug 19 01:47:11 UTC (Base 50157342): Legitimate CCTP mint credits the router with ~191,112 USDC (total ~191,156).
  3. 01:47:17 (50157345): Attacker calls harness with initcode that:
    • constructor → receiveCctpMessage(message, attestation) (phantom 1M credit, 0 mint);
    • flashLoanSimple shortfall (~808,844 USDC) from Aave;
    • tops router to 1M;
    • Router.receiveToken(1e15, 12345, 12345, USDC, harness, …)999,000 USDC out;
    • repays Aave (+ premium).
  4. Net on harness: 189,751.554381 USDC. Router left with the 1,000 USDC fee accrual (later skimmed by a copycat).

PoC#

TEXT
cd audits/evm-hack-registry
bash _shared/run_poc.sh 2026-08-AllbridgeCctpPhantomDeposit_exp -vv
TestWhat it shows
testExploitExact historical harness calldata replay
testPhantomCredit_noMintCredit booked, messenger/router USDC unchanged
testStepByStepFlashLoanDrainExplicit Aave flash-loan + pranked receiveToken

Asserted profit band: 189k–191.156k USDC. Observed: 189751.554381 USDC.


Fix direction#

Credit must be conditional on an observed balance increase across receiveMessage (or require message.recipient to be Circle’s TokenMessengerV2). Unregistering messengers (Allbridge’s containment) closes payouts but does not patch receiveCctpMessage.


References#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.