Contexte

Le 18 juin 2026, le contrat RollupProcessor du protocole de confidentialité Layer2 d'Ethereum, Aztec Connect, a été attaqué. L'attaquant a exploité son mécanisme Escape Hatch pour profiter d'un défaut de confiance lié à l'absence de vérification indépendante des fonds et des plafonds de retrait au niveau de la couche Solidity, ainsi que des contraintes de circuit manquantes. Il a soumis une preuve d'escape hatch acceptée par TurboVerifier et a directement retiré 1 158 ETH (environ 2,06 millions de dollars) du solde du contrat. Par ailleurs, il a volé 150 000 DAI et 0,47 renBTC par le même mécanisme, pour une perte totale d'environ 2,22 millions de dollars.

Aperçu de l’attaque

Transaction d’attaque :

Cause racine de la vulnérabilité

Absence de limite de confiance pour escapeHatch()

La fonction d’entrée publique escapeHatch() de RollupProcessor ne vérifie qu’un paramètre : si le statut d’escape hatch est activé, puis entre directement dans processRollupProof(), sans effectuer aucune vérification des autorisations de l’appelant :

En revanche, dans le traitement standard d’un rollup, processRollup() vérifie l’autorisation rollupProviders[provider] et valide la signature du provider. Dans cette attaque, l’appel se fait à escapeHatch(), avec signatures = 0x, viewingKeys = 0x, contournant entièrement le flux d’autorisation du provider.

verifyProofAndUpdateState() fait confiance à l’aveugle au Verifier

Après être entré dans processRollupProof(), le contrat appelle TurboVerifier.verify(proofData, 0) :

Le comportement de TurboVerifier lui-même est correct : il effectue la vérification mathématique de la preuve Plonk. Le vrai problème est que RollupProcessor assimile la réussite de la vérification mathématique à la légalité métier, sans effectuer de contrôle indépendant supplémentaire au niveau Solidity.

processDepositsAndWithdrawals() exécute le retrait sans condition

Logique d’exécution principale après le succès du Verifier :

Fonction finale de sortie des fonds :

Le circuit zkSNARK manque de portes de contraintes d’égalité (cause fondamentale)

Dans le circuit de preuve Aztec join-split, old_data_root est scindé en deux chemins indépendants :

  • Témoin interne A : transmis au sous-circuit join-split, utilisé pour vérifier l’appartenance Merkle des notes privées

  • Entrée publique B : exposée comme entrée publique à la couche Solidity, pour comparer validateMerkleRoots() avec le dataRoot on-chain

Dans le circuit, la contrainte manquante A == B permet aux deux éléments d’être assignés indépendamment :

  • A peut être une racine Merkle fabriquée par l’attaquant (la arborescence contient des notes de tout montant)

  • B peut être une dataRoot réelle on-chain (pour faire passer le contrôle Solidity)

Comme le système de contraintes permet A ≠ B, l’ensemble de la preuve zkSNARK reste valide.

Flux d’attaque

Étape de préparation : falsifier l’arbre Merkle et la preuve

L’attaquant construit localement un arbre Merkle de données falsifié, insérant une note privée :

  • Valeur : 1158 ETH (environ 1158,7598 ETH détenus par le contrat à l’époque)

  • Propriétaire : l’attaquant détient la clé privée correspondante

L’attaquant construit une preuve zkSNARK :

  • Témoin interne A (old_data_root) = racine d’arbre falsifiée

  • Entrée publique B (old_data_root) = dataRoot réelle on-chain = 0x184bea7d9493cd9a5efb6b679d04066a8c92a34ac8ec150e9635133c6010977b

Première étape : construire une transaction d’attaque

L’attaquant EOA 0x6952d9246e9aFE8B887B2877225163436F78E97F initie directement un appel de niveau supérieur vers RollupProcessor (0x737901bea3eeb88459df9ef1be8ff3ae1b42a2ba) :

  • Signature de la fonction : escapeHatch(bytes,bytes,bytes)

  • msg.value = 0

  • signatures = 0x

  • viewingKeys = 0x

Les proofData construites par l’attaquant contiennent les entrées publiques clés :

Deuxième étape : contourner le contrôle d’autorisations

