Aztec Connect sufre un robo de 2,19 millones de dólares por un fallo en su ZK-Rollup

El 14 de junio de 2026 se explotó el contrato RollupProcessor de Aztec Connect, ya obsoleto, en la dirección 0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455. El atacante extrajo en una sola transacción atómica cerca de 2,19 millones de dólares desde el pool de L1, aprovechando un "hueco de frontera" entre numRealTxs y decoded_slots. Aunque Aztec Connect se retiró en marzo de 2024, el contrato es inmutable y seguía expuesto al conservar activos residuales de usuarios. Qué falló: desalineación entre el bucle de liquidación en L1 y los compromisos ZK La vulnerabilidad nace de una discrepancia estructural: el rango de ciclos de liquidación en L1 que recorre RollupProcessorV3 no coincide con el rango de entradas públicas comprometidas por el hash de entrada pública ZK. Ese desajuste permitió que 31 de 32 "public input slots" quedaran comprometidos dentro del estado L2 mediante pruebas ZK, sin pasar por validación de liquidación en la capa de contrato L1. - Decoder.sol: numRealTxs queda bajo control total del atacante. numRealTxs se lee del calldata en el offset 4516 sin restricciones onchain. Para ajustarse al formato de datos del precompile SHA256, decoded_slots se redondea al múltiplo más cercano de numTxsPerRollup. Ese redondeo crea una región de hueco entre numRealTxs y decoded_slots que el atacante puede rellenar. - RollupProcessorV3.sol: el ciclo de liquidación solo cubre numRealTxs slots. Supuestos de seguridad que se rompieron El modelo normal asume que cada public input slot o bien se valida en L1 (por ejemplo, reduciendo pendingDepositBalance al depositar) o queda restringido en el circuito ZK para que publicValue == 0. En este incidente: - El precompile SHA256 abarca los 32 slots (se comprobó con 8192 bytes = 32 × 256 bytes), y el contenido de los slots del hueco quedó comprometido mediante prueba ZK. - El ciclo de liquidación en L1 solo procesó el primer slot; los slots del hueco [2..32] no recibieron validación en L1. - La restricción del circuito ZK sobre publicValue en los slots del hueco (que debería ser 0) fue evitada o no se aplicó. En la práctica, dos rutas consumían el mismo calldata con límites superiores distintos: para ZK contaban 32 slots, para L1 solo 1. Esa divergencia permitió "acuñar" valor sin respaldo. Cómo se ejecutó el ataque La transacción 0x074ec931…aee1 incluyó 14 llamadas a processRollup() organizadas en dos fases: primero 7 "mints" y después 7 retiradas, todo dentro de una única transacción atómica. Fase 1: "Minting" (Rollup #13277–13283, 7 veces) 1) El EOA 0x0f18d8b44a740272f0be4d08338d2b165b7edd17 invocó el contrato de control maestro 0x06f585f74e0da633ae813a0f23fb9900b61d0fcd, activando el selector 0x6f3ce701. 2) El contrato maestro llamó en cadena a tres contratos relay, cada uno con calldatas maliciosos preconfigurados. Parámetros clave: - numRealTxs = 1, rollupSize = 1024, numInnerRollups = 32 - Slot 1 (visible en L1): proofId = 0 (noop), publicValue = 0 - Slots 2–32 (31 slots del hueco, invisibles para L1): proofId = 1 (deposit), publicValue = N, publicOwner = dirección L2 del atacante - Se adjuntó la prueba ZK correspondiente (el circuito no restringía publicValue del hueco a 0) 3) El Relay A ejecutó RollupProcessor.processRollup() en secuencia (Rollup #13277–13281, 5 veces). El verificador aceptó la prueba ZK y el compromiso SHA256 cubrió los 32 slots, pero el ciclo de liquidación en L1 se detuvo en 1 × TX_PUBLIC_INPUT_LENGTH = 1 slot, procesando solo noops. Los depósitos falsos en [2..32] quedaron comprometidos en una nueva raíz de Merkle, aumentando el saldo L2 del atacante en 5 × 31N. 4) El Relay B repitió el patrón para Rollup #13282–13283 (2 veces), sumando 2 × 31N. Tras esta fase, el atacante acumuló 7 × 31N en depósitos no respaldados en L2, mientras la bóveda en L1 no cambiaba. Fase 2: retiradas (Rollup #13284–13290, 7 veces) El atacante convirtió el saldo inflado en L2 en activos reales de L1 a través de siete rollups de retirada: - Rollup #13284 (DAI): withdraw() → 270.513,054 DAI a 0x0f18…edd17 - Rollup #13285 (wstETH): 167,890 wstETH al atacante - Rollup #13286 (yvDAI): 4.873,857 yvDAI al atacante - Rollup #13287 (yvWETH, toma el relevo el relay C): 16,570 yvWETH al atacante - Rollup #13288 (LUSD): 9.273,734 LUSD al atacante - Rollup #13289 (yvLUSD): 359,047 yvLUSD al atacante - Rollup #13290 (ETH): RollupProcessor transfirió 908,987 ETH mediante CALL interno al atacante La transacción atómica se completó (gasUsed = 4.513.539) y, a nivel de contrato, no era posible un rollback parcial. El beneficio neto fue de aproximadamente 2,19 millones de dólares, extraídos del pool legítimo de activos de usuario en RollupProcessor. Rastreo de fondos Según el seguimiento forense onchain (a 15 de junio de 2026, cerca de un día después): - Todos los activos se movieron en una única transacción desde RollupProcessor, pasando por el contrato intermediario 0x06f585…d0fcD, directamente al EOA del atacante 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17. - El contrato intermediario no retuvo fondos. - Los fondos robados permanecen íntegros en el EOA del atacante, sin señales de blanqueo iniciadas. Conclusión y recomendaciones La lección central es que el límite superior del bucle de liquidación del contrato ZK-Rollup debe quedar estrictamente alineado con el rango de compromisos a las entradas públicas ZK. Si existe una brecha entre numRealTxs en L1 y decoded_slots en el compromiso SHA256, cualquier supuesto que delegue en el circuito ZK la restricción de los slots del hueco puede ser burlado. L1 debe verificar de forma independiente cada public input slot comprometido por la prueba ZK, sin externalizar esa responsabilidad al circuito. El equipo de seguridad de SlowMist recomienda auditorías externas integrales antes de desplegar un sistema Rollup, con foco en la consistencia lógica en el límite de estado L1/L2, los límites de confianza en la decodificación de calldata y la verificación secundaria onchain de entradas públicas ZK. Para contratos retirados pero que aún custodian activos históricos, se aconseja migración ordenada o destrucción para eliminar el riesgo de exposición continuada. Este análisis fue elaborado por el equipo de Threat Intelligence de SlowMist, apoyándose en MistEye Threat Intelligence System, MistTrack Tracking Platform y análisis impulsado por IA de SlowMist Agent. Para consultas o comentarios, el equipo invita a ponerse en contacto.