Back to Blog

~$39,5M Perdidos: Allbridge, Wanchain e Mais | BlockSec Semanal

Code Auditing
July 30, 2026
20 min read
Key Insights

Durante a semana passada (2026/07/20 - 2026/07/26), os seguintes 8 incidentes de segurança notáveis são apresentados, envolvendo aproximadamente $39,5M em perdas totais.

Data Incidente Tipo Perda Estimada
2026/07/19* Allbridge Validação de Entrada Falha ~$1,65M
2026/07/20 Zilliqa Geração de Nonce Falha ~$400K
2026/07/20 Wanchain Codificação de Mensagem Falha ~$500K
2026/07/22 42DAO Comprometimento de Chave Privada ~$900K
2026/07/22 AFX Trade Comprometimento de Chave Privada ~$24,15M
2026/07/22 B² Network Comprometimento de Chave Privada ~$3,8M
2026/07/23 Verus Comprometimento de Chave Privada ~$7,6M
2026/07/24 Lien Finance Lógica de Validação Falha ~$542K

*O incidente Allbridge ocorreu em 19 de julho (17:51 UTC) e não foi coberto no relatório da semana passada. Está incluído aqui por completude.

  • Zilliqa foi selecionada porque expôs uma vulnerabilidade há muito não detectada em uma implementação de carteira off-chain que colocava usuários de contas ZIL nativas legadas em risco de comprometimento de chave privada desde 2019.
  • Allbridge foi selecionada porque demonstra uma vulnerabilidade de aliasing de conta habilitada pelo modelo de conta posicional da Solana. A vulnerabilidade teve que ser reconstruída a partir do binário do programa implantado porque o código-fonte estava indisponível.
  • Wanchain foi selecionada porque demonstra como uma bridge pode ser drenada sem comprometer a criptografia. As assinaturas eram válidas e corretamente verificadas, ainda assim uma codificação de mensagem não injetiva e sem delimitadores permitiu que uma autorização de 3.097,56 tokens fosse reinterpretada no Cardano como 203.001.692,164714 tokens.
  • Lien Finance foi selecionada porque ilustra como uma validação que verifica se os saldos correspondem às contagens sem verificar a identidade e multiplicidade dos itens correspondidos pode ser contornada.

Melhor Auditor de Segurança para Web3

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

Destaque da Semana: Allbridge Core

Este incidente é destacado porque o padrão de aliasing de conta se aplica a qualquer programa Solana que aceite a mesma conta mutável em dois papéis de instrução. O mecanismo subjacente, duas visões mutáveis do mesmo estado onde a segunda escrita silenciosamente sobrescreve a primeira, tem a mesma estrutura do clássico bug de auto-transferência ERC-20 (transferFrom onde from == to). O post-mortem do projeto [1] fornece um resumo de alto nível, mas não detalha o mecanismo em nível de código. A análise abaixo foi reconstruída a partir do binário do programa implantado para preencher essa lacuna.

Em 19 de julho de 2026 (17:51 UTC), o Allbridge Core, um protocolo de bridge cross-chain na Solana, foi explorado por aproximadamente $1,65M (~$1,12M em USDC e ~$539K em USDT). A causa raiz foi que a instrução de swap aceitou a mesma conta Pool nos papéis de envio e recebimento sem forçar que as duas sejam diferentes. Quando a mesma conta foi passada duas vezes, as atualizações de contabilidade interna de um papel foram silenciosamente sobrescritas pelo outro, enquanto as transferências reais de tokens já tinham sido liquidadas. Cinco auto-swaps distorceram o estado registrado do Pool o suficiente para o atacante converter uma pequena entrada de USDT em ~$2,24M em USDC.

Contexto

O Allbridge Core é uma bridge cross-chain que roteia swaps através de uma unidade de contabilidade interna chamada vUSD. Um swap consiste em duas etapas contábeis:

token de origem -- swap_to_v_usd(send_pool) --> vUSD
vUSD            -- swap_from_v_usd(receive_pool) --> token de destino

