Back to Blog

~$18M Perdidos: jaredFromSubway, Aztec e Mais | BlockSec Semanal

Code Auditing
June 25, 2026
12 min read
Key Insights

Durante a semana passada (2026/06/15 - 2026/06/21), observamos 3 incidentes de segurança notáveis com perdas totais de aproximadamente $18,3M.

Data Incidente Tipo Perda Estimada
2026/06/18 Aztec Vinculação Imprópria de Entrada Pública ~$2,2M
2026/06/20 Token LABUBU Configuração Incorreta ~$1,1M
2026/06/20 jaredFromSubway Gerenciamento Impróprio de Aprovações ~$15M
  • Aztec: Selecionado porque o protocolo foi explorado pela segunda vez em três dias, desta vez por meio de seu circuito de escape hatch, destacando problemas recorrentes de vinculação de provas ZK.
  • jaredFromSubway: Selecionado porque o contrato do bot MEV concedeu aprovações a contratos de tokens não confiáveis sem verificar o consumo ou revogar permissões residuais, permitindo que o atacante acumulasse e drenasse ~$15M.

Melhor Auditor de Segurança para Web3

Valide design, código e lógica de negócios antes do lançamento

Destaque da Semana: jaredFromSubway

Ao contrário dos exploits tradicionais de aprovação — onde os atacantes abusam de vulnerabilidades em contratos DeFi confiáveis para drenar ativos que os usuários aprovaram para esses contratos — este ataque visa a direção inversa: o bot MEV concedeu proativamente aprovações sobre seus próprios ativos a contratos de terceiros não confiáveis como parte de suas operações de arbitragem. O atacante construiu um ambiente de negociação falso (essencialmente um honeypot) onde pools de swap falsos emitiram eventos reais de Swap e Sync enquanto os tokens falsos nunca consumiam as permissões concedidas, acumulando essas aprovações voltadas para o exterior antes de coletá-las, com perdas totais reportadas de ~$15M.

Em 20 de junho de 2026, jaredFromSubway, um operador de bot MEV no Ethereum, perdeu aproximadamente $15M [1]. Com base em análise on-chain, a causa raiz foi o gerenciamento impróprio de aprovações no contrato do bot: aprovações foram concedidas a contratos wrapper não confiáveis que nunca as consumiram, e o atacante acumulou essas permissões não consumidas até drenar os saldos reais do bot em uma única transação.

Contexto

jaredFromSubway é um operador de bot MEV bem conhecido no Ethereum, especializado em ataques sanduíche e arbitragem on-chain. O contrato vítima (0x1f2f...f387) é uma de suas carteiras operacionais, mantendo grandes saldos de capital de trabalho em WETH, USDC e USDT.

Bots MEV desse tipo devem interagir dinamicamente com novos tokens e pools arbitrários que aparecem on-chain. Eles monitoram o mempool, simulam transações e aprovam automaticamente interações com tokens para capturar oportunidades de arbitragem. Esse modelo operacional se baseia na premissa de que os tokens se comportam como esperado: quando um swap é executado, o contrato do token consome a permissão concedida chamando transferFrom.

Análise de Vulnerabilidade

A causa raiz é o gerenciamento impróprio de aprovações do bot MEV ao interagir com contratos não confiáveis.

O bot executa diversos caminhos de arbitragem em pools e roteadores Uniswap. Na maioria das interações, o bot envia tokens diretamente para os pools via transfer, onde o próprio bot é o msg.sender e nenhuma aprovação é necessária. No entanto, as interações com contratos de tokens do tipo wrapper seguem um modelo pull: o bot chama wrapper.wrapTo(), e dentro dessa chamada o contrato wrapper chama realToken.transferFrom(bot, wrapper, amount) para puxar os tokens reais do bot. Como o msg.sender durante o transferFrom é o contrato wrapper — não o bot — uma aprovação prévia é necessária:

  1. <real_token>.approve(tokenContract, amount) — conceder permissão sobre o token real ao contrato wrapper
  2. tokenContract.wrapTo()swap() multi-hop em pools → tokenContract.unwrap() — encapsular o token real, rotear pelos pools e desencapsular de volta ao token real

O bot assumiu que wrapTo() consumiria a permissão via transferFrom, como um contrato wrapper bem comportado faria. No entanto, o bot nunca verificou se a permissão foi realmente consumida após a operação, nem revogou nenhuma permissão residual. Se wrapTo() não chamar transferFrom, a permissão completa sobrevive à operação e se torna uma superfície de ataque persistente — qualquer contrato que detenha tal permissão pode posteriormente chamar transferFrom para mover os ativos reais do bot.

