Aztec 2.0 escapeHatch() old_data_root decoupling / unsound proof withdrawal (2026-06-17)

  • Name: Aztec 2.0 escapeHatch() old_data_root decoupling / unsound proof withdrawal
  • Date: 2026-06-17
  • Protocol: Aztec 2.0
  • Chain: Ethereum
  • Category: witness/public-input binding failure / unsound zk-proof circuit / deprecated-rollup withdrawal drain
  • Loss: about $2.2M in the primary drain on 2026-06-17, plus about $1.3k in residual ETH drained by a copycat on 2026-06-18
  • Summary: A deprecated Aztec 2.0 rollup contract still let users withdraw through escapeHatch() if they could present a valid ownership proof. The deployed escape-hatch verifier key encoded a flawed circuit that used old_data_root in two security-critical places without forcing those two uses to match. Aztec Labs’ own postmortem says the attacker’s accepted proof explicitly claimed deposit = 0 and withdraw = 1,158 ETH, while the cited funding notes were fabricated — an impossible combination for a legitimate exit. The only way that contradiction can pass is if note-membership checks ran against attacker-invented witness-side state while Solidity separately compared a different public-input root against the genuine live dataRoot. The verifier accepted that split statement, the contract believed the withdrawal was backed by the live ledger, and escapeHatch() released 1,158 ETH, 150,000 DAI, and 0.46963295 renBTC across three transactions. Roughly 14 hours later, a second actor used a different still-authorized path to sweep the final 0.7598 ETH dust.
  • Trigger / entry point:
    • The primary exploit entry point was escapeHatch() on the original Aztec 2.0 RollupProcessor contract at 0x737901bea3eeb88459df9ef1BE8fF3Ae1B42A2ba.
    • The escape-hatch verifier relied on a verification key generated from an old circuit that remained deployed onchain even after the codebase had removed that circuit.
    • The settlement contract separately required oldDataRoot == dataRoot onchain, but the proof system did not soundly bind the proof-side ownership check to that same root.
  • Root cause:
    • Aztec Labs said the verifier itself and the shared trusted setup were not compromised; the flaw sat in the circuit logic behind the verification key.
    • Aztec Labs’ leading hypothesis, consistent with third-party analysis, was that the escape-hatch circuit used old_data_root twice without constraining the two values to be equal.
    • One old_data_root instance fed note-membership checks inside the proof, while another appeared as a public input later compared in Solidity against the live onchain root.
    • That let the attacker satisfy membership proved and public root matches chain state independently instead of as one bound statement.
    • Because the settlement contract trusted the verifier to establish ownership, a proof accepted under that flawed key became direct withdrawal authority.

Three proof-statement promotions this incident collapsed

  1. Some proof-shaped state object existed

    • A verification key was live onchain, a public oldDataRoot input matched the contract’s current dataRoot, and a zero-knowledge proof could be generated that the deployed verifier accepted.
    • That only established the weakest fact: some proof-adjacent object passed under the old escape-hatch statement.
  2. That accepted object deserved to count as canonical ownership truth for the live Aztec ledger

    • The harder question was whether the same root the contract treated as canonical rollup state was also the root against which note ownership had actually been proven inside the circuit.
    • This was the broken promotion: the witness-side membership story and the public-input live-root story were allowed to diverge, so proof verified no longer meant these notes existed under the live ledger the contract is about to honor.
  3. That accepted ownership truth deserved immediate withdrawal authority against real assets

    • Once the contract flattened verifier success into payout authority, the accepted split statement could directly release ETH, DAI, and renBTC from the deprecated rollup.
    • The copycat follow-on mattered for the same reason one layer later: residual operator authority and dust balances were still treated as economically live after the main drain.