vUSD não é um token SPL. Ele existe apenas como um campo dentro dos dados on-chain de cada Pool. Cada Pool rastreia token_balance, v_usd_balance e reserves. A etapa de envio transfere o token de origem do usuário para o cofre da bridge de envio, aumenta a contabilidade do lado do token do Pool de envio e calcula a saída de vUSD. A etapa de recebimento adiciona esse vUSD ao Pool de recebimento, diminui sua contabilidade do lado do token e transfere o token de destino do cofre da bridge de recebimento para o usuário.

Os programas Solana recebem suas contas como um array posicional do chamador. O manipulador de instrução do programa especifica quais posições do array correspondem a quais papéis (send_pool, receive_pool, send_mint, etc.), mas o runtime não impede que um chamador passe o mesmo endereço de conta em duas posições. Quando o programa desserializa contas em duas posições diferentes, cada desserialização produz um objeto separado na memória. Mutações em um objeto não afetam o outro. Se a mesma conta for passada duas vezes, ambos os objetos começam a partir dos mesmos dados on-chain, mas divergem assim que qualquer um é mutado.

Quando o manipulador termina, a lógica de saída do programa (por exemplo, AccountsExit do Anchor) serializa cada objeto de conta de volta para os dados da conta em uma ordem fixa. Se dois objetos mapeiam para a mesma conta, a segunda serialização sobrescreve completamente a primeira.

Análise de Vulnerabilidade

O código-fonte exato para o deployment do Allbridge Core afetado estava indisponível. A análise foi reconstruída a partir do ELF de 1.770.736 bytes armazenado em ProgramData (SHA-256: 40f776...346bb6). O último slot de deployment (204.727.029) precede o slot do ataque (433.941.722).

O programa com falha é BrdgN2...ceWB. Nas instruções Swap da transação do ataque (instruções 3 a 7), os quatro pares de contas de envio e recebimento eram idênticos:

Papéis Conta
send_mint / receive_mint Es9vMF...wNYB
send_pool / receive_pool DW4a2E...wCX
send_bridge_token / receive_bridge_token 2xY9TD...vohV
send_user_token / receive_user_token 817UdW...CVct

O parser contém 16 comparações recuperadas de 32 bytes que vinculam cada papel ao seu mint, cofre, proprietário, PDA, autoridade ou programa Token esperado. No entanto, nenhuma comparação impõe send_pool.key() != receive_pool.key(). Os dois papéis de Pool são desserializados independentemente antes de o manipulador ser executado.

O ELF implantado mostra duas construções de Pool, os cálculos de envio e recebimento, e duas saídas de Pool em uma ordem fixa:

L54249-L54252  function_9881(accounts[5])     -> send_pool local
L54294-L54297  function_9881(accounts[6])     -> receive_pool local

L74177-L74195  function_12623(send_pool)      -> swap_to_v_usd
L74368-L74389  function_12940(receive_pool)   -> swap_from_v_usd

L55932-L55936  function_8300(send_pool)       -> serialização completa do Pool
L55940-L55944  function_8300(receive_pool)    -> serialização completa do Pool

Cada chamada function_9881 aloca um objeto Pool local separado de 176 bytes e copia os campos decodificados nele. Quando as chaves das contas são iguais, ambos os objetos começam a partir dos mesmos dados, mas uma mutação do objeto de envio não altera o objeto de recebimento.

Pseudo-Rust reconstruído (comportamento implantado, não código-fonte recuperado) [2] [3]:

let mut send_pool = Account::<Pool>::try_from(&accounts[5])?;
let mut receive_pool = Account::<Pool>::try_from(&accounts[6])?;

require_keys_eq!(send_pool.mint, send_mint.key());
require_keys_eq!(receive_pool.mint, receive_mint.key());

// Vinculações de cofre, proprietário, PDA, autoridade e programa Token são verificadas.
// AUSENTE: require_keys_neq!(send_pool.key(), receive_pool.key());

token::transfer(send_user_token, send_bridge_token, amount)?;
let v_usd = swap_to_v_usd(&mut send_pool, amount)?;
let receive_amount =
    swap_from_v_usd(&mut receive_pool, v_usd, receive_amount_min)?;
