Durante a semana passada (2026/06/08 - 2026/06/15), 4 incidentes notáveis foram detectados no Ethereum e Solana, resultando em aproximadamente $5,98M em perdas totais. A tabela abaixo destaca os eventos representativos:
| Data | Incidente | Tipo | Perda Estimada |
|---|---|---|---|
| 2026/06/08 | Flooring Protocol | Estouro de Inteiro | ~$900K |
| 2026/06/09 | Top Token | Ataque de Governança | ~$1,59M |
| 2026/06/10 | Raydium (no Solana) | Falta de Validação de Entrada | ~$1,34M |
| 2026/06/15 | Aztec | Falta de Validação de Entrada | ~$2,15M |
- Aztec: Uma lacuna de validação entre o caminho de prova do rollup e o caminho de liquidação L1 permitiu que os dois processassem conjuntos de transações diferentes, atingindo estados inconsistentes.
- Raydium: Uma verificação de validação ausente permitiu que o atacante manipulasse o cálculo de resgate de tokens LP, drenando as reservas completas de quatro pools.
Melhor Auditor de Segurança para Web3
Valide design, código e lógica de negócio antes do lançamento
Destaque da Semana: Aztec
Neste incidente, o verificador de prova ZK e a lógica de liquidação L1 processaram conjuntos de transações diferentes porque um único parâmetro foi deixado sem limite. Essa lacuna de consistência entre prova e liquidação se aplica a qualquer design de rollup onde esses dois caminhos são executados como código separado.
Em 15 de junho de 2026, o Aztec Connect, um rollup com foco em privacidade no Ethereum, foi explorado por aproximadamente $2,15M [1]. A causa raiz foi uma divergência entre o conjunto de transações do rollup verificado e o limite de processamento da liquidação L1, que permitiu que o caminho de prova ZK e a lógica de liquidação processassem listas de transações diferentes. O atacante explorou essa lacuna para creditar saldos de depósito sem lastro no estado do rollup e, em seguida, sacá-los por meio de fluxos normais de liquidação.
Contexto
O Aztec Connect é um rollup com foco em privacidade no Ethereum que permite transações privadas em L2. Como os fundos dos usuários se originam em L1, eles devem primeiro ser depositados no contrato do processador de rollup antes de serem representados como notas na árvore de Merkle do L2.
O processo de depósito funciona em duas etapas:
Etapa 1: O usuário chama depositPendingFunds(), que aumenta userPendingDeposits[assetId][owner] por meio de increasePendingDepositBalance() e transfere os tokens para o RollupProcessor. Isso cria um depósito pendente em L1.
function depositPendingFunds(uint256 _assetId, uint256 _amount, address _owner, bytes32 _proofHash) external {
increasePendingDepositBalance(_assetId, _owner, _amount);
// ... transfere tokens para o contrato
}
Etapa 2: O usuário envia uma prova de depósito, que é posteriormente incluída em um rollup e adicionada ao estado L2. Quando processRollup() é executado, decodeProof() lê numTxs do calldata codificado e o retorna junto com os dados de prova decodificados. Ambos são então passados para processRollupProof():
function processRollup(bytes calldata, bytes calldata _signatures) external {
(bytes memory proofData, uint256 numTxs, uint256 publicInputsHash) = decodeProof();
processRollupProof(proofData, _signatures, numTxs, publicInputsHash, rollupBeneficiary);
}
Dentro de processRollupProof(), duas funções são chamadas sequencialmente. Primeiro, verifyProofAndUpdateState() verifica a prova ZK contra todas as transações decodificadas e atualiza o estado do rollup. Em seguida, processDepositsAndWithdrawals() lida com a liquidação L1, iterando apenas os primeiros _numTxs slots e chamando decreasePendingDepositBalance() para cada depósito (essa chamada reverte se o usuário não depositou fundos de fato na Etapa 1, vinculando o crédito do rollup a uma transferência L1 real):
function processRollupProof(bytes memory _proofData, bytes memory _signatures,
uint256 _numTxs, uint256 _publicInputsHash, address _rollupBeneficiary) internal {
verifyProofAndUpdateState(_proofData, _publicInputsHash); // caminho de prova: todas as transações decodificadas
processDepositsAndWithdrawals(_proofData, _numTxs, _signatures); // caminho de liquidação: apenas os primeiros _numTxs
}
// dentro de processDepositsAndWithdrawals:
end := add(proofDataPtr, mul(_numTxs, TX_PUBLIC_INPUT_LENGTH))
while (proofDataPtr < end) {
// ... para cada depósito:
decreasePendingDepositBalance(assetId, publicOwner, publicValue);
}
Esse design de duas etapas exige que a lógica de liquidação L1 processe exatamente o mesmo conjunto de transações que a prova ZK verificou. Se os dois caminhos divergirem em quais transações processar, os depósitos podem ser creditados no estado do rollup sem consumir seus saldos pendentes em L1.
Análise de Vulnerabilidade
No contrato do processador de rollup (0x7d65...2728), numTxs não estava efetivamente vinculado ao conjunto de transações imposto pela prova ZK. O caminho de prova e o caminho de liquidação podiam, portanto, processar listas de transações diferentes.
No rollup_circuit off-chain, num_txs é carregado como uma testemunha e apenas restringido por intervalo. O circuito o usa para determinar quais slots são tratados como transações reais, mas não verifica se num_txs é igual à contagem real de provas não-preenchidas:
const auto num_txs = uint32_ct(witness_ct(&composer, rollup.num_txs));
field_ct(num_txs).create_range_constraint(MAX_TXS_BIT_LENGTH);
// ...
auto is_real = num_txs > uint32_ct(&composer, i); // determina a lógica de transação real por slot
O provador pode definir num_txs para qualquer valor dentro do intervalo permitido. Os slots além de num_txs ainda são verificados recursivamente, mas suas entradas públicas são zeradas, portanto não contribuem para o estado do rollup:

