Aztec Connect : un exploit ZKRollup siphonne 2,19 M$
Le 14 juin 2026, le contrat RollupProcessor d'Aztec Connect, aujourd'hui obsolète, a été exploité : RollupProcessor (0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455). L'attaquant a retiré environ 2,19 M$ d'actifs du pool L1 en une seule transaction atomique, en créant un écart de bord entre numRealTxs et decoded_slots.
Aztec Connect a été déprécié en mars 2024. Le contrat étant immuable, il est resté exposé tant qu'il conservait des actifs résiduels d'utilisateurs. L'analyse ci-dessous reconstitue l'attaque à partir du code source et des calldata on-chain.
Aperçu de l'attaque
Cause racine
La faille vient d'un décalage structurel entre (i) la plage des cycles de règlement L1 effectivement parcourue par RollupProcessorV3 et (ii) la plage engagée par le hachage des entrées publiques ZK (ZK public input hash). Les attaquants ont tiré parti de ce décalage pour faire engager, via preuves ZK, 31 des 32 slots d'entrées publiques dans la racine d'état L2, sans que ces slots passent par la validation de règlement côté contrat L1.
Decoder.sol : numRealTxs est entièrement piloté par l'attaquant
numRealTxs est lu depuis la calldata à l'offset 4516 sans contrainte on-chain. decoded_slots est arrondi au multiple supérieur de numTxsPerRollup afin de respecter le format de données attendu par le précompile SHA256. Cet arrondi crée une zone "gap" entre numRealTxs et decoded_slots, zone que l'attaquant peut remplir librement.
RollupProcessorV3.sol : le cycle de règlement ne couvre que numRealTxs slots
Le cycle de règlement L1 ne traite que numRealTxs slots.
Hypothèses de sécurité mises en défaut
En temps normal, chaque slot d'entrée publique est soit validé au niveau du contrat L1 (par exemple via la réduction de pendingDepositBalance lors d'un dépôt), soit contraint par le circuit ZK pour imposer publicValue == 0. Dans le scénario de la faille :
- Le précompile SHA256 couvre les 32 slots (testé avec 8192 bytes = 32 × 256 bytes) et le contenu des slots "gap" est bien engagé par preuve ZK.
- Le cycle de règlement L1 ne traite que le premier slot ; les slots "gap" [2..32] ne subissent aucune validation L1.
- La contrainte du circuit ZK sur publicValue des slots "gap" (censée imposer 0) a été contournée ou n'était pas appliquée.
Ces lignes de défense sont interdépendantes : si la contrainte ZK manque, la couche contrat L1 ne détecte pas non plus le problème.
Modèle de divergence "dual path"
Une même calldata est consommée par deux chemins aux bornes supérieures différentes : la ZK considère 32 slots, le L1 n'en considère qu'un. Ce désaccord sur "quels slots comptent" permet la création d'actifs ex nihilo.
Déroulé de l'attaque
La transaction d'attaque 0x074ec931…aee1 contient 14 appels à processRollup(), organisés en deux phases : "7 mint" puis "7 withdrawals", le tout exécuté atomiquement.
Phase 1 : mint sur L2 (Rollup #13277–13283, 7 fois)
1) L'EOA de l'attaquant 0x0f18d8b44a740272f0be4d08338d2b165b7edd17 appelle le contrat d'entrée maître 0x06f585f74e0da633ae813a0f23fb9900b61d0fcd, déclenchant le sélecteur 0x6f3ce701.
2) Ce contrat enchaîne trois contrats relais, chacun embarquant plusieurs calldatas de rollup malveillantes. Paramètres clés : numRealTxs = 1, rollupSize = 1024, numInnerRollups = 32.
- Slot 1 (visible L1) : proofId = 0 (noop), publicValue = 0
- Slots 2–32 (31 slots "gap" invisibles L1) : proofId = 1 (deposit), publicValue = N, publicOwner = adresse L2 de l'attaquant
Le tout accompagné de la preuve ZK correspondante (le circuit ne contraint pas publicValue des slots "gap" à 0).
3) Le contrat relais A appelle RollupProcessor.processRollup() en série (Rollup #13277–13281, 5 fois) :
- Le verificateur confirme la validité de la preuve ZK ; l'engagement SHA256 couvre 32 slots.
- Le cycle de règlement L1 s'arrête à 1 × TX_PUBLIC_INPUT_LENGTH, donc 1 slot, et ne traite que des noops.
- Les faux dépôts dans les slots [2..32] sont engagés dans une nouvelle racine Merkle ; le solde L2 de l'attaquant augmente de 5 × 31N.
4) Le contrat relais B traite Rollup #13282–13283 de la même manière (2 fois), ajoutant 2 × 31N.
À ce stade, le compte L2 de l'attaquant cumule 7 × 31N de dépôts non adossés, tandis que le coffre L1 reste inchangé.
Phase 2 : retraits vers L1 (Rollup #13284–13290, 7 fois)
L'attaquant convertit l'intégralité du solde L2 gonflé en actifs L1 via sept rollups de retrait :
- Rollup #13284 (DAI) : withdraw() → transfert direct de 270/513.054 DAI vers 0x0f18…edd17
- Rollup #13285 (wstETH) : transfert de 167.890 wstETH vers l'attaquant
- Rollup #13286 (yvDAI) : transfert de 4/873.857 yvDAI vers l'attaquant
- Rollup #13287 (yvWETH, prise en charge par le relais C) : transfert de 16.570 yvWETH vers l'attaquant
- Rollup #13288 (LUSD) : transfert de 9/273.734 LUSD vers l'attaquant
- Rollup #13289 (yvLUSD) : transfert de 359.047 yvLUSD vers l'attaquant
- Rollup #13290 (ETH, dernier) : RollupProcessor transfère 908.987 ETH via un CALL interne vers l'attaquant
La transaction atomique aboutit (gasUsed = 4/513/539) ; un rollback partiel au niveau du contrat est impossible. Le gain est estimé à environ 2,19 M$, prélevés sur le pool d'actifs légitimes des utilisateurs détenus par RollupProcessor.
Suivi des fonds
Selon le traçage forensique on-chain (au 15 juin 2026, environ un jour après l'incident) :
- Tous les actifs ont été transférés en une seule transaction depuis RollupProcessor, via le contrat d'attaque intermédiaire 0x06f585…d0fcD, directement vers l'EOA de l'attaquant 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17.
- Le contrat intermédiaire ne conserve aucun solde résiduel.
- Les fonds volés restent intacts à 100 % dans l'EOA de l'attaquant, sans activité de blanchiment identifiée à ce stade.
Conclusion et recommandations
Cet incident rappelle qu'un ZKRollup doit aligner strictement la borne supérieure de la boucle de règlement du contrat L1 avec l'ensemble des engagements pris sur les entrées publiques ZK. Lorsqu'un écart apparaît entre la borne numRealTxs côté L1 et decoded_slots côté engagement SHA256, un attaquant peut contourner toute hypothèse reposant sur le circuit ZK pour contraindre les slots "gap". Le L1 doit vérifier de façon indépendante chaque slot d'entrée publique engagé par la preuve ZK et ne peut pas déléguer cette responsabilité au circuit.
La SlowMist Security Team recommande un audit externe complet avant le déploiement d'un système de rollup, avec un focus sur la cohérence logique à la frontière d'état L1/L2, les limites de confiance dans le décodage de la calldata, et une vérification secondaire on-chain des entrées publiques ZK. Pour les contrats dépréciés conservant des actifs historiques, une migration ordonnée ou la destruction des actifs résiduels est conseillée afin de supprimer l'exposition persistante.
Ce rapport a été préparé par l'équipe Threat Intelligence de SlowMist, avec l'appui de MistEye Threat Intelligence System, de la plateforme de traçage MistTrack et d'analyses pilotées par SlowMist Agent AI. L'équipe indique rester disponible pour toute question ou retour.