token::transfer(receive_bridge_token, receive_user_token, receive_amount)?;

// AccountsExit após o retorno do manipulador:
send_pool.exit(program_id)?;     // primeira escrita
receive_pool.exit(program_id)?;  // segunda escrita, sobrescreve a primeira

function_8300 inicia seu escritor na posição 0 e serializa todos os 131 bytes definidos do Pool. A primeira saída escreve send_pool; a segunda saída escreve receive_pool sobre os mesmos bytes. O estado final da conta é, portanto, o valor obsoleto do lado de recebimento, não uma mesclagem das duas atualizações locais.

As transferências de token SPL são CPIs separadas. Elas atualizam contas de cofre pertencentes ao programa SPL Token antes das saídas do Pool, portanto a segunda serialização do Pool não pode reverter a transferência de entrada. Cada swap com aliasing, portanto, deixa os tokens transferidos no cofre enquanto descarta a atualização do Pool do lado de envio correspondente. A atualização do lado de recebimento permanece e move o estado registrado do Pool para um v_usd_balance mais alto e uma contabilidade menor do lado do token.

A falha central é que o programa não verifica que send_pool e receive_pool são contas diferentes. Todas as outras validações (vinculação de mint, propriedade do cofre, derivação de PDA) passam porque ambos os papéis pertencem legitimamente ao mesmo Pool.

Análise do Ataque

A análise a seguir é baseada na transação 3LNLaG...Y39Q.

O exploit foi concluído em uma transação contendo um flash borrow do Kamino, sete instruções de Swap do Allbridge e um pagamento ao Kamino.

  • Passo 1: O atacante emprestou ~1,12M de USDC do Kamino e fez swap pelo Allbridge por ~949K de USDT (instruções 1-2). Este primeiro swap forneceu o USDT necessário para os auto-swaps seguintes e deslocou os estados dos Pools de USDC e USDT.
  • Passo 2: O atacante submeteu cinco auto-swaps de ~100K de USDT usando as contas idênticas de mint, Pool, cofre e token do usuário para envio e recebimento (instruções 3-7). Como o Pool de recebimento era serializado por último em cada chamada, a contabilidade persistida registrava a redução do lado de recebimento, mas não a adição do lado de envio. As saídas observadas diminuíram em cada chamada conforme a distorção se acumulou:
Auto-swap Entrada vUSD registrado Preço (vUSD/USDT) Saída USDT
1 ~100K ~162K ~5,24 ~47,8K
2 ~100K ~256K ~16,6 ~25,4K
3 ~100K ~471K ~57,2 ~12,5K
4 ~100K ~918K ~190 ~5,73K
5 ~100K ~1,82M ~563 ~2,36K

As cinco chamadas transferiram ~500K de USDT para o cofre e retornaram ~93,8K de USDT. O token_balance registrado do Pool caiu a cada chamada enquanto o saldo real do cofre crescia, ampliando a lacuna que o próximo passo explora.

  • Passo 3: O atacante forneceu apenas ~3,99K de USDT (instrução 8). O Pool de USDT distorcido produziu ~2,24M de vUSD, que o Pool de USDC converteu em ~2,24M de USDC. A saída inflada de vUSD foi possível porque o saldo de token registrado do Pool estava muito abaixo do saldo real do cofre após cinco rodadas de atualizações descartadas do lado de envio.
  • Passo 4: O atacante reembolsou o principal do flash loan de ~1,12M de USDC do Kamino e a taxa de ~11,2 de USDC (instrução 9). Os saldos finais do atacante foram ~1,12M de USDC e ~539K de USDT.

Conclusão

A causa raiz deste incidente foi a validação ausente de que send_pool e receive_pool se referem a contas diferentes. Um único Pool mutável foi desserializado em dois objetos de contabilidade locais, e a segunda serialização completa sobrescreveu a primeira depois que o programa Token já havia liquidado as transferências do cofre. Isso separou a contabilidade registrada do Pool do seu saldo real no cofre. Os aliases de conta, o fluxo de controle do ELF, a ordem de serialização, os logs de transação e os saldos do cofre todos suportam o mesmo mecanismo [1].

