Aztec Connect sofre ataque de US$ 2,19 milhões após falha em ZKRollup

Em 14 de junho de 2026, o contrato RollupProcessor do Aztec Connect, já descontinuado, foi explorado. O alvo foi o RollupProcessor (0xff1f2b4adb9df6fc8eafecdcbf96a2b351680455), de natureza imutável, que permaneceu exposto por ainda custodiar ativos residuais de usuários mesmo após a descontinuação do Aztec Connect em março de 2024. O invasor retirou cerca de US$ 2,19 milhões do pool na L1 em uma única transação atômica ao criar um "vão" de limites entre numRealTxs e decoded_slots. Causa raiz A falha decorre de um desalinhamento estrutural entre (i) o intervalo de ciclos de liquidação na L1 efetivamente percorrido pelo RollupProcessorV3 e (ii) o intervalo comprometido pelo hash de entradas públicas (ZK public input hash). O ataque explorou essa divergência para fazer com que 31 de 32 slots de entradas públicas fossem comprometidos no estado da L2 via provas ZK sem passar pela validação de liquidação na camada de contrato da L1. No Decoder.sol, numRealTxs é totalmente controlado pelo atacante: o valor é lido do calldata no offset 4516 sem restrições on-chain. Já os decoded_slots são arredondados para o múltiplo mais próximo de numTxsPerRollup, exigência do layout de dados do precompile de SHA256. Esse arredondamento abre uma região de "gap" entre numRealTxs e decoded_slots que pode ser preenchida livremente. No RollupProcessorV3.sol, o ciclo de liquidação cobre apenas numRealTxs slots. Na prática, a suposição de segurança usual é que cada slot de entrada pública seja validado na L1 (por exemplo, reduzindo pendingDepositBalance no depósito) ou então seja rigidamente restringido pelo circuito ZK para que publicValue == 0. Nesta ocorrência, a combinação de defesas falhou: o precompile SHA256 cobriu todos os 32 slots (com input de 8192 bytes = 32 × 256 bytes), e o conteúdo dos slots do "gap" foi comprometido via prova ZK; o ciclo de liquidação da L1 processou apenas o primeiro slot, deixando os slots [2..32] sem validação na L1; a restrição do circuito ZK para publicValue nos slots do "gap" (que deveria ser 0) foi contornada ou não aplicada. Modelo de divergência em dois caminhos O mesmo calldata é consumido por dois caminhos com limites superiores diferentes: o ZK "enxerga" 32 slots, enquanto a L1 contabiliza apenas 1. Essa discrepância sobre "quais slots contam" permitiu a criação de saldo sem lastro. Como o ataque foi executado A transação 0x074ec931…aee1 inclui 14 chamadas a processRollup(), organizadas em duas fases dentro de uma única transação atômica: "7 mints" seguidos de "7 saques". Fase 1 — "Mint": criação de ativos na L2 sem respaldo (Rollups #13277–13283) 1) A EOA do atacante 0x0f18d8b44a740272f0be4d08338d2b165b7edd17 chamou o contrato controlador 0x06f585f74e0da633ae813a0f23fb9900b61d0fcd, acionando o selector 0x6f3ce701. 2) O contrato mestre chamou sequencialmente três contratos de relay, com calldatas maliciosos hardcoded. Parâmetros-chave: numRealTxs = 1, rollupSize = 1024, numInnerRollups = 32. - Slot 1 (visível na L1): proofId = 0 (noop), publicValue = 0 - Slots 2–32 (31 slots no "gap", invisíveis à L1): proofId = 1 (deposit), publicValue = N, publicOwner = endereço L2 do atacante Os pacotes incluíam a prova ZK correspondente, e o circuito não impôs publicValue = 0 nos slots do "gap". 3) O Relay A chamou RollupProcessor.processRollup() em sequência (Rollups #13277–13281, 5 vezes). O verificador confirmou a prova ZK; o compromisso SHA256 cobriu 32 slots. O ciclo de liquidação na L1 encerrou em 1 × TX_PUBLIC_INPUT_LENGTH (1 slot), processando apenas noops. Depósitos falsos nos slots [2..32] foram comprometidos no novo Merkle root, elevando o saldo do atacante na L2 em 5 × 31N. 4) O Relay B processou os Rollups #13282–13283 do mesmo modo (2 vezes), adicionando mais 2 × 31N. Ao fim da fase, o atacante acumulou 7 × 31N em depósitos sem lastro na L2, enquanto o cofre na L1 permaneceu inalterado. Fase 2 — Saque: conversão do saldo inflado na L2 em ativos na L1 (Rollups #13284–13290) O atacante escoou o saldo obtido na fase anterior por meio de sete rollups de saque: - Rollup #13284 (DAI): withdraw() → transferência direta de 270.513,054 DAI para 0x0f18…edd17 - Rollup #13285 (wstETH): 167,890 wstETH → atacante - Rollup #13286 (yvDAI): 4.873,857 yvDAI → atacante - Rollup #13287 (yvWETH, relay C): 16,570 yvWETH → atacante - Rollup #13288 (LUSD): 9.273,734 LUSD → atacante - Rollup #13289 (yvLUSD): 359,047 yvLUSD → atacante - Rollup #13290 (ETH): 908,987 ETH transferidos via CALL interno → atacante A transação atômica foi concluída (gasUsed = 4.513.539), sem possibilidade de rollback parcial em nível de contrato. O ganho estimado foi de aproximadamente US$ 2,19 milhões, retirados do pool legítimo de ativos do RollupProcessor. Rastreamento dos fundos Segundo a análise forense on-chain (em 15 de junho de 2026, cerca de um dia após o incidente), os ativos foram transferidos em uma única transação do RollupProcessor, passando pelo contrato intermediário 0x06f585…d0fcD e chegando diretamente à EOA do atacante 0x0F18D8b44a740272f0be4d08338d2b165b7EdD17. O contrato intermediário não reteve saldo. Os valores roubados permanecem 100% intactos e sem movimentações de "lavagem" até o momento. Conclusões e recomendações A lição central é que o limite superior do loop de liquidação do contrato de ZKRollup precisa estar rigidamente alinhado ao intervalo comprometido nas entradas públicas do ZK. Quando existe um "gap" entre o limite numRealTxs na L1 e os decoded_slots do compromisso SHA256, qualquer premissa que dependa do circuito ZK para impor restrições nos slots do "gap" pode ser contornada. A L1 deve verificar de forma independente cada slot de entrada pública coberto pela prova ZK, sem terceirizar essa responsabilidade ao circuito. A equipe da SlowMist recomenda auditoria externa abrangente antes do deployment de sistemas de rollup, com foco em consistência lógica na fronteira de estado L1/L2, limites de confiança na decodificação de calldata e verificação secundária on-chain das entradas públicas do ZK. Para contratos descontinuados que ainda retêm ativos legados, a orientação é realizar migração ordenada ou destruição dos contratos/posições para eliminar riscos persistentes. O material foi preparado pela Threat Intelligence Team da SlowMist, com apoio do MistEye Threat Intelligence System, da plataforma MistTrack e de análises conduzidas pelo SlowMist Agent com IA. Contato para dúvidas e feedback permanece aberto.