escapeHatch() ne vérifie que que getEscapeHatchStatus() renvoie true, puis appelle directement processRollupProof(). Ce chemin n’exécute pas les vérifications d’autorisation du tableau rollupProviders[provider] dans le processRollup() normal, et ne valide pas non plus la signature du provider. Il n’y a aucun lien entre l’identité de l’appelant et le bénéficiaire du retrait.

Troisième étape : vérification mathématique par le Verifier

RollupProcessor lance un STATICCALL vers TurboVerifier (0x48cb7ba00d087541dc8e2b3738f80fdd1fee8ce8) :

Comme rollup_size = 0, TurboVerifier choisit EscapeHatchVk via VerificationKeys.getKeyById(0) (vk.num_inputs = 26) pour effectuer la vérification mathématique Plonk de la preuve. Le Verifier vérifie uniquement la cohérence mathématique entre la proof et les public inputs, sans lire l’état de RollupProcessor, et sans vérifier l’attribution des soldes.

Dans cette transaction, le retour de l’appel à verifier réussit ; cela indique que la preuve d’escape hatch soumise par l’attaquant est correcte mathématiquement.

Quatrième étape : exécuter le retrait

Après le retour de succès du Verifier, RollupProcessor met à jour l’état du rollup :

Puis, entrée dans processDepositsAndWithdrawals() : en mode escape hatch, même si rollupSize == 0, il traite toujours 1 inner tx. Après analyse de proofData, il apparaît que :

Appel direct à withdraw(1158 ETH, adresse de l’attaquant, 0) ; en utilisant receiverAddress.call{value: 1158 ETH} ("") pour transférer les fonds.

Cinquième étape : attaque selon le même modèle contre DAI et renBTC

Durant la même période, l’attaquant a utilisé exactement le même chemin de vulnérabilité et a construit des preuves d’escape hatch ciblant respectivement un actif ERC20 (assetId ≠ 0), afin de voler 150 000 DAI et 0,47 renBTC depuis RollupProcessor. Dans la fonction withdraw(), la branche assetId ≠ 0 exécute le transfert ERC20 via transfer() pour effectuer le transfert.

Bénéfice net des trois attaques :

Traçage des fonds

Analyse de l’attaquant EOA 0x6952d9246e9aFE8B887B2877225163436F78E97F via le système de suivi anti-blanchiment MistTrack (Threat Intelligence) :

Origine des fonds : le gas initial utilisé par l’adresse créée par l’attaquant provient de 0x963737c550e70ffe4d59464542a28604edb2ef9a (adresse de l’entité unionchain.ai). Cette adresse a été activée pour la première fois le 18 juin 2026 à 02:21:11 UTC, au moment exact où l’action d’attaque a démarré ; il s’agit d’une adresse à usage unique créée spécifiquement pour cette attaque.

Destinations des fonds : la répartition actuelle des fonds volés est la suivante :

  • Les jetons de 802 ETH restent encore sur l’adresse principale de l’attaquant 0x6952d9246e9aFE8B887B2877225163436F78E97F ; 150 000 DAI et 0,47 renBTC n’ont également pas encore été transférés

  • 300 ETH ont été transférés vers 0x15930a0fef3421f48c6553b5691682cc1b22edb3 (MistTrack marque comme adresse malveillante associée à un Aztec Exploiter)

  • Environ 56 ETH ont été transférés vers une autre adresse malveillante (marquée également comme associée à Aztec Exploiter)

  • Le score de risque AML de 0x33d6a0d9bc210e823e043d604179cd844eb467df est de 100/100 (Severe), impliquant également l’événement Aztec Exploiter

Résumé

Le leçon centrale de cette attaque est la suivante : le « caractère mathématiquement correct » d’un circuit zkSNARK n’est pas équivalent à la « sécurité métier ». Le vérificateur ne peut prouver que « vous connaissez un témoin qui satisfait les contraintes » ; si les contraintes elles-mêmes sont erronées (ou manquantes), le système de preuve devient une machine de falsification légitime. L’équipe de sécurité SlowMist recommande aux équipes projet d’auditer spécifiquement tous les chemins de contraintes des preuves zero-knowledge afin d’assurer la sécurité du circuit.

Cet article est rédigé par l’équipe Threat Intelligence de SlowMist, en combinant le système MistEye Threat Intelligence, la plateforme de suivi MistTrack et l’analyse pilotée par l’IA de SlowMist Agent. Pour toute question, n’hésitez pas à nous contacter et à nous faire part de votre retour.