Root Cause
Allbridge's CCTP (Cross-Chain Transfer Protocol) integration on Base accepted Circle attestations as proof that a legitimate USDC transfer had occurred. However, Circle's MessageTransmitter only validates the cryptographic signature of a message — it does not verify that an equivalent USDC burn happened on the source chain, who initiated the message, or whether the claimed amount matches an on-chain mint.
Allbridge's receiveCctpMessage function failed to independently validate any of these three economic properties:
// Vulnerable Allbridge CCTPTokenMessenger (simplified)
function receiveCctpMessage(
bytes calldata message,
bytes calldata attestation
) external {
// ❌ No check: msg.sender == Circle's canonical TokenMessenger
// ❌ No check: the declared amount matches an actual on-chain USDC mint
// ❌ No check: source domain + sender == expected burn address
// Circle's transmitter only validates that Circle signed the message:
bool success = IMessageTransmitter(transmitter)
.receiveMessage(message, attestation);
require(success, "CCTP receive failed");
// amount is attacker-controlled within the forged message
uint256 amount = _decodeAmount(message); // reads 1,000,000 USDC
_creditRouter(amount); // ← credits phantom $1M
}
The critical insight: MessageTransmitter.sendMessage on Polygon can be called directly by anyone. Circle will issue a valid attestation for any well-formed message that doesn't reference a TokenMessenger burn — because Circle's role is message relay, not economic verification.
Attack Steps
| # | Date | Action | Detail |
|---|---|---|---|
| 1 | Jul 26 | Forge CCTP message | Attacker calls MessageTransmitterV2.sendMessage directly on Polygon with src=Polygon, dst=Base, declared amount=1,000,000 USDC — no USDC burned |
| 2 | Jul 26 | Obtain Circle attestation | Circle issues valid attestation (it validates message format + signature, not economic reality) |
| 3 | Jul 26–Aug 18 | Wait | Attacker waits for Allbridge Base Router balance to accumulate enough legitimate USDC deposits |
| 4 | Aug 19 | Balance threshold reached | Base Router balance reaches ~191,156 USDC |
| 5 | Aug 19 (T+6 s) | Submit forged message | Calls Allbridge receiveCctpMessage with forged message + Circle attestation; protocol credits phantom $1M |
| 6 | Aug 19 | Flash loan + drain | Aave flash loan covers the difference between $1M phantom credit and actual balance; full 191,156 USDC drained |
Impact
- Loss: 191,156 USDC (~$191,156)
- Chain: Base (bridge from Polygon)
- Preparation window: ~24 days (July 26 → August 19)
- Protocol response: Allbridge urged all liquidity providers to withdraw; router suspended
- Root cause disclosure: SlowMist published a detailed post-mortem (Medium, August 2026)
Lessons for Auditors
- Circle attestation ≠ proof of USDC transfer. The CCTP attestation guarantees message authenticity, not economic validity. Bridge implementations must independently verify that: (a) the message was initiated by
TokenMessenger(notMessageTransmitterdirectly), (b) a USDC burn of the declared amount occurred on the source chain, and (c) thedestinationCallermatches expectations. - Validate message sender identity at the receiver. Check that
message.sender == TokenMessengeron the source chain, not just that Circle signed the outer envelope. - Cross-chain bridge state must account for timing attacks. The attacker waited 24 days for the router balance to reach exploitable levels. Bridges should cap per-message withdrawal amounts relative to verified inflows, not total balance.
- Flash loan composability amplifies bridge exploits. Even a small phantom credit becomes dangerous when combined with flash loans. Bridges should use reentrancy guards and balance-checks that are flash-loan-aware.
- Consider allowlisting valid CCTP message initiators at the application layer — only messages that provably originated from the canonical USDC
TokenMessenger(which enforces burns) should be credited.