Contexto

El 18 de junio de 2026, el contrato RollupProcessor del protocolo de privacidad Layer2 de Ethereum, Aztec Connect, fue atacado. El atacante aprovechó su mecanismo de Escape Hatch para explotar una falla en el límite de confianza debido a la falta de verificación de la propiedad y los límites de retiro de fondos en la capa de Solidity, así como la ausencia de restricciones en los circuitos relacionados. Presentó una prueba de escape hatch aceptada por TurboVerifier y retiró directamente 1,158 ETH (aproximadamente 2.06 millones de dólares) del saldo del contrato. Además, mediante el mismo mecanismo, robó 150,000 DAI y 0.47 renBTC, lo que resultó en una pérdida total de aproximadamente 2.22 millones de dólares.

Resumen del ataque

Transacción de ataque:

Causa raíz de la vulnerabilidad

Falta del límite de confianza de escapeHatch()

La función de entrada pública escapeHatch() de RollupProcessor solo comprueba si el estado de escape hatch está habilitado y luego entra directamente en processRollupProof(), sin ejecutar ninguna comprobación de permisos del llamante:

En cambio, en la ruta normal de procesamiento de rollup, processRollup() valida la autorización de rollupProviders[provider] y verifica la firma del provider. En este ataque, se llama a escapeHatch() pasando signatures = 0x y viewingKeys = 0x, lo que elude por completo el flujo de autorización del provider.

verifyProofAndUpdateState() confía ciegamente en el Verifier

Después de entrar en processRollupProof(), el contrato llama a TurboVerifier.verify(proofData, 0):

El propio TurboVerifier se comporta correctamente: realiza la verificación matemática del proof de Plonk. El verdadero problema es que RollupProcessor equipara el éxito de la verificación matemática con la legalidad del negocio y no realiza una validación independiente adicional en la capa Solidity.

processDepositsAndWithdrawals() ejecuta los retiros sin condiciones

Lógica de ejecución principal después de que Verifier devuelve éxito:

Función final de salida de fondos:

El circuito zkSNARK carece de puertas de restricción de igualdad (causa raíz)

En el circuito de prueba join-split de Aztec, old_data_root se divide en dos rutas independientes usando:

  • Testigo interno A: se pasa el subcircuito join-split para verificar la pertenencia Merkle de las notas privadas

  • Entrada pública B: se expone como entrada pública a la capa de Solidity, para que validateMerkleRoots() la compare con el dataRoot en la cadena

En el circuito falta la restricción A == B, lo que permite asignarlas de forma independiente:

  • A puede ser una raíz Merkle falsificada que construyó el atacante (la cual contiene notas por cualquier monto dentro del árbol)

  • B puede ser un dataRoot real en la cadena (para que la comprobación de Solidity pase)

Debido a que el sistema de restricciones permite A ≠ B, todo el proof de zkSNARK sigue siendo válido.

Flujo de ataque

Fase de preparación: falsificar el árbol Merkle y el proof

El atacante construye localmente un árbol Merkle de datos falsificado e inserta una nota privada dentro:

  • Valor: 1158 ETH (el contrato tenía aproximadamente 1158.7598 ETH en ese momento)

  • Propietario: el atacante controla la clave privada correspondiente

El atacante construye un proof zkSNARK:

  • Testigo interno A (old_data_root) = raíz de árbol falsificada

  • Entrada pública B (old_data_root) = dataRoot real en la cadena = 0x184bea7d9493cd9a5efb6b679d04066a8c92a34ac8ec150e9635133c6010977b

Primera fase: construir la transacción de ataque

El atacante EOA 0x6952d9246e9aFE8B887B2877225163436F78E97F llama directamente al contrato de nivel superior RollupProcessor (0x737901bea3eeb88459df9ef1be8ff3ae1b42a2ba):

  • Firma de la función: escapeHatch(bytes,bytes,bytes)

  • msg.value = 0

  • signatures = 0x

  • viewingKeys = 0x

El proofData construido por el atacante incluye entradas públicas clave:

Segunda fase: eludir la comprobación de permisos