Healthy review gets materially better once teams stop flattening those three promotions into one vague moment called the escape-hatch proof verified.

  • Exploit path:
    1. On 2026-06-17 at 18:21 UTC, the attacker funded fresh wallet 0x6952d9246e9aFE8B887B2877225163436F78E97F with about 0.13 ETH for gas.
    2. They read the live rollup state and copied the genuine onchain Merkle root into the public-input side of an escapeHatch() proof so the contract’s oldDataRoot == dataRoot check would still pass.
    3. Separately, they built witness-side ownership against a fake ledger they controlled, with fabricated notes that justified deposit = 0 and withdraw = 1,158 ETH even though no legitimate deposit history supported that claim.
    4. Because the deployed verification key never forced the witness-side membership root to equal the public-input settlement root, the verifier accepted the proof as a valid exit for rollupId 4487.
    5. At 18:34 UTC, transaction 0xab306cd2184d23b6ba3e151b10b3b9a0b81f211cc16f4f3b0c79f0b17a59c2b5 drained 1,158 ETH.
    6. At 18:49 UTC, transaction 0x5c196c37a109d74c9797254287a0331f30e0daa637af241bd28fdc43774705c3 drained 150,000 DAI.
    7. At 18:50 UTC, transaction 0x9e1d6ab7c20ae235409d7dd3a9cd47c04f07293585b3498b8beed82d6f6b03ca drained the full 0.46963295 renBTC balance.
    8. The main exploit therefore completed in about 37 minutes from gas funding to final asset drain.
    9. On 2026-06-18 at about 08:16 UTC, separate copycat EOA 0xb6f89d12Ff9A9e394f40B701Ac46428DE0d1bEa5 used the normal operator path rather than escapeHatch() to sweep the residual 0.759804206229645062 ETH from rollupId 4490 in transaction 0x389bce3dad78c09900cb4c9e72397a73b39b8217ae37ef264f3439046737cb0c.
  • What the attacker needed to be true:
    • The deployed verification key had to keep accepting proofs generated under the flawed escape-hatch circuit.
    • The proof-side note-membership root and the Solidity-checked public root had to remain independently satisfiable.
    • escapeHatch() had to trust verifier success as sufficient ownership evidence and release assets without any second independent provenance check.
    • The deprecated Aztec 2.0 contract had to remain funded and callable even though Aztec Labs no longer retained admin or upgrade authority.
    • For the copycat sweep, an operator role also had to remain reachable through the decommissioned system’s remaining permissions.
  • Technical anatomy worth remembering:
    • This was not the trusted setup was broken or the verifier accepted arbitrary nonsense. The dangerous part was subtler: the verification key encoded the wrong statement.
    • Aztec Labs says re-running the verifier confirms that arbitrary proof tampering still causes rejection. The contradiction was narrower and more useful: this specific forged statement passed because the key still represented an underbound circuit.
    • The exploit lived in a duplicated semantic value. old_data_root mattered both as the live ledger root the contract trusted and as the root against which note ownership was supposed to be proven.
    • Once those two uses drifted apart, the attacker could prove ownership against invented state while still satisfying the contract’s separate live-root check.
    • The codebase had already removed the vulnerable circuit years earlier, but the deployed verifier still accepted proofs generated from that old circuit because the onchain key had not changed. Aztec’s incident writeup and third-party analysis both tie that stale live statement to the old escape-hatch circuit later removed in PR #402 / commit 8c3953a4d79e3130508389ff4a55a5dfd2fb3c49.
    • The copycat follow-up sharpened the lifecycle lesson twice over: even after the main drain, a deprecated rollup with residual permissions and dust balances still offered a second exploit surface, and that second surface existed because a default Anvil/Hardhat developer test account had been left registered as an authorized operator during the 2024 decommissioning process.
  • Representative transactions and addresses:
    • Vulnerable Aztec 2.0 RollupProcessor: 0x737901bea3eeb88459df9ef1BE8fF3Ae1B42A2ba
    • Primary attacker EOA: 0x6952d9246e9aFE8B887B2877225163436F78E97F
    • Attacker holding wallet A: 0x15930A0fEF3421F48c6553b5691682CC1b22EDb3 (300 ETH, per Aztec Labs appendix)
    • Attacker holding wallet B: 0x33d6A0D9BC210E823e043D604179cd844eb467df (56 ETH, per Aztec Labs appendix)
    • Primary exploit txs:
      • 0xab306cd2184d23b6ba3e151b10b3b9a0b81f211cc16f4f3b0c79f0b17a59c2b5 (1,158 ETH, rollupId 4487)
      • 0x5c196c37a109d74c9797254287a0331f30e0daa637af241bd28fdc43774705c3 (150,000 DAI)
      • 0x9e1d6ab7c20ae235409d7dd3a9cd47c04f07293585b3498b8beed82d6f6b03ca (0.46963295 renBTC)
    • Copycat EOA: 0xb6f89d12Ff9A9e394f40B701Ac46428DE0d1bEa5
    • Copycat sweep tx: 0x389bce3dad78c09900cb4c9e72397a73b39b8217ae37ef264f3439046737cb0c (rollupId 4490)
    • 2024 renounce tx cited by Aztec Labs: 0x72c42759d18dc452a94de9599a975c2800e58e8e832df4c9e7ecfa6d86928194
    • Vulnerable circuit removal commit cited publicly after the incident: 8c3953a4d79e3130508389ff4a55a5dfd2fb3c49
  • Response / mitigation:
    • Aztec Labs stated that Aztec 2.0 had been deprecated in 2022 and that all administrative and upgrade authority had been renounced onchain in April 2024 after a year of withdrawal notice.
    • Because the contract was immutable and no admin keys remained, there was no patch, pause, or fund-recovery control available to stop the main drain in progress.
    • Aztec Labs emphasized that the live Aztec Network and current proving systems were separate and unaffected.
    • After the incident, Aztec Labs worked with incident-response partner Groom Lake to try to block funds at exchanges, but the contract itself was simply emptied rather than repaired.