No lado do Solidity, decodeProof() lê numTxs dos metadados do calldata que não são copiados para o proofData reconstruído verificado por verifyProofAndUpdateState(). O limite do loop de liquidação, portanto, também não está coberto pela prova ZK:

Com nenhum dos lados restringindo esse valor, um atacante poderia definir numTxs como menor do que o número real de transações decodificadas. O loop de liquidação então pularia transações que a prova já havia creditado no estado do rollup. Uma transação não acionável poderia ocupar o primeiro slot decodificado (dentro do intervalo de varredura da liquidação), enquanto um depósito real poderia estar em um slot posterior (provado pelo circuito, mas fora do intervalo de varredura da liquidação). A prova creditaria o depósito no estado do rollup, mas a lógica de liquidação o pularia inteiramente, incluindo a chamada decreasePendingDepositBalance(). Isso deixava o saldo de depósito pendente não consumido em L1, enquanto o estado do rollup já refletia o depósito.
Análise do Ataque
A análise a seguir é baseada na transação 0x074ec9...9aeeb1.
O atacante explorou a lacuna entre o caminho de prova e o caminho de liquidação em duas fases.
Fase 1: Criando saldos sem lastro
-
Passo 1: O atacante enviou múltiplos lotes de rollup, cada um contendo duas transações decodificadas: uma transação não acionável (lixo) no slot 1 e um depósito real no slot 2, com
numTxsdefinido como 1. A lógica de liquidação L1 processou apenas a transação lixo no slot 1, ignorando completamente o depósito real no slot 2. -
Passo 2: A prova ZK, no entanto, verificou e creditou todas as transações decodificadas, incluindo o depósito no slot 2. Como a lógica de liquidação nunca chegou a esse depósito,
decreasePendingDepositBalance()não foi chamado, e o saldo de depósito pendente em L1 permaneceu não consumido. O atacante repetiu esse padrão para sete ativos diferentes, acumulando saldos sem lastro no estado do rollup.
Fase 2: Extraindo fundos
- Passo 3: Uma vez que os sete saldos sem lastro foram estabelecidos, o atacante iniciou saques padrão para cada ativo. Esses saques pareciam legítimos para a lógica de liquidação porque os saldos existiam no estado do rollup, então o contrato L1 liberou os fundos correspondentes — aproximadamente $2,15M no total.