escapeHatch() solo comprueba que getEscapeHatchStatus() devuelva true y luego llama directamente a processRollupProof(). Esta ruta no ejecuta la comprobación de autorización de rollupProviders[provider] en el processRollup() normal, ni verifica la firma del provider. No existe ninguna vinculación entre la identidad del llamante y el destinatario del retiro.

Tercera fase: verificación matemática del Verifier

RollupProcessor realiza STATICCALL a TurboVerifier(0x48cb7ba00d087541dc8e2b3738f80fdd1fee8ce8):

Como rollup_size = 0, TurboVerifier selecciona EscapeHatchVk mediante VerificationKeys.getKeyById(0) (vk.num_inputs = 26) para realizar la verificación matemática del proof de Plonk. El Verifier solo verifica la consistencia matemática entre el proof y las public inputs, no lee el estado del RollupProcessor, ni comprueba la asignación de saldos.

En esta transacción, la llamada del verifier devuelve éxito; esto indica que el proof de escape hatch enviado por el atacante es correcto matemáticamente.

Cuarta fase: ejecutar el retiro

Después de que Verifier devuelva éxito, RollupProcessor actualiza el estado del rollup:

Luego entra en processDepositsAndWithdrawals(); en modo escape hatch, aunque rollupSize == 0, todavía se procesa 1 inner tx. Después de analizar proofData, se descubre que:

Llama directamente a withdraw(1158 ETH, dirección del atacante, 0) y transfiere el dinero mediante receiverAddress.call{value: 1158 ETH}("") .

Quinta fase: ataque con el mismo patrón contra DAI y renBTC

El atacante, durante el mismo periodo, utilizó exactamente la misma ruta de vulnerabilidad y construyó proofs de escape hatch dirigidos a activos ERC20 (assetId ≠ 0), robando 150,000 DAI y 0.47 renBTC del RollupProcessor. En la función withdraw(), la rama cuando assetId ≠ 0 ejecuta el transfer() de ERC20 para completar la transferencia.

Beneficio neto de las tres operaciones de ataque:

Seguimiento de fondos

Mediante el sistema de seguimiento antilavado de SlowMist MistTrack, se analizó al atacante EOA 0x6952d9246e9aFE8B887B2877225163436F78E97F:

Origen de fondos: el gas inicial usado para que el atacante creara la dirección proviene de 0x963737c550e70ffe4d59464542a28604edb2ef9a (dirección de la entidad de unionchain.ai). Esta dirección se activó por primera vez el 18 de junio de 2026 a las 02:21:11 UTC, justo en el momento en que se iniciaron las acciones de ataque. Pertenece a una dirección única creada específicamente para este ataque.

Destino de los fondos: los fondos robados se distribuyen actualmente de la siguiente manera:

  • Los tokens de 802 ETH aún permanecen en la dirección principal del atacante 0x6952d9246e9aFE8B887B2877225163436F78E97F; 150,000 DAI y 0.47 renBTC tampoco se han transferido

  • 300 ETH se han transferido a 0x15930a0fef3421f48c6553b5691682cc1b22edb3 (MistTrack lo marca como una dirección maliciosa asociada a Aztec Exploiter)

  • Aproximadamente 56 ETH se han transferido a otra dirección maliciosa (también marcada como asociada a Aztec Exploiter)

  • El address 0x33d6a0d9bc210e823e043d604179cd844eb467df tiene una puntuación de riesgo AML de 100/100 (Severe); también está relacionado con el evento de Aztec Exploiter

Resumen

La lección central de este ataque es: "matemáticamente correcto" en el circuito de zkSNARK ≠ "seguro en el negocio". El verificador solo puede demostrar que "sabes un testigo que cumple las restricciones"; si las restricciones en sí son incorrectas (o están ausentes), el sistema de pruebas se convierte en una máquina de falsificación legítima. El equipo de seguridad de SlowMist recomienda que el proyecto realice una auditoría especializada de todas las rutas de restricciones de pruebas de conocimiento cero para asegurar la seguridad del circuito.

Este artículo fue redactado por el equipo de inteligencia de amenazas de SlowMist, combinando el sistema de inteligencia de amenazas MistEye, la plataforma de seguimiento MistTrack y el análisis impulsado por IA de SlowMist Agent. Si tienes cualquier problema, no dudes en consultarnos y darnos tu feedback.