Contexto

Em 18 de junho de 2026, o contrato RollupProcessor do protocolo de privacidade Layer2 do Ethereum, Aztec Connect, sofreu um ataque. O invasor explorou a falha na confiança do mecanismo Escape Hatch, onde não havia verificação independente do pertencimento de fundos e limites de saque na camada Solidity, além da falta de restrições nas circuitos relacionados, submetendo uma prova de escape hatch aceita pelo TurboVerifier, retirando diretamente 1.158 ETH (cerca de 2,06 milhões de dólares) do saldo do contrato, e utilizando o mesmo mecanismo para roubar 150.000 DAI e 0,47 renBTC, totalizando uma perda de aproximadamente 2,22 milhões de dólares.

Visão geral do ataque

Transação de ataque:

Causa raiz da vulnerabilidade

Fronteira de confiança ausente do escapeHatch()

A função de entrada pública escapeHatch() do RollupProcessor só verifica se o status do escape hatch está habilitado e então entra diretamente em processRollupProof(), sem executar nenhuma validação de permissões do chamador:

Em contrapartida, a rota de processamento normal de rollup processa processRollup() e valida a autorização rollupProviders[provider] e verifica a assinatura do provider. Neste ataque, é chamada escapeHatch(), com signatures = 0x e viewingKeys = 0x, contornando completamente o fluxo de autorização do provider.

verifyProofAndUpdateState() confia cegamente no Verifier

Após entrar em processRollupProof(), o contrato chama TurboVerifier.verify(proofData, 0):

O comportamento do TurboVerifier em si está correto — ele realiza a verificação matemática da prova Plonk. O verdadeiro problema é que o RollupProcessor equipara sucesso na verificação matemática à legitimidade do negócio e não faz uma validação independente adicional na camada Solidity.

processDepositsAndWithdrawals() executa saques incondicionalmente

Lógica central de execução após o Verifier retornar sucesso:

Função final de saída de fundos:

Circuito zkSNARK sem portas de restrição de equação (causa raiz)

No circuito de prova join-split do Aztec, o old_data_root é dividido em duas rotas independentes para uso:

  • Testemunha interna A: recebe o subcircuito join-split, usado para verificar a inclusão de membros do Merkle da nota privada

  • Entrada pública B: exposta como entrada pública para a camada Solidity, para que validateMerkleRoots() compare com o dataRoot na cadeia

No circuito, a restrição ausente A == B faz com que ambos possam receber valores independentemente:

  • A pode ser uma raiz de Merkle forjada que o atacante construiu (com notas de qualquer quantia na árvore)

  • B pode ser um dataRoot real na cadeia (faz a checagem da Solidity passar)

Como o sistema de restrições permite A ≠ B, a prova zkSNARK permanece válida o tempo todo.

Fluxo do ataque

Fase de preparação: forjar a árvore Merkle e a prova

O atacante constrói localmente uma árvore Merkle de dados forjada, inserindo uma nota privada:

  • Valor: 1158 ETH (o contrato tinha cerca de 1158,7598 ETH na época)

  • Proprietário: o atacante possui a chave privada correspondente

O atacante constrói a prova zkSNARK:

  • Testemunha interna A (old_data_root) = raiz da árvore forjada

  • Entrada pública B (old_data_root) = dataRoot real na cadeia = 0x184bea7d9493cd9a5efb6b679d04066a8c92a34ac8ec150e9635133c6010977b

Primeira fase: construir a transação de ataque

O agressor EOA 0x6952d9246e9aFE8B887B2877225163436F78E97F faz uma chamada de nível superior diretamente ao RollupProcessor (0x737901bea3eeb88459df9ef1be8ff3ae1b42a2ba):

  • Assinatura da função: escapeHatch(bytes,bytes,bytes)

  • msg.value = 0

  • signatures = 0x

  • viewingKeys = 0x

No proofData construído pelo atacante, estão incluídas as entradas públicas-chave:

Segunda fase: burlar a verificação de permissões