Para programas Solana, qualquer instrução que aceite o mesmo tipo de conta em dois ou mais papéis mutáveis deve impor unicidade (require_keys_neq!) ou mesclar mutações antes da saída. O sistema de restrição #[account] do framework Anchor não impõe unicidade entre papéis por padrão, portanto essa verificação deve ser adicionada explicitamente. O padrão geral, duas visões mutáveis do mesmo estado onde a segunda escrita silenciosamente sobrescreve a primeira, pode aparecer onde quer que programas gerenciem sua própria ordem de serialização.

Referências

Comece a usar o Phalcon Explorer

Mergulhe nas Transações para Agir com Sabedoria

Experimente agora gratuitamente

Mais Incidentes desta Semana

Carteira Ledger Zilliqa

Em 20 de julho de 2026, a Zilliqa observou atividade on-chain consistente com exploração ativa de contas ZIL nativas legadas, com perdas conhecidas de aproximadamente $400K. A causa raiz foi um nonce enviesado no caminho de assinatura EC-Schnorr do aplicativo Ledger da Zilliqa: o código de geração de nonce copiou os 32 bytes errados de um buffer de 40 bytes após a redução modular, fixando os 64 bits mais significativos em zero. Com várias assinaturas públicas da mesma conta, um atacante poderia recuperar a chave privada por meio de técnicas baseadas em lattice e drenar a conta.

Contexto

As transações nativas da Zilliqa usam assinaturas EC-Schnorr sobre secp256k1. Para cada assinatura, o assinante deve amostrar um nonce efêmero fresco, de largura total e imprevisível kk onde 0k<N0 \le k < N (a ordem da curva secp256k1). O fluxo de assinatura produz um compromisso Q=compress(kG)Q = \text{compress}(kG), um desafio r=SHA256(QpublicKeymsg)Nr = \text{SHA256}(Q \| \text{publicKey} \| \text{msg}) \bmod N, e uma resposta s=krxNs = k - r \cdot x \bmod N, onde xx é a chave privada. Uma vez transmitido, (r,s)(r, s) é público.

A relação linear s=krxNs = k - rx \bmod N é o ponto crítico. Uma implementação correta permanece segura porque cada kk é fresco e uniformemente distribuído. Se o nonce for enviesado ou muito pequeno, cada assinatura pública vaza informações sobre a mesma chave privada.

Análise de Vulnerabilidade

O caminho de geração de nonce relevante foi introduzido no aplicativo Ledger da Zilliqa no commit "Bug fixes based on Ledger team review". O fluxo pretendido era: gerar 40 bytes de aleatoriedade, reduzir módulo a ordem da curva NN, e usar o escalar resultante como kk. Gerar um inteiro aleatório amplo e reduzi-lo módulo a ordem da curva não é um problema por si só. A falha apareceu durante a cópia para o buffer de nonce:

unsigned char nonce[size+8];
cx_rng(nonce, size+8);
cx_math_modm(nonce, size+8, (unsigned WIDE char *) PIC(domain->n), size);
os_memcpy(T->K, nonce, size);

Após cx_math_modm, o resultado escalar de 32 bytes fica alinhado à direita dentro do buffer de 40 bytes. os_memcpy(T->K, nonce, size) copia os primeiros size (32) bytes, o que retém os 8 bytes de preenchimento zero iniciais e descarta os últimos 8 bytes de entropia. O nonce gerado, portanto, satisfaz k<2192k < 2^{192} em vez de k<N2256k < N \approx 2^{256}.

Isso significa que 64 bits de cada nonce são fixos em zero. A partir da equação de resposta Schnorr ki=si+rixNk_i = s_i + r_i x \bmod N, cada assinatura fornece uma relação linear modular pública onde o resultado é limitado por 21922^{192}. Esta é uma instância do Problema do Número Oculto (HNP). Com aproximadamente quatro ou mais assinaturas afetadas da mesma chave, um algoritmo padrão de redução de lattice recupera a chave privada em segundos em hardware comum [1].

