# Aztec Connect: unconstrained proof_id in escape hatch mints notes

Aztec escape hatch circuit publishes unconstrained inner proof_id, letting a prover mint a private note from public_input while Solidity skips deposit validation, then withdraw DAI.

- Payout: Real-world hack
- Severity: critical
- Program: Aztec Escape Hatch
- Credited: ivanbogatyy
- Disclosed: 2026-06-22
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-06/AztecEscapeHatch_exp2.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260622-aztecescapehatch-exp2
- Tags: domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained technique:source-review vuln:business-logic vuln:crypto

**Target:** RollupProcessorV2 escapeHatch / escape_hatch_circuit
**Root cause:** escape_hatch_circuit.cpp publishes the inner proof_id via public_witness_ct(&composer, 0), which makes it public but does not constrain it to 0. A prover can publish proof_id=1 while proving a join-split with public_input>0, so RollupProcessor.processDepositsAndWithdrawals skips the proofId==0 deposit validation/debit.
**Steps:**
1. Fork mainnet at block 25,295,800 where escape hatch is open (200 blocks remaining).
2. Craft escape proof #1: rollup_size=0, proof_id=1, public_input=150_000e18, public_output=0, asset_id=1 (DAI), output owner = attacker.
3. Call `rollup.escapeHatch(fakeDepositProof, "", "")` — Solidity skips deposit debit because proofId!=0, minting a 150k DAI private note.
4. Craft escape proof #2: proof_id=0, public_input=0, public_output=150_000e18, output owner = receiver.
5. Call `rollup.escapeHatch(withdrawProof, "", "")` to withdraw 150,000 DAI to receiver.
**How found:** Code review of escape_hatch_circuit.cpp: noticed public_witness_ct(&composer, 0) only exposes proof_id publicly without constraining it to 0, and cross-referenced RollupProcessor's proofId==0 gate on deposit processing.
**Exploitation:** During an escape-hatch-open window, submit a first escape proof publishing proof_id=1 with public_input=150k DAI to mint a private note without a matching pending deposit, then a second escape proof spending that note to withdraw real DAI from the RollupProcessor vault.
**Insight:** In ZK circuits, public_witness_ct only exposes a value — it does not constrain it. Audit every public input that gates Solidity-side accounting (proof_id, asset_id, rollup_size) for missing equality constraints, especially in escape-hatch/emergency paths that bypass normal validation.