Pattern tags

Why the exploit worked

  • Aztec 2.0’s escape-hatch verifier did not prove I own notes under the same root the contract is about to honor; it proved two weaker half-claims that the attacker could satisfy separately.
  • The attacker therefore did not need a forged settlement-consumer mismatch of the Aztec Connect kind. They only needed one underbound proof statement: witness-side note membership could be checked against an attacker-invented ledger while the public-input root still matched the genuine onchain dataRoot.
  • Because escapeHatch() deliberately delegated ownership reasoning to the verifier, once the verification key accepted that split statement the Solidity contract had no second provenance test left. proof verified had already become pay the withdrawal.
  • The stale-verifier lifecycle problem mattered just as much as the circuit bug itself: the vulnerable escape-hatch circuit had been removed from source years earlier, but the old verification key still defined live withdrawal truth for a funded immutable contract.
  • The copycat sweep sharpened the retirement lesson again. Even after the main drain, residual balances and a leftover operator path still meant the deprecated system retained real authority rather than a narrowly monitored exit-only surface.

Detection and audit pivots

  • Ask the exact statement question, not only the verifier question: does the proof force witness-side membership state, public-input state, and contract-side state to describe the same root, index, and withdrawal object?
  • Diff every duplicated semantic value inside the circuit. If a field like old_data_root appears once for membership checks and again as a public input, prove there is an explicit equality constraint rather than an assumed one.
  • Reconstruct one impossible-but-accepted statement from accepted public inputs the way Aztec Labs did: if the proof says deposit = 0 and withdraw = 1,158 ETH, where exactly was the backing state supposed to have existed?
  • Keep a live inventory of deployed verification keys and the statements they still authorize, even after source code, docs, or products move on. Removed from the repo is not the same thing as cannot still move funds onchain.
  • Audit decommissioned zk systems as residual custody systems, not as abandoned code. Enumerate every old verifier, escape hatch, operator role, and remaining balance that can still turn a proof or privileged call into real asset movement.
  • When comparing Aztec-family incidents, ask the boundary-setting question directly: did the proof statement itself already split one security object into separately satisfiable pieces, or did a sound proof later get settled against a different economic effect set? This incident belongs to the first branch; proof-to-effects binding failure belongs to the second.

Failure-prevention checklist

  • Treat duplicated semantic roots as one proof object. If old_data_root or any similar ledger commitment appears in both witness-side membership logic and public settlement inputs, force literal equality inside the circuit rather than relying on developers to thread matching values through two paths.
  • Make ownership proof prove one complete withdrawal story: same authenticated root, same note set, same consumed-nullifier story, and same asset-out effect. A contract should never need to infer that those pieces probably referred to the same ledger.
  • Keep legacy verification keys on a live authority inventory. A circuit removed from source is not retired if an old onchain key still turns accepted proofs into withdrawals.
  • Narrow deprecated systems to true exit-only surfaces before attention decays. If a legacy rollup must stay open for withdrawals, remove sibling operator, admin, migration, or maintenance paths that can still move value under broader semantics.
  • Add impossible-statement regression tests drawn from the real exploit shape. A healthy escape-hatch review suite should fail any proof attempt that combines deposit = 0 with a positive withdrawal against fabricated notes while the public settlement root remains genuine.
  • Review every decommissioning runbook for leftover privileged identities, including test keys and convenience operators. The copycat sweep showed that even dust balances become real authority incidents when a residual operator path survives retirement.
  • Treat residual balances as live custody until proven otherwise. Draining most funds is not enough if a deprecated verifier or operator route can still release the remainder.
  • Require one artifact that maps each live verifier to the exact statement it proves, the balances it can still reach, and the contracts or roles that still trust it.