O defeito estava presente em todas as versões lançadas do aplicativo Ledger da Zilliqa desde 2019 até o incidente ser identificado em julho de 2026 [2].

Nenhuma Análise de Ataque é fornecida para este incidente. A exploração foi realizada inteiramente off-chain: o atacante recuperou chaves privadas a partir de assinaturas on-chain publicamente disponíveis usando redução de lattice e então assinou transações de transferência padrão. Não há sequência de ataque on-chain de múltiplas etapas para analisar.

Conclusão

Este incidente foi causado por uma falha de implementação de assinatura off-chain, não por um bug de contrato inteligente on-chain. O aplicativo Ledger da Zilliqa produziu assinaturas EC-Schnorr com nonces restritos a k<2192k < 2^{192}, vazando informações estruturadas suficientes para recuperação de chave privada após várias transações nativas da mesma conta.

A mitigação primária é a aposentadoria da chave, não apenas a atualização do aplicativo Ledger. Um aplicativo corrigido previne novas assinaturas enfraquecidas, mas não pode apagar as assinaturas já públicas registradas on-chain. Qualquer chave afetada que tenha produzido assinaturas vulneráveis suficientes deve ser tratada como comprometida [2].

Para equipes de carteiras e protocolos: trate a geração e codificação de nonce como código crítico de segurança, prefira geração determinística de nonce seguindo um padrão bem revisado onde aplicável, e para usuários afetados, forneça um caminho de migração coordenado que leve em conta o front-running por atacantes que podem já deter a mesma chave privada.

Referências


Bridge Wanchain Cardano

Em 20 de julho de 2026, a bridge Cardano da Wanchain perdeu aproximadamente 515,2M de NIGHT (~$500K) devido a uma codificação de mensagem não injetiva no validador Plutus TreasuryCheck do Cardano [1]. O post-mortem do projeto [2] descreve o mecanismo de alto nível, mas não fornece detalhes de codificação em nível de byte ou código; a análise abaixo reconstrói a colisão completa. As assinaturas dos nós da bridge eram válidas e corretamente verificadas, mas a concatenação sem delimitadores de campos de comprimento variável permitiu ao atacante deslocar os limites de bytes entre dois campos numéricos adjacentes, reinterpretando uma autorização de 3.097,56 NIGHT como uma retirada de 203.001.692,164714 NIGHT.

Contexto

O componente afetado é o sistema de tesouraria cross-chain do Cardano da Wanchain. Um usuário da cadeia de origem queima ou bloqueia tokens, os nós da bridge assinam uma mensagem de autorização construída a partir dos campos do redeemer analisados, e a prova assinada é submetida ao validador Plutus TreasuryCheck do Cardano para liberar ativos da tesouraria.

Análise de Vulnerabilidade

O validador TreasuryCheck verifica a assinatura do nó da bridge sobre uma concatenação bruta de 14 campos do redeemer analisados:

hashRedeemer = sha3_256 $ mconcat
  [ toPkhPay, toPkhStk, policy, assetName
  , packInteger amount, packInteger adaAmount
  , txHash, packInteger index, packInteger mode
  , uniqueId, packInteger txType, packInteger ttl
  , packInteger outputCount, userData
  ]

A codificação inteira usada por packInteger é de comprimento variável, e a concatenação não tem prefixo de comprimento, separador de tipo ou serialização tipada com domínio separado. Diferentes tuplas semânticas podem, portanto, produzir a mesma string de bytes assinada.

No ataque representativo, os nós da bridge assinaram uma retirada normal:

amount    = 3097560000     -> packInteger = b8a103c0
adaAmount = 1206800        -> packInteger = 126a10
combinado                              = b8a103c0126a10

O atacante submeteu um redeemer Cardano analisado como:

amount    = 203001692164714 -> packInteger = b8a103c0126a
adaAmount = 16              -> packInteger = 10
combinado                               = b8a103c0126a10