Conclusão
Essa vulnerabilidade não foi uma fraqueza criptográfica, mas um bug de consistência de estado entre dois caminhos de código críticos na arquitetura do rollup. A causa raiz: numTxs não estava vinculado ao conjunto de transações provado em nenhum dos lados. O circuito apenas o restringia por intervalo, e o decodificador Solidity o lia de metadados de calldata não verificados. Sem esse vínculo, o caminho de prova e o caminho de liquidação podiam processar listas de transações diferentes. O atacante definiu numTxs como menor do que a contagem real de transações para que a lógica de liquidação pulasse depósitos que a prova já havia creditado no estado do rollup. Os saldos sem lastro resultantes foram então sacados por meio de fluxos normais de liquidação.
O rollup Aztec Connect anunciou o encerramento de suas atividades, com o processamento de transações e saques programados para terminar em 31 de março de 2024 [2]. No entanto, o contrato do processador de rollup ainda foi atualizado em 10 de abril de 2024 por meio de um pull request [3], e a lógica vulnerável está presente nessa atualização pós-encerramento.
A correção exige vincular numTxs ao conjunto completo de transações verificadas pela prova ZK, para que ambos os caminhos sempre processem o mesmo conjunto. Qualquer design de rollup que separe a verificação de prova da liquidação L1 deve garantir que ambos os caminhos operem em um conjunto de transações idêntico e verificavelmente delimitado. Uma discrepância em até um único parâmetro pode transformar um sistema de prova intrinsecamente sólido em um vetor para criação de saldos sem lastro.
Referências
- [1] Alerta BlockSec Phalcon: Análise do Incidente Aztec
- [2] Aviso de Encerramento do Aztec Connect
- [3] PR de Atualização RollupProcessorV3 #67
Comece a usar o Phalcon Explorer
Mergulhe nas Transações para Agir com Sabedoria
Experimente gratuitamenteMais Incidentes desta Semana
Raydium
Em 10 de junho de 2026, quatro pools no programa legado AMM v3 do Raydium no Solana foram explorados por aproximadamente $1,34M [1]. O manipulador de saques não verificava se uma conta fornecida pelo chamador correspondia à contraparte armazenada no pool, então o atacante substituiu uma conta controlada por ele para manipular o cálculo de pagamento. A mesma técnica drenou todas as reservas de quatro pools em segundos.
Contexto
O AMM do Raydium é um criador de mercado de produto constante no Solana. Cada pool mantém dois cofres de tokens e cunha um token LP representando uma parcela proporcional das reservas. Quando um provedor de liquidez saca, o manipulador calcula o pagamento proporcionalmente e transfere a parcela correspondente de ambos os cofres:
coin_out = total_coin * withdraw_amount / lp_supply
pc_out = total_pc * withdraw_amount / lp_supply
No Solana, cada tipo de token é definido por uma conta Mint que armazena o fornecimento total, decimais e autoridade de cunhagem. O saldo de cada titular é armazenado em uma conta Token separada vinculada a essa Mint — uma Mint pode ter muitas contas Token entre diferentes titulares. Isso difere do EVM, onde um único contrato ERC-20 gerencia tanto a definição do token quanto todos os saldos internamente.
Na fórmula de saque acima, lp_supply é lido da conta Mint de LP do pool — aquela que rastreia o fornecimento total de LP. A correção do cálculo depende desse valor ser a Mint de LP real. No entanto, no Solana, o chamador passa cada conta para cada instrução posicionalmente, portanto o manipulador deve validar que cada conta fornecida pelo chamador corresponde à conta canônica armazenada no estado do pool.
Análise de Vulnerabilidade
O programa explorado (27haf8...8vQv) não era de código aberto, e seus dados executáveis (ProgramData) foram fechados após o ataque, tornando a inspeção direta do bytecode impossível. A análise abaixo é baseada no bytecode reconstruído a partir do último buffer de atualização do programa e cruzado com o comportamento de transações on-chain.
No manipulador de saques, a conta Mint de LP passada pelo chamador não estava vinculada ao amm.lp_mint registrado no pool. O seguinte pseudocódigo de engenharia reversa reconstruído a partir do bytecode on-chain mostra o layout das contas. O manipulador verificou vínculos para o estado do pool, autoridade PDA, ambos os cofres e contas de usuário — mas não para a Mint de LP no slot 5:
let amm_info = next_account_info(it)?; // accounts[1] — estado do pool (contém amm.lp_mint)
// ...
let amm_lp_mint_info = next_account_info(it)?; // accounts[5] — mint fornecida pelo chamador
let amm = AmmInfo::load(amm_info)?;
// vínculos de autoridade, cofres, open_orders verificados aqui...
// >>> AUSENTE: verificação de que accounts[5].key == amm.lp_mint <<<
let lp_mint = Mint::unpack(&amm_lp_mint_info.data.borrow())?;
let lp_mint_supply = lp_mint.supply; // lê da mint não verificada
let coin_amount = total_coin * withdraw_amount / lp_mint_supply;
let pc_amount = total_pc * withdraw_amount / lp_mint_supply;
Como a conta Mint de LP não estava vinculada, um atacante poderia substituir uma conta Mint que controlava completamente. Definir seu supply total como 1 e queimar 1 token resultava em uma proporção de pagamento de 1 / 1 = 100% de cada reserva.
O código vulnerável estava ativo e sem alterações desde a última atualização do programa em 3 de janeiro de 2023, aproximadamente 1.254 dias antes do exploit.
Análise do Ataque
A análise a seguir é baseada na transação 1csN6v...3s7s.
- Passo 1: O atacante criou uma conta Mint de LP falsa com
decimals = 0esupplytotal = 0.