Análise do Ataque

Com base na reconstrução on-chain, o atacante construiu um ambiente de negociação falso com três componentes para explorar a vulnerabilidade descrita acima:

  1. Tokens wrapper falsos: Cada token falso usava o nome do token real, mas prefixava o símbolo com f (por exemplo, nome USD Coin com símbolo fUSDC para USDC). Ele implementava wrapTo() e unwrap() para imitar um wrapper legítimo, além de uma função withdraw() restrita ao atacante que drenava permissões não consumidas via transferFrom.

  2. Pools de swap falsos: O atacante implantou aproximadamente 44 pools no estilo Uniswap V2 por meio de uma factory própria. Esses pools emparelhavam tokens falsos entre si para formar rotas de swap convincentes. Quando swap() era chamado, os pools emitiam eventos reais de Sync e Swap indistinguíveis de negociações legítimas.

  3. Lucros fabricados pelo atacante: Durante unwrap(), o token falso enviava uma pequena quantidade de tokens reais de volta ao bot via transfer. O bot recebia lucro real, mas este era deliberadamente fabricado pelo atacante, e não obtido de arbitragem de mercado.

O atacante controlava esses componentes por meio de um interruptor getStatus() por bloco em um contrato externo. getStatus() retornava 1 quando chamado no mesmo bloco de uma transação de ativação (que definia _getStatus = block.number), e 0 caso contrário. Quando getStatus() == 0, wrapTo() chamava transferFrom normalmente e a permissão era consumida. Quando getStatus() == 1, wrapTo() ignorava o transferFrom — a permissão não era consumida — enquanto unwrap() ainda retornava tokens fabricados pelo atacante ao bot. O atacante provavelmente usou subornos a builders para colocar a transação de ativação no mesmo bloco que a transação do bot quando queria acumular permissões.

O ataque ocorreu em três fases:

Fase 1: Implantar infraestrutura de ataque

  • Passo 1: O atacante configurou a infraestrutura entre os blocos 25354424 e 25354519. Isso incluiu a implantação de um contrato factory de tokens falsos (0x81f2...0091), a criação de ~44 pools Uniswap V2 falsos via uma factory própria, o financiamento dos pools com saldos iniciais de tokens para que as chamadas swap() fossem bem-sucedidas, e o envio de 0,01 ETH ao contrato de coleta (0xb84d...df52) para gás e suborno ao builder.

  • Passo 2: O atacante produziu em massa tokens wrapper falsos via CREATE2, cada um imitando um token real (usando o nome real, mas prefixando o símbolo com f) e carregando uma função withdraw() restrita ao atacante. O CREATE2 forneceu endereços determinísticos que o contrato de coleta podia iterar.

Fase 2: Construir confiança e acumular aprovações

  • Passo 3 (confiança inicial): Nas primeiras transações (por exemplo, 0x542d...362b no bloco 25354425), os tokens falsos não tinham o interruptor getStatus()wrapTo() chamava transferFrom diretamente, consumindo a permissão. O bot aprovava, encapsulava, trocava, desencapsulava e lucrava normalmente. Isso estabeleceu os tokens falsos como oportunidades de negociação lucrativas.

  • Passo 4 (confiança contínua): Em transações subsequentes (por exemplo, 0x085e...37e51), o interruptor getStatus() estava implantado, mas retornava 0 (bloco diferente da ativação). wrapTo() ainda chamava transferFrom e consumia a permissão. O bot continuou lucrando e mantendo as interações.

  • Passo 5 (acumulação): A partir de 0x8560...1915 no bloco 25360519, o atacante colocou uma transação de ativação no mesmo bloco da transação do bot via suborno ao builder, fazendo getStatus() retornar 1. Nesse modo, wrapTo() ignorava transferFrom — a permissão não era consumida — mas unwrap() ainda enviava uma pequena quantidade de tokens reais de volta ao bot. O bot via uma operação lucrativa e deixava a aprovação intacta. Ao longo de aproximadamente 600 blocos (~13 transações), o bot repetiu esse padrão em WETH, USDC e USDT, acumulando permissões não consumidas nos três ativos reais.