As sequências de bytes são idênticas. A verificação de assinatura passou, e o contrato Cardano interpretou o redeemer como uma retirada aproximadamente 65.000 vezes maior do que autorizado.

Análise do Ataque

A análise a seguir referencia a transação representativa do Cardano 0a4861...2ea1 e a transação de origem na BSC 0xe90111...d5f26b.

  • Passo 1: O atacante criou solicitações de bridge na cadeia de origem com aparência normal. Para o caso representativo, a transação BSC queimou 3.110 NIGHT, e a API da Wanchain registrou um valor de recebimento esperado no Cardano de 3.097,56 NIGHT [3].

  • Passo 2: Os nós da bridge assinaram o hash de autorização construído a partir dos campos concatenados brutos.

  • Passo 3: O atacante alterou os limites do redeemer Cardano entre amount e adaAmount enquanto mantinha a sequência de bytes assinada inalterada.

  • Passo 4: O validador TreasuryCheck do Cardano analisou o redeemer como uma retirada de alto valor e verificou com sucesso a assinatura reutilizada. A transação pagou 203.001.692,164714 NIGHT ao atacante.

  • Passo 5: O mesmo padrão foi repetido em múltiplas transações Cardano, resultando em uma drenagem total de aproximadamente 515.206.545,426856 NIGHT (~$500K).

Conclusão

Nenhuma criptografia foi quebrada. Os assinantes produziram assinaturas válidas, e o validador TreasuryCheck as verificou corretamente. A falha estava na construção da mensagem: hashRedeemer dobrou 14 campos de comprimento variável em uma única string de bytes sem delimitador ou prefixo de comprimento, tornando a codificação não injetiva. Diferentes tuplas de campo produzem a mesma pré-imagem, o mesmo hash e a mesma assinatura válida.

A correção é tornar a codificação assinada injetiva para que exatamente uma tupla de campo possa produzir qualquer string de bytes. A codificação inteira de largura fixa ou o prefixo de comprimento de cada campo fixa os limites que o atacante moveu. Um codificador estruturado padrão alcança o mesmo resultado e é mais difícil de errar. A regra geral: assine uma serialização canônica em vez de uma concatenação bruta. Esta é a mesma classe de bug que o abi.encodePacked do Solidity com argumentos de comprimento variável, onde diferentes tuplas de entrada podem produzir sequências de bytes idênticas. Aplica-se a qualquer mensagem assinada montada a partir de campos de comprimento variável, e é especialmente relevante para bridges onde as partes que constroem e analisam a mensagem operam em sistemas diferentes.

Referências


Lien Finance

Em 24 de julho de 2026, o Lien Finance, um protocolo OTC de títulos descentralizados no Ethereum, foi explorado por aproximadamente $542K em USDC. A causa raiz foi uma falha de validação na função de troca de títulos: ela verificava que o número total de títulos compartilhados correspondia entre os grupos de entrada e saída, mas não rastreava quais títulos específicos foram correspondidos. Isso permitiu ao atacante contornar a etapa de queima para todos os títulos de entrada enquanto mintava um título não colateralizado, que foi então vendido por USDC real através dos pools OTC do protocolo.

Contexto

O Lien Finance é um protocolo DeFi que emite títulos colateralizados contra ETH. Seu contrato BondMakerCollateralizedEth permite que os usuários bloqueiem ETH como garantia para mintar tokens de título. O payoff de um título é definido por um fnMap, uma função linear por partes que mapeia o preço da garantia no vencimento para o valor que esse título paga.

A unidade significativa é o grupo de títulos. registerNewBondGroup() verifica que os payoffs de todos os títulos em um grupo somam o preço do ETH em cada ponto de quebra de seu fnMap combinado. Um grupo que satisfaz essa condição reconstitui exatamente uma unidade de garantia. É por isso que os grupos são intercambiáveis: quaisquer dois que satisfaçam a condição valem a mesma coisa, e o caminho de conversão exchangeEquivalentBonds() depende dessa garantia.

