Aztec Connect Hacked for $2.19M via ZK-Rollup Vulnerability

iconMetaEra
Share
Share IconShare IconShare IconShare IconShare IconShare IconCopy
AI summary iconSummary

expand icon
On June 14, 2026, Aztec Connect suffered a $2.19M exploit due to a zk-rollup vulnerability in the deprecated RollupProcessor contract (0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455). Attackers manipulated a boundary gap between numRealTxs and decoded_slots within a single atomic transaction, draining the L1 pool. The flaw stemmed from a misalignment between L1 settlement and zk-SNARKs public input hash commitments, enabling 31 of 32 slots to bypass validation. Through a two-phase attack involving 14 processRollup() calls, funds were minted and withdrawn. As of June 15, 2026, all stolen assets remained in the attacker’s externally owned account.

Context

On June 14, 2026, the deprecated Aztec Connect RollupProcessor contract (0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455) was exploited. The attacker extracted approximately $2.19 million in assets from the L1 pool in a single atomic transaction by crafting a boundary gap between numRealTxs and decoded_slots. Aztec Connect was deprecated in March 2024, but this immutable contract remained exposed due to holding residual user assets. This article reconstructs the full technical details of the attack from both the contract source code and on-chain calldata.

Attack Overview

Root cause of the vulnerability

There is a structural gap between the range of L1 settlement cycles traversed by RollupProcessorV3 and the range committed to by the ZK public input hash. Attackers exploited this gap to have the contents of 31 out of 32 public input slots committed into the L2 state root via ZK proofs, without undergoing any settlement validation on the L1 contract layer.

Decoder.sol: numRealTxs is entirely controlled by the attacker

numRealTxs is read from calldata offset 4516 with no on-chain constraints:

The decoded_slots are rounded up to the nearest multiple of numTxsPerRollup, as required by the SHA256 precompile's data layout. However, this rounding creates a gap region between numRealTxs and decoded_slots that an attacker can freely fill.

RollupProcessorV3.sol: The settlement cycle covers only numRealTxs slots.

Breakdown of security assumptions

The normal security assumption is that each public input slot is either validated at the L1 contract layer (by reducing pendingDepositBalance upon deposit) or constrained by the ZK circuit such that publicValue == 0. However, in this vulnerability scenario:

  1. The SHA256 precompile covers all 32 slots (tested with an input of 8192 bytes = 32 × 256 bytes), and the contents of the gap slots have been committed to via ZK proof.
  2. The L1 settlement cycle only processes the first slot; gap slots [2..32] are not subject to any L1-level validation.
  3. The ZK circuit's constraint on the gap slot publicValue (which should be 0) was bypassed or not enforced by the attacker.

The three lines of defense are interdependent, but none can independently provide security for the gap slot—when ZK circuit constraints are missing, the L1 contract layer also fails to detect it.

Dual-path divergence model

The same calldata is consumed by two paths with different upper bounds:

The discrepancy in understanding of "which slots count" (ZK recognizes 32, L1 recognizes only 1) is the root cause of minting out of thin air.

Attack flow

The attack transaction 0x074ec931…aee1 contains 14 processRollup() calls, structured in a two-phase pattern of "first 7 mints followed by 7 withdrawals," all executed within a single atomic transaction.