Fase 3: Coleta

  • Passo 6: O atacante chamou withdraw() em todos os tokens falsos na transação de coleta 0x2be870...cf3e65, usando as permissões não consumidas para chamar transferFrom e mover os saldos reais do bot para o atacante. Um suborno de 0,01 ETH ao builder foi incluído para garantir a inclusão no bloco. A coleta extraiu 1.474,58 WETH + 2.870.573 USDC + 2.035.760 USDT (~$7,5M) apenas do contrato vítima.

A transação de ataque identificada levou a ~$7,5M em perdas, e a perda total é de aproximadamente $15M com base na declaração de jaredFromSubway [1].

Conclusão

A causa raiz deste incidente foi o gerenciamento impróprio de aprovações do bot MEV ao interagir com contratos não confiáveis. Ao contrário dos exploits tradicionais de aprovação, onde os atacantes abusam de vulnerabilidades em contratos DeFi confiáveis para drenar ativos aprovados pelos usuários, este ataque funciona na direção inversa: o bot aprovou proativamente seus próprios ativos a contratos de terceiros não confiáveis como parte de suas operações de arbitragem. O atacante acumulou essas aprovações voltadas para o exterior e não consumidas e as coletou em uma única transação.

Para reduzir riscos semelhantes no futuro, contratos de bot que interagem com contratos de tokens não confiáveis devem verificar se as aprovações são consumidas após cada operação e revogar qualquer permissão residual. Permissões não consumidas após uma negociação aparentemente bem-sucedida são um forte sinal de comportamento malicioso do token.

Comece a Usar o Phalcon Explorer

Mergulhe nas Transações para Agir com Sabedoria

Experimente gratuitamente

Mais Incidentes desta Semana

Aztec

Em 18 de junho de 2026, uma restrição de igualdade ausente no circuito ZK de escape hatch do Aztec permitiu que um atacante sacasse aproximadamente $2,2M (1.158 ETH, 150 mil DAI e ~0,47 renBTC) do contrato legado RollupProcessor no Ethereum [2], [3]. Este foi o segundo exploit do Aztec em três dias (o primeiro, coberto em nosso relatório anterior, visou o RollupProcessorV3 atualizado) por meio de um bug relacionado de vinculação de entrada pública no circuito ZK.

Contexto

O RollupProcessor legado do Aztec inclui uma função escapeHatch: um mecanismo de segurança que permite que qualquer pessoa envie uma prova de transação única quando o operador do rollup para de processar. Ao contrário de processRollup (que requer um provedor autorizado), o escape hatch abre em janelas periódicas com base no número do bloco e pode ser chamado por qualquer pessoa:

function escapeHatch(
    bytes calldata proofData,
    bytes calldata signatures,
    bytes calldata viewingKeys
) external override whenNotPaused {
    (bool isOpen, ) = getEscapeHatchStatus();
    require(isOpen, 'Rollup Processor: ESCAPE_BLOCK_RANGE_INCORRECT');
    processRollupProof(proofData, signatures, viewingKeys);
}

O escape hatch usa um circuito ZK dedicado (escape_hatch_circuit) que processa uma transação join-split: consumindo notas de entrada da árvore de Merkle e criando notas de saída. O circuito deve verificar que as notas de entrada existem na árvore de dados atual (usando old_data_root para verificação de pertinência Merkle), depois expor a mesma raiz como uma entrada pública para que o contrato L1 valide contra o estado on-chain.

Análise de Vulnerabilidade

A vulnerabilidade está no circuito de escape hatch (escape_hatch_circuit.cpp). O valor old_data_root é transformado em duas testemunhas independentes sem nenhuma restrição de igualdade conectando-as.

A primeira testemunha (linha 33) é passada para o componente do circuito join-split, onde é usada para provas de pertinência Merkle para verificar que as notas de entrada existem na árvore de dados:

join_split_inputs inputs = {
    // ...
    witness_ct(&composer, tx.js_tx.old_data_root),        // linha 33: primeira testemunha
    // ...
};
auto outputs = join_split_circuit_component(composer, inputs);

A segunda testemunha (linha 50) é criada de forma independente e exposta como uma entrada pública (linha 88), que o contrato Solidity extrai e verifica contra a raiz de dados on-chain:

auto old_data_root = field_ct(witness_ct(&composer, tx.js_tx.old_data_root));  // linha 50: segunda testemunha
// ...
composer.set_public_input(old_data_root.witness_index);    // linha 88: exposta como entrada pública