escapeHatch() só verifica se getEscapeHatchStatus() retorna true e então chama diretamente processRollupProof(). Essa rota não executa a verificação de autorização de rollupProviders[provider] no processRollup() comum, nem valida a assinatura do provider. Não existe qualquer vínculo entre a identidade do chamador e o destinatário do saque.

Terceira fase: verificação matemática do Verifier

O RollupProcessor faz um STATICCALL para o TurboVerifier (0x48cb7ba00d087541dc8e2b3738f80fdd1fee8ce8):

Como rollup_size = 0, o TurboVerifier usa EscapeHatchVk (vk.num_inputs = 26) selecionado por VerificationKeys.getKeyById(0) para realizar a verificação matemática da prova Plonk. O Verifier só verifica a consistência matemática entre a proof e as public inputs, não lê o estado do RollupProcessor e não verifica a atribuição de saldo.

Nesta transação, a chamada do verifier retorna com sucesso, indicando que a prova do escape hatch enviada pelo atacante está correta matematicamente.

Quarta fase: execução do saque

Após o Verifier retornar sucesso, o RollupProcessor atualiza o estado do rollup:

Em seguida, entra em processDepositsAndWithdrawals(); no modo escape hatch, mesmo com rollupSize == 0, ainda processa como 1 transação interna. Após analisar o proofData, foi descoberto que:

Chama diretamente withdraw(1158 ETH, endereço do atacante, 0); transfere os fundos chamando receiverAddress.call{value: 1158 ETH}("")

Quinta fase: ataque no mesmo padrão a DAI e renBTC

O atacante usou, no mesmo período, exatamente a mesma rota de vulnerabilidade, construindo separadamente provas de escape hatch direcionadas a ativos ERC20 (assetId ≠ 0), roubando 150.000 DAI e 0,47 renBTC do RollupProcessor. No withdraw(), a ramificação assetId ≠ 0 executa o transfer() do ERC20 para efetuar a transferência.

Ganho líquido das três transações de ataque:

Rastreamento de fundos

Por meio do sistema de rastreamento de antirrastreamento AML MistTrack da névoa lenta, analisar o agressor EOA 0x6952d9246e9aFE8B887B2877225163436F78E97F:

Fonte dos fundos: o gás inicial usado pelo endereço criado pelo atacante veio de 0x963737c550e70ffe4d59464542a28604edb2ef9a (endereço da entidade unionchain.ai). Esse endereço ficou ativo pela primeira vez em 2026-06-18 02:21:11 UTC, exatamente no momento em que a ação de ataque foi iniciada; trata-se de um endereço único criado especificamente para este ataque.

Destino dos fundos: a distribuição atual dos fundos roubados é a seguinte:

  • Os tokens de 802 ETH ainda permanecem no endereço principal do atacante 0x6952d9246e9aFE8B887B2877225163436F78E97F; além disso, 150.000 DAI e 0,47 renBTC também não foram transferidos

  • 300 ETH já foram transferidos para 0x15930a0fef3421f48c6553b5691682cc1b22edb3 (MistTrack marca como endereço malicioso associado ao Aztec Exploiter)

  • Cerca de 56 ETH foram transferidos para outro endereço malicioso (também marcado como associado ao Aztec Exploiter)

  • 0x33d6a0d9bc210e823e043d604179cd844eb467df com pontuação de risco AML de 100/100 (Severe); também envolve o evento Aztec Exploiter

Resumo

O principal aprendizado deste ataque é: o "correto matematicamente" de um circuito zkSNARK ≠ "seguro para o negócio". O verificador só consegue provar que "você sabe uma testemunha que satisfaz as restrições"; se as próprias restrições estiverem erradas (ou faltando), o sistema de prova vira uma máquina legítima de falsificação. A equipe de segurança da SlowMist recomenda que o projeto faça auditoria específica de todas as rotas de restrições de provas de conhecimento zero para garantir a segurança do circuito.

Este artigo foi escrito pelo time de inteligência de ameaças da SlowMist em conjunto com o sistema MistEye, a plataforma de rastreamento MistTrack e análises guiadas por IA do SlowMist Agent; para quaisquer dúvidas, sinta-se à vontade para consultar e enviar feedback.