O registro de títulos e grupos é permissionless: qualquer endereço pode registrar um novo título fornecendo um vencimento e um fnMap arbitrário, e qualquer endereço pode registrar qualquer lista de IDs de títulos já registrados como um grupo, sujeito apenas à verificação de soma de payoff.

Análise de Vulnerabilidade

Os contratos com falha são 0xDA6F...BEf0 e 0x8432...7de0.

Quando um título aparece tanto no grupo de entrada quanto no de saída, queimá-lo e imediatamente remintá-lo é trabalho desperdiçado. O parâmetro exceptionBonds nomeia esses títulos para que a troca possa pular ambas as etapas. A função impõe isso com um único contador exceptionCount: ele incrementa uma vez por correspondência ao percorrer o grupo de entrada e decrementa uma vez por correspondência ao percorrer o grupo de saída, nunca registrando qual bondID correspondeu.

Isso significa que o grupo de entrada [BondA, BondB] e o grupo de saída [BondC, BondA, BondA], com exceptionBonds = [BondA, BondB], passam na validação. O contador chega a 2 no lado de entrada (uma correspondência cada para BondA e BondB) e retorna a 0 no lado de saída, mas ambos os decrementos vêm das duas entradas de BondA. A troca não queima nada e minta apenas BondC: tokens do grupo de saída criados sem nenhuma entrada consumida.

Análise do Ataque

O ataque está dividido em duas transações: o Passo 1 ocorre em 0xe8689a...284d0f, e os Passos 2-5 ocorrem em 0xb96d57...48e0e7.

  • Passo 1: O atacante chamou o registerNewBond() permissionless três vezes para definir BondA, BondB e BondC, compartilhando o mesmo vencimento. BondA e BondB são calls alavancadas dividindo a garantia igualmente abaixo de $3.200, onde os payoffs somam o preço do ETH conforme _assertBondGroup() requer. BondC é um LBT deep out-of-the-money com strike em $3.200 contra $1.870 no spot: zero de valor intrínseco, mas ainda aproximadamente $7,64/IMT de valor temporal como opção.

  • Passo 2: O atacante registrou dois grupos de títulos. registerNewBondGroup([BondA, BondB]) retornou o Grupo 36 (entrada), e registerNewBondGroup([BondC, BondA, BondA]) retornou o Grupo 37 (saída). O payoff flat-zero de BondC não acrescenta nada às somas dos pontos de quebra, portanto sua presença não perturba a condição de equivalência. BondA é listado duas vezes no Grupo 37, o que faz a troca passar na validação.

  • Passo 3: O atacante chamou exchangeEquivalentBonds(inputGroup = 36, outputGroup = 37, amount = 6968565865905, exceptionBonds = [BondA, BondB]). Percorrendo o grupo de entrada: BondA e BondB correspondem cada um a uma exceção, portanto ambos pulam a queima e exceptionCount chega a 2. Percorrendo o grupo de saída: BondC não corresponde a nada e é mintado, então as duas entradas de BondA correspondem cada uma a uma exceção e decrementam o contador para 0.

  • Passo 4: O atacante vendeu o BondC mintado por USDC através dos pools OTC do protocolo (GeneralizedDotc), obtendo 532.144 USDC.

  • Passo 5: O atacante repetiu o mesmo padrão contra o segundo contrato BondMakerCollateralizedEth, levando o lucro total a aproximadamente 542.144,63 USDC.

Conclusão

O mecanismo exceptionBonds, destinado a pular a re-mintagem de títulos compartilhados, poderia ser aplicado a todos os títulos de entrada de uma vez duplicando um bondID no grupo de saída. Isso quebrou o invariante de que tokens de título são criados apenas contra garantias depositadas via issueNewBonds(). A correção é rastrear quais bondIDs específicos foram correspondidos (por exemplo, usando um bitmap ou conjunto), garantindo que cada exceção seja consumida exatamente uma vez em ambos os grupos.

Comece a usar o Phalcon Security

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

Experimente agora gratuitamente

Sobre a BlockSec

A BlockSec é um provedor completo de segurança blockchain e conformidade cripto. Construímos 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 blockchain em conferências de prestígio, reportou vários ataques de dia zero 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