- Passo 2: O atacante inicializou uma conta Token vinculada à Mint de LP falsa e, em seguida, cunhou exatamente 1 token nela (como autoridade Mint), fixando o
supplytotal da Mint em 1.

- Passo 3: O atacante chamou a função de saque, passando a Mint de LP falsa no slot de conta esperado e a conta Token do Passo 2 (contendo 1 token LP falso) como origem de LP. Com
withdraw_amount = 1elp_supply = 1, o manipulador calculoutotal_coin * 1 / 1etotal_pc * 1 / 1, o que equivalia a 100% de ambas as reservas (893.700USDCe 66.837RAYpara o pool RAY/USDC).

- Passo 4: O manipulador queimou o 1 token do atacante e transferiu as reservas completas de ambos os cofres do pool, drenando completamente o pool RAY/
USDC.

O atacante repetiu o mesmo padrão contra mais três pools em aproximadamente 15 segundos. Entre todos os quatro pools, os valores drenados foram:
| Pool | Drenado (aprox.) |
|---|---|
| RAY/USDC | ~66.837 RAY + ~893.700 USDC |
| RAY/wSOL | ~74.720 RAY + ~5.603 wSOL |
| RAY/SRM | ~8.622 RAY + ~10.692 SRM |
| RAY/Sollet ETH | ~5.038 RAY + ~16 Sollet ETH |
Conclusão
A causa raiz é uma única verificação de validação de conta ausente: o manipulador de saques usou o supply de uma conta Mint fornecida pelo chamador como divisor do fornecimento de LP sem vinculá-la ao amm.lp_mint registrado no pool. No Solana, cada conta fornecida pelo chamador deve ser vinculada à sua contraparte canônica armazenada no estado do pool. Uma implementação correta deve rejeitar qualquer Mint de LP cuja chave não corresponda ao registro armazenado no pool, e calcular o resgate a partir de um contador de LP interno ao pool em vez do supply da Mint fornecida externamente. O contrato explorado era uma implantação mais antiga (última atualização em janeiro de 2023) que foi fechada no mesmo dia do ataque. De acordo com a equipe do Raydium, a compensação total será realizada pelo tesouro do Raydium [1].
Referências
Sobre a BlockSec
A BlockSec é uma provedora completa de segurança em blockchain e conformidade em criptomoedas. Desenvolvemos produtos e serviços que ajudam os 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, relatou 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.
-
Site oficial: https://blocksec.com/
-
Conta oficial no Twitter: https://twitter.com/BlockSecTeam