Phase 1: Minting — Acquiring assets out of thin air on L2 (Rollup #13277–13283, 7 times)

1. The attacker EOA 0x0f18d8b44a740272f0be4d08338d2b165b7edd17 called the master control entry contract 0x06f585f74e0da633ae813a0f23fb9900b61d0fcd, triggering the selector 0x6f3ce701.

2. The master contract sequentially invokes three relay contracts, each of which has hard-coded several malicious rollup calldatas. Key parameters of each calldata:

  • 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
  • Accompanied by the corresponding ZK proof (the circuit does not constrain the gap slot publicValue to 0)

3. Relay Contract A sequentially calls RollupProcessor.processRollup() (Rollup #13277–13281, 5 times):

  • Verifier confirms ZK proof passed — SHA256 commitment covers all 32 slots
  • L1 settlement cycle ends at 1 × TX_PUBLIC_INPUT_LENGTH = 1 slot, processing only no-ops.
  • Fake deposits in the gap slots [2..32] are ZK-committed into a new Merkle root → attacker's L2 balance increases by 5 × 31N

4. Relay Contract B processes Rollup #13282–13283 in the same manner (twice), granting the attacker an additional 2 × 31N to their L2 balance. At this point, the attacker’s L2 account has accumulated a total of 7 × 31N in unsupported deposits, while the L1 vault remains unchanged.

Phase Two: Withdrawal — Convert L2 inflated balances into L1 assets (Rollup #13284–13290, 7 times)

The attacker exchanged their entire L2 balance, obtained during the minting phase, for L1 assets through seven withdrawal rollups:

  1. Rollup #13284 (DAI): withdraw() → RollupProcessor directly transfers 270,513.054 DAI to 0x0f18…edd17
  2. Rollup #13285 (wstETH): Transfer 167.890 wstETH → Attacker
  3. Rollup #13286 (yvDAI): Transfer 4,873.857 yvDAI → Attacker
  4. Rollup #13287 (yvWETH, relay contract C takes over): Transfer 16.570 yvWETH → Attacker
  5. Rollup #13288 (LUSD): Transfer 9,273.734 LUSD → Attacker
  6. Rollup #13289 (yvLUSD): Transfer 359.047 yvLUSD → Attacker
  7. Rollup #13290 (ETH, last transaction): RollupProcessor transferred 908.987 ETH via internal CALL → attacker.

A single atomic transaction executed successfully (gasUsed = 4,513,539), and contract-level partial rollback is not possible. The attacker netted approximately $2.19M, all drawn from the legitimate user asset pool of RollupProcessor.

Fund tracking

Based on on-chain forensic tracking (as of June 15, 2026, approximately one day after the incident), the status of the stolen funds is as follows:

All assets were transferred in a single transaction from RollupProcessor through the intermediary attack contract 0x06f585…d0fcD directly to the attacker’s EOA 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17. The intermediary contract held no residual funds.

The stolen funds remain 100% intact and untouched in the attacker's EOA, with no money laundering activity yet initiated.

Summary

The key lesson from this attack is that the upper bound of the ZK-Rollup contract’s settlement loop must be strictly aligned with the range of commitments to ZK public inputs. When a gap exists between the loop boundary numRealTxs at the L1 contract layer and the decoded_slots of the SHA256 commitment, any security assumption relying on the ZK circuit to enforce constraints on gap slots can be bypassed by an attacker—L1 must independently verify every public input slot committed to by the ZK proof, and cannot outsource this verification responsibility to the circuit layer.

The SlowMist Security Team recommends that project teams conduct a comprehensive external security audit before deploying a Rollup system, with a focus on logical consistency at the L1/L2 state boundary, trusted boundaries for calldata decoding, and on-chain secondary verification of ZK public inputs. For contracts that have been deprecated but still hold legacy assets, it is recommended to execute an orderly asset migration or destruction to eliminate ongoing exposure risks.

This article was prepared by SlowMist’s Threat Intelligence Team, leveraging the MistEye Threat Intelligence System, MistTrack Tracking Platform, and SlowMist Agent AI-driven analysis. Feel free to reach out with any questions or feedback.

Disclaimer: The information on this page may have been obtained from third parties and does not necessarily reflect the views or opinions of KuCoin. This content is provided for general informational purposes only, without any representation or warranty of any kind, nor shall it be construed as financial or investment advice. KuCoin shall not be liable for any errors or omissions, or for any outcomes resulting from the use of this information. Investments in digital assets can be risky. Please carefully evaluate the risks of a product and your risk tolerance based on your own financial circumstances. For more information, please refer to our Terms of Use and Risk Disclosure.