Audit prompts

  • Where exactly is the equality constraint that forces witness-side ownership state and contract-side settlement state to use the same root rather than two variables with similar names?
  • Can you hold the public root fixed to the genuine onchain dataRoot, substitute a fake witness-side ledger, and still generate a proof that the live contract accepts?
  • Does the verification key prove these notes existed under this live root and authorize this withdrawal, or does it only prove weaker fragments that downstream Solidity later stitches together into payout authority?
  • Which legacy verification keys, escape hatches, or operator paths still authorize withdrawals today, and what balances remain reachable through each one?
  • During decommissioning, which residual roles were intentionally kept, and were any test or default operator identities accidentally left authoritative?
  • If the repository removed a circuit years ago, what evidence proves the old onchain key no longer matters economically?
  • Can reviewers reconstruct an impossible accepted statement from real public inputs and note fields, the way Aztec Labs did with deposit = 0 and withdraw = 1,158 ETH, and then point to the exact missing binding constraint?
  • Is the deprecated system truly exit-only, or do residual helper paths still let operators, relayers, or privileged accounts move value under semantics broader than user proves their own withdrawal against the live ledger?

Lessons that should already be standard

  • A verifier can be cryptographically healthy and still be economically unsound if the statement encoded in its key splits one security object into separately satisfiable public and witness stories.
  • Removed from the codebase is not a security boundary. Deployed verification keys and legacy escape hatches continue to define live truth until someone inventories and economically retires them.
  • Deprecated immutable systems are still custody systems if they retain funded withdrawal paths or residual privileged roles. The label legacy does not narrow authority by itself.
  • One missing equality over a ledger root is enough to turn prove I own this note into pay me from the real contract anyway.
  • Residual dust still deserves discipline. Small leftover balances and forgotten operator identities are exactly where copycat follow-on drains appear after the headline exploit.
  • The right review question is not only does the proof verify? but what exact world did the proof verify, and is it the same world the contract is about to honor with assets?

Why this incident matters

  • It is a crisp example of a zk system failing because the proof statement itself was underbound, not because of stolen keys, a broken trusted setup, or a later settlement-layer reinterpretation.
  • It preserves an important recurring lesson for proof-driven withdrawal systems: if a value matters both as public state and as witness-side membership context, those uses must be bound as one statement or the verifier may prove the wrong world.
  • It is also the useful contrast case beside [[aztec-connect-processrollup-numrealtxs-proof-settlement-boundary-drain-2026-06-14|Aztec Connect processRollup()]]: both incidents drained deprecated Aztec systems, but here the proof statement itself was weaker than reviewers thought, whereas Aztec Connect’s sharper failure was that a valid proof and the later settlement consumer disagreed about the economically real effect set.
  • It also reinforces the lifecycle lesson from other legacy incidents: deprecated immutable systems remain exploitable as long as they still custody withdrawable value or retain meaningful permissions.
  • The sharper retirement boundary here is legacy withdrawal obligation still exists versus legacy rollup still exposes broad live authority. Aztec 2.0 kept the old escape-hatch verifier and a residual operator permission economically live long after the product was considered retired.

Internal linkages

  • Core recurring pattern: witness-public-input-binding-failure.
  • Direct statement-binding companion: proof-state-object-unification-and-indexed-binding-discipline — the sharper proof-design question here is how the public settlement root and witness-side membership root should have been forced into one canonical state object before any withdrawal authority existed.
  • Live-verifier inventory companion: verification-key-statement-inventory-and-asset-reachability-discipline — the sharper positive-design question one layer out is how the old escape-hatch key stayed legible as live authority, what statement it still proved, and what balances and residual rights it could still reach.
  • Adjacent same-protocol contrast: [[aztec-connect-processrollup-numrealtxs-proof-settlement-boundary-drain-2026-06-14|Aztec Connect processRollup()]] — the clean boundary-setting companion because that exploit preserved a valid proof whose later L1 accounting and withdrawal settlement consumed a narrower effect set, whereas this incident broke one step earlier when the proof statement itself split live root from witness-side ownership root.
  • Adjacent proof-consumer family: proof-to-effects-binding-failure — use this when the proof may be sound but the economically settled effect set later drifts; Aztec 2.0 belongs on the other side of that boundary because the verifier key itself still authorized the wrong statement.
  • Adjacent proof-authenticity family: proof-to-message-binding-failure — similar trusted root detached from claimed object intuition, but that note centers on bridge-style message membership rather than zk withdrawal circuits that decouple public inputs from witness-side state.
  • Lifecycle frame: deprecation-without-deauthorization — Aztec 2.0 had been socially and operationally retired, yet the old verifier and residual permissions still controlled real value.
  • Positive-design decommissioning companion: decommissioning-authority-extinguishment-discipline — the broader retirement lesson is that immutable legacy systems still need an explicit authority-extinguishment inventory covering old verifier keys, residual operator roles, and any balances that remain economically reachable.
  • Residual-service companion: exit-only-residual-surface-discipline — this is the narrower positive-design question for what an old escape-hatch withdrawal service should have looked like once the rest of the product was gone but legacy exits still remained.

Control surface