Aztec Connect exploit drains $2.19M after attacker abuses ZK rollup public-input gap
On June 14, 2026, an attacker exploited the deprecated Aztec Connect RollupProcessor contract (0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455), draining about $2.19 million from the L1 pool in a single atomic transaction. Although Aztec Connect was deprecated in March 2024, the immutable contract still held residual user assets and remained exposed.
SlowMist’s Threat Intelligence Team reconstructed the incident using contract source code and onchain calldata. The core issue was a mismatch between what the L1 settlement loop processed and what the ZK proof’s public-input hash committed to: RollupProcessorV3 traversed only the first numRealTxs slots, while the SHA256 commitment covered 32 public input slots. By engineering a boundary gap between numRealTxs and decoded_slots, the attacker committed values for 31 of 32 slots into the L2 state root via ZK proofs without those slots undergoing L1 settlement validation.
Key mechanics
- Decoder.sol: numRealTxs is read from calldata at offset 4516 without onchain constraints. decoded_slots is rounded up to a multiple of numTxsPerRollup to match the SHA256 precompile layout, creating a gap region the attacker can fill.
- RollupProcessorV3.sol: the settlement cycle only covers numRealTxs slots.
This broke the usual security model where each public input slot is either validated on L1 (e.g., pendingDepositBalance reductions) or constrained in the ZK circuit so that publicValue == 0. In the exploited configuration, the SHA256 precompile still committed all 32 slots (8192 bytes = 32 × 256 bytes), but the L1 settlement processed only the first slot. The attacker also bypassed or leveraged missing ZK circuit constraints that should have forced gap-slot publicValue values to zero.
SlowMist described the failure as a dual-path divergence: the same calldata was consumed by two paths with different upper bounds, with ZK recognizing 32 slots while L1 recognized only 1. That discrepancy enabled assets to be minted “out of thin air” on L2 and then withdrawn from the legitimate L1 vault.
Attack flow and transaction details
The attack transaction 0x074ec931…aee1 executed 14 processRollup() calls in a two-phase pattern: seven minting rollups followed by seven withdrawal rollups, all within one atomic transaction (gasUsed = 4,513,539).
Phase 1 — Minting on L2 (Rollup #13277–13283, 7 times)
1) Attacker EOA 0x0f18d8b44a740272f0be4d08338d2b165b7edd17 called the master entry contract 0x06f585f74e0da633ae813a0f23fb9900b61d0fcd using selector 0x6f3ce701.
2) The master contract invoked three relay contracts with hardcoded malicious rollup calldatas. Each payload used:
- numRealTxs = 1
- rollupSize = 1024
- numInnerRollups = 32
Slot 1 (L1-visible): proofId = 0 (noop), publicValue = 0
Slots 2–32 (31 gap slots, L1-invisible): proofId = 1 (deposit), publicValue = N, publicOwner = attacker’s L2 address
These were accompanied by ZK proofs where the circuit did not constrain gap-slot publicValue to 0.
3) Relay contract A called RollupProcessor.processRollup() five times (Rollup #13277–13281). The verifier accepted the ZK proofs and the SHA256 commitment covered all 32 slots, but the L1 settlement loop ended after 1 × TX_PUBLIC_INPUT_LENGTH, processing only the noops. The fake deposits in slots [2..32] were committed into the new Merkle root, increasing the attacker’s L2 balance by 5 × 31N.
4) Relay contract B repeated the same approach twice (Rollup #13282–13283), adding 2 × 31N more. After seven rollups, the attacker had accumulated 7 × 31N in unsupported L2 deposits while the L1 vault remained unchanged.
Phase 2 — Withdrawing to L1 (Rollup #13284–13290, 7 times)
The attacker converted the inflated L2 balance into L1 assets via seven withdrawal rollups:
- Rollup #13284 (DAI): withdraw() transferred 270,513.054 DAI to 0x0f18…edd17
- Rollup #13285 (wstETH): 167.890 wstETH to attacker
- Rollup #13286 (yvDAI): 4,873.857 yvDAI to attacker
- Rollup #13287 (yvWETH, relay contract C): 16.570 yvWETH to attacker
- Rollup #13288 (LUSD): 9,273.734 LUSD to attacker
- Rollup #13289 (yvLUSD): 359.047 yvLUSD to attacker
- Rollup #13290 (ETH): 908.987 ETH transferred via internal CALL to attacker
Because the entire sequence executed atomically, contract-level partial rollback was not possible.
Fund tracking (as of June 15, 2026)
SlowMist’s onchain tracking found that all assets flowed in a single transaction from RollupProcessor through the intermediary attack contract 0x06f585…d0fcD directly to the attacker EOA 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17. The intermediary contract retained no balance. The stolen funds reportedly remained intact in the attacker EOA, with no laundering activity observed at that time.
Takeaways and recommendations
SlowMist said the incident underscores the need to align the L1 settlement loop’s upper bound with the exact range of ZK public inputs being committed. If a gap exists between numRealTxs on L1 and decoded_slots in the SHA256 commitment, attackers can abuse any missing circuit constraints on gap slots, and L1 must not rely on the circuit to enforce those constraints. Projects are advised to conduct comprehensive external audits before deploying rollup systems, focusing on L1/L2 boundary logic, trusted calldata decoding boundaries, and onchain secondary verification of ZK public inputs. For deprecated contracts still holding legacy assets, SlowMist recommended orderly migration or destruction of assets to eliminate ongoing exposure.
The report was prepared by SlowMist’s Threat Intelligence Team using the MistEye Threat Intelligence System, MistTrack Tracking Platform, and SlowMist Agent AI-driven analysis.