Allbridge's $191K CCTP Exploit: How a 24-Day-Old Forged Circle Message and a Flash Loan Drained a Base Router
An attacker constructed a forged Circle CCTP-style message on Polygon on July 25–26, waited 24 days for Allbridge's Base router to accumulate funds, then executed the exploit six seconds after 191,156 USDC arrived. Allbridge's receiveCctpMessage function treated Circle's message attestation as settlement proof — crediting a fictitious 999,000 USDC deposit — while a flash loan from Aave filled the gap to make the payout possible. Net loss: approximately 191,156 USDC.
This article is published for educational and informational purposes only. It does not constitute legal, financial, or investment advice. Nothing in this article should be relied upon as the sole basis for any decision relating to the security, investment value, or legal standing of any protocol or digital asset.
Information published on any code vulnerabilities must not be used to attack, test, or probe any protocol or system without explicit authorisation from its owner. Use of this content is subject to our Terms of Service and Privacy Policy.
What happened
The Allbridge exploit of August 19, 2026 was not an impulsive attack. It was a premeditated, patient operation: the attacker constructed the core instrument of the exploit on July 25–26, held it for 24 days, and executed it precisely six seconds after a legitimate transfer funded Allbridge's Base CCTP router with enough USDC to make the attack worthwhile. The loss was approximately 191,156 USDC — the entire real balance of the router at the moment of execution.
The instrument was a forged message. Using Circle's generic sendMessage() function on Polygon, the attacker constructed a CCTP-style message that declared a transfer of 1,000,000 USDC without burning a single dollar on the source chain. They submitted it to Circle's attestation service, which confirmed the message's authenticity — because the message was structurally valid. Circle's attestation does not verify that USDC was burned. It verifies that the message was signed by an authorised sender. With a valid Circle attestation in hand, the attacker waited.
At block 50157342 (01:47:11 UTC, August 19), a legitimate user transferred 191,156 USDC into the Allbridge Base CCTP router. At block 50157345 (01:47:17 UTC — six seconds later), the attacker deployed their exploit contract and submitted the 24-day-old forged message to Allbridge's receiveCctpMessage function. The router verified Circle's attestation, found it valid, and credited the attacker with a fictitious 999,000 USDC balance (1,000,000 less the 0.1% protocol fee).
The router's real balance — 191,156 USDC — was insufficient to pay out 999,000 USDC. The attacker covered the gap with a flash loan of 808,844 USDC from Aave, deposited into the router to make it solvent against the fictitious credit, then withdrew 999,000 USDC, repaid Aave 809,248 USDC (principal plus 404 USDC premium), and netted approximately 189,752 USDC. The entire execution sequence — deploy, relay forged message, flash borrow, drain, repay — occurred atomically.
Twenty-five minutes later, a second party replicated the exploit and drained the remaining ~1,000 USDC from the router across two transactions, demonstrating that the vulnerability was self-describing once the first attack was visible on-chain. The two events together account for the ~191,156 USDC total loss figure.
Root cause
The vulnerability was in Allbridge's receiveCctpMessage function on Base — the entry point through which the CCTP router processes inbound cross-chain transfer messages. The function conflated two distinct properties of a Circle-attested message: its authenticity and its settlement proof. They are not the same thing, and treating them as equivalent is the precise flaw the attacker exploited.
What Circle attestation proves — and what it does not
Circle's Cross-Chain Transfer Protocol works by having the source-chain TokenMessenger burn USDC and emit a message. That message is submitted to Circle's off-chain attestation service, which signs it if it was emitted by an authorised sender. The destination-chain contract verifies the attestation signature before processing the message.
Critically: Circle attests to the message, not to the burn. The attestation confirms that the message was signed by an authorised sender in the correct format. It does not independently verify that USDC was destroyed on the source chain, that the declared amount matches any on-chain burn event, or that USDC was subsequently minted on the destination chain. A message submitted through sendMessage() — the generic, non-transfer message function — receives the same class of attestation as a genuine depositForBurn message, because the attestation service evaluates message structure and sender authorisation, not economic settlement.
The attacker exploited this by submitting a sendMessage() call on Polygon with the following fields crafted to pass Allbridge's validation:
{
messageSender: remoteTokenMessengers[5], // copied from on-chain readable mapping
recipient: attackerContract, // not Circle's TokenMessenger
amount: 1_000_000_000_000, // 1,000,000 USDC (6 decimals)
hookData: precomputedHash, // crafted to match Allbridge's
// internal message hash calculation
// No USDC burned on Polygon.
// Circle's attestation service signed this because the message was
// structurally valid and sender-authorised — not because USDC moved.
}The receiveCctpMessage validation gap
When the attacker submitted this message plus its Circle attestation to Allbridge's receiveCctpMessage on Base, the function followed this logic:
function receiveCctpMessage(
bytes calldata message,
bytes calldata attestation
) external {
// Step 1: Verify Circle's attestation signature — passes.
// The message is structurally valid and signed by an authorised sender.
circleMessageTransmitter.receiveMessage(message, attestation);
// Step 2: Decode message fields.
(address sender, address recipient, uint256 amount, bytes32 hookData)
= decodeCctpMessage(message);
// [BUG 1]: sender is not validated against the expected TokenMessenger.
// The attacker used remoteTokenMessengers[5], readable on-chain.
// No check: require(sender == trustedTokenMessenger, "invalid sender");
// [BUG 2]: recipient is not validated — it is the attacker's contract,
// not Circle's TokenMessenger on Base.
// No check: require(recipient == address(this), "invalid recipient");
// [BUG 3]: No verification that USDC was actually minted into the router.
// No pre/post balance check, no token transfer observed.
// The function credits the declared amount unconditionally.
// [BUG 4]: hookData is accepted as authoritative without independent
// verification that the declared amount matches any on-chain event.
// Router books a 1,000,000 USDC credit for the message originator.
_creditDeposit(sender, amount); // fictitious 1M USDC credit recorded
}The flash loan as a gap-filler
With a fictitious 999,000 USDC credit booked in the router (1,000,000 less the 0.1% fee), the attacker needed the router to be liquid enough to pay it out. The real balance was 191,156 USDC — a shortfall of 807,844 USDC. The Aave flash loan of 808,844 USDC filled that gap. The attacker deposited the flash-borrowed USDC into the router in the same transaction, making the router solvent against the fictitious credit, then immediately withdrew 999,000 USDC and repaid Aave. The flash loan was not the attack vector; it was the accounting tool that made an insufficiently funded router pay out a credit larger than its real balance:
// All steps in one atomic transaction:
// 1. Deploy exploit contract.
ExploitContract exploit = new ExploitContract();
// 2. Submit 24-day-old forged message + Circle attestation.
// Router books fictitious 999,000 USDC credit.
allbridge.receiveCctpMessage(forgedMessage, circleAttestation);
// 3. Flash-borrow the gap from Aave.
aave.flashLoan(exploit, USDC, 808_844e6);
// Inside the flash loan callback:
// 4. Deposit flash-borrowed USDC into router (router now holds ~1M USDC).
allbridge.deposit(USDC, 808_844e6);
// 5. Withdraw against the 999,000 USDC credit.
allbridge.withdraw(USDC, 999_000e6); // router pays out 999,000 USDC
// 6. Repay Aave: 808,844 + 404 (0.05% premium) = 809,248 USDC.
USDC.transfer(aave, 809_248e6);
// Net: 999,000 - 809,248 = 189,752 USDC profit.
// Router balance: 0 USDC (191,156 real USDC gone).What could have been done
- Verify the message sender against a trusted TokenMessenger allowlist. The
messageSenderfield in a genuine CCTP transfer will always be Circle'sTokenMessengercontract on the source chain — not an arbitrary address. The attacker populated this field with the value ofremoteTokenMessengers[5], which is publicly readable on-chain but which the receiving router should have independently verified as the expected sender for a valid deposit message. A one-line check —require(sender == trustedRemoteTokenMessenger)— would have rejected the forged message before any credit was booked. - Verify the intended recipient is the router itself. In a genuine CCTP transfer into an Allbridge router, the
recipientfield should be the router's own address, as USDC is minted to the router before being allocated to the depositor. The attacker's message specified their exploit contract as recipient. Checking thatrecipient == address(this)before processing a deposit credit would have caught this immediately. - Verify actual USDC balance increase, not declared amount. The most direct control is a pre- and post-balance check around the Circle message relay call. Record the router's USDC balance before invoking
circleMessageTransmitter.receiveMessage(); after the call, compare the balance to the pre-call snapshot. Credit the depositor only with the observed difference — not the amount declared in the message. This check is immune to the attestation ambiguity entirely: it measures what actually arrived, not what was claimed. If no USDC arrives, no credit is booked. - Distinguish depositForBurn messages from generic sendMessage calls. Circle's CCTP protocol defines a specific message type identifier for
depositForBurntransfers. Allbridge's router should have verified that the incoming message was of this type — not a genericsendMessagepayload that happens to be attested. A type-check on the message body before processing the deposit would have filtered out the forged message at the classification stage, before any of the downstream validation logic was reached. - Monitor router inflows and respond to large credits in real time. A 1,000,000 USDC credit booked into a router that held 191,156 USDC is an anomaly with no legitimate explanation. On-chain monitoring that flags credits exceeding the router's observable balance — or credits whose declared amount is not matched by a corresponding USDC mint event in the same block — would have produced an alert before the withdrawal was executed.
- Apply a withdrawal delay for credits that arrive via CCTP messages. A mandatory delay of even one block between a CCTP credit being booked and it becoming withdrawable would have been sufficient to allow an automated monitor to flag the anomaly and trigger a pause. The attacker's flash loan only works within a single atomic transaction; a withdrawal delay breaks atomicity and eliminates the flash loan component entirely.
Lessons for the industry
The Allbridge exploit illustrates a precise and important distinction that any protocol integrating Circle's CCTP must internalise: a Circle-attested message is evidence that a message is authentic. It is not evidence that USDC moved. Circle's attestation service signs messages submitted by authorised senders in valid formats — including messages sent through the generic sendMessage() function, which does not require any USDC to be burned. Any protocol that treats attestation as settlement proof is building on a misreading of what CCTP guarantees, and that misreading is directly exploitable.
The 24-day premeditation period is the most operationally significant aspect of this incident. The attacker constructed and submitted the forged message to Circle on July 25–26, received its attestation, and then waited — watching the Base router for a deposit large enough to make the exploit economically meaningful. The attack was not triggered by a code change or an unusual network event. It was triggered by the appearance of 191,156 USDC in a router that the attacker had already identified as vulnerable and pre-loaded with the instrument to drain it. The six-second gap between the legitimate deposit and the exploit transaction is consistent with automated monitoring: the attacker's system was watching router inflows and triggered the exploit on the first block after the target threshold was reached.
The copycat attack 25 minutes later — which drained the remaining ~1,000 USDC in two transactions — is a separate lesson. Once the first exploit transaction was on-chain, the vulnerability was self-describing: anyone who read the transaction could reconstruct the technique and reuse it. A protocol that has been exploited and not immediately paused is offering an open invitation to secondary attackers. Automated pause mechanisms triggered by anomalous drain events — not dependent on human review of an on-chain transaction — should be the standard response posture for any router or bridge handling user funds.
The role of the flash loan is worth examining as a distinct consideration. Flash loans did not cause this exploit; the missing message validation did. But flash loans expanded the blast radius by allowing the attacker to drain a router balance larger than the fictitious credit's face value, using borrowed liquidity to fill the gap and repaying within the same block. Protocols that credit deposits based on declared amounts rather than observed balance increases are exposed to flash loan amplification of any credit-recording flaw. The correct fix — verify actual balance increase — removes both the fundamental vulnerability and its flash loan amplifier simultaneously.
For protocols integrating CCTP or any other cross-chain messaging standard, the general principle is: never trust declared amounts in messages as direct evidence of asset settlement. Message authenticity and asset settlement are separately verifiable properties. Attestation proves the former; a balance observation proves the latter. Both checks are necessary, and the balance check is the stronger one — it remains valid regardless of what an attacker does to message fields, sender spoofing, or attestation-compatible payloads. Credit what you can measure, not what was declared.
- 01SlowMist on X — Allbridge attack breakdown — forged message, flash loan, missing validation
- 02Defimon — Allbridge $191K 'phantom CCTP deposit' exploit — receiveCctpMessage analysis
- 03Coffiasse on X — Circle's generic sendMessage used to produce CCTP-looking message
- 04KuCoin — Allbridge cross-chain bridge hacked after month-long setup
- 05KuCoin / SlowMist — Fake CCTP messages, flash loans, and insufficient minting verification
- 06Binance Square — Month-long cross-chain attack — forged message fields and remediation
- 07Binance Square — Allbridge loses approximately $190,000
- 08Defimon Alerts on X — Base CCTP router targeted — $191K loss