Em um circuito ZK, cada chamada witness_ct cria uma variável independente. Sem um assert_equal explícito entre as linhas 33 e 50, o provador pode atribuir valores diferentes a essas duas testemunhas. No lado Solidity, validateMerkleRoots verifica require(oldDataRoot == dataRoot) usando apenas a entrada pública da linha 50, sem visibilidade sobre o valor usado na linha 33.

O mesmo padrão de desvinculação também existe para input_owner e output_owner: esses valores são testemunhados nas linhas 38–39 (passados para join_split_circuit_component para verificação de propriedade) e novamente nas linhas 111–112 (expostos como testemunhas públicas independentes). No entanto, não identificamos um caminho de exploit prático para essa lacuna.

Análise do Ataque

O circuito de escape hatch foi removido do código-fonte aztec-connect [4], mas o contrato verificador implantado ainda contém a chave de verificação EscapeHatchVk, portanto provas geradas com o circuito vulnerável ainda podem passar na verificação on-chain. No momento do ataque, o contrato estava silencioso há 142 dias e a janela de escape hatch estava aberta. O endereço do atacante havia sido criado apenas 14 horas antes do exploit via Union Chain [2]. O ataque consiste em três etapas principais:

  • Passo 1: O atacante construiu uma árvore de Merkle falsa contendo notas de propriedade própria com valor arbitrário. Essas notas não existiam na árvore de dados real on-chain (a árvore de Merkle que armazena todas as notas válidas).

  • Passo 2: O atacante gerou uma prova de escape hatch explorando as testemunhas desvinculadas descritas acima. A testemunha da linha 33 (usada para pertinência Merkle no componente join-split) foi definida como a raiz de Merkle falsa (a verificação de pertinência passou porque as notas fabricadas existiam na árvore falsa, e as verificações de propriedade passaram porque o atacante detinha as chaves de assinatura). A testemunha da linha 50 (exposta como a entrada pública verificada pelo Solidity) foi definida como a raiz de dados real on-chain (a verificação require(oldDataRoot == dataRoot) do Solidity passou porque esse valor correspondia à raiz armazenada no contrato).

  • Passo 3: Com as verificações do circuito e do Solidity satisfeitas, a prova foi verificada com sucesso. O contrato processou a transação de escape hatch como legítima e liberou os fundos.

O atacante repetiu esse processo em três transações (0x9e1d6a...6b03ca, 0xab306c...59c2b5, 0x5c196c...4705c3), visando diferentes ativos e extraindo 1.158 ETH, 150 mil DAI e ~0,47 renBTC respectivamente, totalizando aproximadamente $2,2M.

Conclusão

A causa raiz deste incidente foi uma restrição de igualdade ausente entre duas testemunhas para old_data_root no circuito de escape hatch. Uma testemunha foi usada para verificação de pertinência de notas privadas dentro do componente join-split, a outra foi exposta como a entrada pública verificada pelo Solidity. Sem uma restrição vinculando-as, o atacante provou a propriedade de notas fabricadas contra uma árvore de Merkle falsa enquanto o contrato L1 via uma raiz on-chain válida. Notavelmente, remover o circuito vulnerável do código-fonte não neutralizou o contrato verificador já implantado — a função escapeHatch no RollupProcessor legado permanece chamável sempre que sua janela de número de bloco estiver aberta.

Para reduzir riscos semelhantes no futuro, quando o mesmo valor lógico aparece em múltiplos pontos de um circuito ZK, todas as instâncias devem ser explicitamente restringidas a ser iguais — chamadas independentes de witness_ct para o mesmo valor constituem uma lacuna de vinculação. Auditorias de circuito devem verificar sistematicamente que toda entrada pública está vinculada ao valor interno ao circuito que ela representa.

Comece a Usar o Phalcon Security

Detecte cada ameaça, alerte o que importa e bloqueie ataques.

Experimente gratuitamente

Referências

Sobre a BlockSec

A BlockSec é uma provedora full-stack de segurança em blockchain e conformidade cripto. Desenvolvemos produtos e serviços que ajudam nossos clientes a realizar auditorias de código (incluindo contratos inteligentes, blockchain e carteiras), interceptar ataques em tempo real, analisar incidentes, rastrear fundos ilícitos e cumprir obrigações de AML/CFT, ao longo de todo o ciclo de vida de protocolos e plataformas.

A BlockSec publicou múltiplos artigos de segurança em blockchain em conferências de prestígio, reportou vários ataques zero-day em aplicações DeFi, bloqueou múltiplos hacks para resgatar mais de 20 milhões de dólares e protegeu bilhões em criptomoedas.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit