Back to Blog

~US$23 milhões perdidos: exploits no Cosmos EVM e Moonwell | BlockSec Weekly

Code Auditing
3 de setembro de 2026
30 min read
Key Insights
  • Cinco incidentes esta semana causaram perdas de aproximadamente $22,7M nas redes Ethereum, Solana, Base, Cronos, e em outras seis chains Cosmos EVM, como a TAC.

  • Código compartilhado transformou dois incidentes em eventos multiprotocolo. A falha no tratamento de saldos da Cosmos EVM explorou seis chains Cosmos EVM na mesma semana, incluindo a TAC Chain; o contrato de cartão desatualizado da Rain expôs a Avici, a Tria e outros programas de cartão. Um defeito em uma infraestrutura comum pode se propagar para todos os sistemas downstream vulneráveis que a utilizam.

  • A manipulação de preços foi o tipo de ataque mais comum, presente em dois dos cinco incidentes e responsável por uma grande parte das perdas da semana. Os exploits da Moonwell e da Tectonic seguiram o mesmo padrão em mercados no estilo Compound: cada um inflou tanto o preço de um oráculo quanto a taxa de câmbio do token de recibo do mercado, e então tomou empréstimos usando o colateral sobrevalorizado como garantia.

Durante o período do relatório (22/08/2026 - 30/08/2026), observamos 5 incidentes de segurança com uma perda total estimada de aproximadamente $22,7M.

Data Incidente Tipo Perda Estimada
20/08/2026* A Série de Explorações da Cosmos EVM (MANTRA, TAC, KiiChain, +3) Underflow/Overflow Aritmético ~$5,7M*
27/08/2026 Moonwell Manipulação de Preço ~$9,1M
28/08/2026 Ajna Lógica de Negócio Inadequada ~$775 mil
28/08/2026 Série de Explorações do Contrato do Rain Card (Avici, Tria, e outros) Bypass de Verificação de Assinatura ~$1,1M†
30/08/2026 Tectonic Manipulação de Preço ~$6M‡

*A série da Cosmos EVM começou em 20/08 (MANTRA), antes do período coberto pelo relatório desta semana, e não foi abordada no relatório da semana passada; está incluída aqui para fins de integridade. O valor de ~$5,7M é o que os atacantes realizaram nas seis cadeias afetadas (aproximadamente $2,87M via DEXes e ~$2,85M via exchanges centralizadas, desde então congelados), a preços de 19 de agosto, de acordo com o post-mortem oficial da Cosmos. As perdas nominais foram maiores nos casos em que o valor do token é conhecido (TAC ~3 bilhões de TAC, ~$7,5M; MANTRA 720,9 milhões de tokens, ~$3,6M; KiiChain 148,3 milhões de KII), mas a maioria dos tokens não foi vendida, permanecendo congelada ou recuperável on-chain.

†O valor de ~$1,1M é a estimativa agregada entre os programas suportados pela Rain expostos pelo contrato compartilhado; Avici e Tria são os dois maiores, divulgando aproximadamente $500.859 (1.685 usuários) e $431.945 (636 usuários), respectivamente.

‡O valor de ~$6M é a perda realizada, transferida via bridge para a Ethereum antes do rollback. As estimativas do total drenado variam de ~$74M (rastreados nas carteiras do atacante) a ~$119,5M (saída bruta de mercado); a maior parte permaneceu na Cronos e foi eliminada quando os validadores reverteram a cadeia ao seu estado anterior à exploração. Nem a Tectonic nem a Cronos confirmaram um valor final de perda.

Melhor Auditor de Segurança para Web3

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

Destaque da Semana: A Série de Explorações da Cosmos EVM (Rastreada na Cadeia TAC)

Esta série foi selecionada pelo que revela sobre a divulgação de infraestrutura compartilhada: como o bug foi mal avaliado como não sendo uma ameaça séria à segurança, sua correção foi lançada como um patch público silencioso em vez de por meio de distribuição privada coordenada, e um fork de terceiros então descreveu abertamente o caminho de exploração, expondo todas as cadeias não corrigidas que ainda executavam o módulo.

Entre 20/08/2026 e 25/08, os atacantes executaram uma única cadeia de exploração combinando duas vulnerabilidades no módulo compartilhado cosmos/evm em seis cadeias Cosmos EVM. Ambos os bugs estavam no código que reconcilia o estado da EVM com o livro-razão x/bank da Cosmos: um underflow de saldo e um overflow correspondente, encadeados dentro de uma única transação neutra em termos de suprimento.

De acordo com o post-mortem oficial da Cosmos, o bug foi reportado através do programa de recompensas em abril e inicialmente mal avaliado como não ameaçador aos fundos em produção, então passou pelo processo de patch público silencioso e foi lançado nas versões v0.6.2 e v0.7.2 em 19/08. Em 20/08, um fork de terceiros descreveu publicamente o caminho de exploração, e as primeiras drenagens começaram horas depois, atingindo primeiro a MANTRA (20/08), depois a TAC e a KiiChain (22/08) [1][2]. Em todas as seis cadeias, os atacantes realizaram aproximadamente $5,7M a preços de 19 de agosto (cerca de $2,87M vendidos em DEXes e ~$2,85M em exchanges centralizadas, desde então congelados) [1].

Este relatório analisa a TAC Chain em detalhes como exemplo resolvido; foi a cadeia mais atingida da série, com uma perda no pool de staking de aproximadamente $7,5M nominais [3].

Contexto

A TAC Chain executa tanto o Cosmos SDK quanto a EVM. Um endereço tac1... e um endereço 0x... compartilham os mesmos 20 bytes subjacentes, então um único endereço pode primeiro ser criado como uma conta de vesting da Cosmos e depois ter um contrato EVM implantado nele. Estas não são duas contas separadas: o mesmo endereço carrega simultaneamente o estado de vesting e o código do contrato. Os dois funcionam lado a lado, e a TAC expõe ações nativas da Cosmos, como staking, para chamadores EVM por meio de contratos pré-compilados em endereços fixos, de modo que um contrato EVM pode invocá-los como qualquer outra chamada.

O saldo total no Bank de uma conta de vesting inclui tokens bloqueados. A parte bloqueada não pode ser transferida antes de sofrer vesting, enquanto o saldo disponível é o que resta após subtrair o valor bloqueado do total. Quando a EVM carrega uma conta, ela inicializa o saldo do StateDB a partir do saldo disponível, e as transferências de contratos durante a execução operam sobre esse saldo.

Ao final de uma transação, as mudanças de saldo no StateDB são liquidadas de volta no x/bank, o livro-razão autoritativo, e apenas o que é liquidado ali é TAC real e transferível. A TAC executa a linha v0.7.x, que escreve um saldo da EVM diretamente no x/bank, mas somente se o valor sobreviver a uma conversão de uint256 para int256, portanto um saldo próximo a 2^256 não pode ser liquidado de forma alguma.

Tokens bloqueados não podem ser transferidos, mas ainda podem ser delegados para staking. A delegação verifica o saldo total no Bank, que inclui tokens bloqueados, e então mantém a restrição de vesting através de DelegatedVesting. Para uma conta totalmente bloqueada com total=1, delegar essa unidade reduz o saldo total no Bank para 0 e atualiza DelegatedVesting, enquanto o saldo disponível permanece corretamente em 0 tanto antes quanto depois.

Análise da Vulnerabilidade

O componente com o bug é a sincronização de saldo no manipulador compartilhado cosmos/evm, que é executado a cada chamada de precompilado de staking com alteração de estado, exposto através do precompilado de staking em 0x0000...0800 e implementado em [4]. Ele reconcilia saldos da EVM com o livro-razão da Cosmos através de aritmética uint256 não verificada, e isso expõe dois defeitos complementares: um underflow de saldo no caminho de gravação de retorno do staking, e um overflow correspondente no caminho de adição comum. Nenhum dos dois é perigoso isoladamente; o risco vem da combinação de ambos em um caminho aritmético compartilhado.

O primeiro defeito é um underflow na gravação de retorno do saldo. Quando uma chamada de precompilado de staking com alteração de estado é executada, a ação nativa da Cosmos é executada entre um hook BeforeBalanceChange e um AfterBalanceChange, e o AfterBalanceChange é responsável por refletir a mudança de saldo nativa de volta no StateDB da EVM.

A delegação é validada contra o saldo total no Bank, então uma conta totalmente bloqueada pode passar na verificação de delegação enquanto seu saldo disponível permanece em 0. A delegação nativa deduz o valor delegado do saldo total no Bank e emite um evento coin_spent. O defeito está na forma como o AfterBalanceChange consome esse evento: em vez de recarregar o saldo disponível atual da conta e atribuí-lo, ele reproduz o valor do coin_spent como stateDB.SubBalance(spender, amount), subtraindo do saldo existente na EVM.

Como o SubBalance realiza a subtração com aritmética uint256 não verificada, qualquer conta cujo saldo na EVM seja menor que o valor do evento sofre underflow. Para uma conta com saldo na EVM igual a 0 e um valor de coin_spent de 1, 0 - 1 retorna a MAX_UINT256.

O segundo defeito é um overflow correspondente no caminho de adição. Toda transferência de saldo executa SubBalance no remetente e AddBalance no destinatário através do mesmo StateDB, de modo que ambas as direções compartilham um único caminho aritmético.

Ambos resolvem para as mesmas primitivas stateObject, onde AddBalance e SubBalance calculam new(uint256.Int).Add(s.Balance(), amount) e .Sub(s.Balance(), amount) sem nenhuma verificação de intervalo. Assim como a subtração sofre underflow abaixo de 0, uma adição suficientemente grande sofre overflow acima de MAX_UINT256 e retorna para um valor menor.

Os dois defeitos são complementares. Isoladamente, o underflow produz um saldo MAX_UINT256 que é inerte, porque um valor próximo a 2^256 não pode sobreviver à conversão de uint256 para int256 exigida pela liquidação no x/bank. A adição não verificada é a contrapartida: é o único caminho que pode trazer um saldo tão desproporcional de volta abaixo desse teto de liquidação.

Análise do Ataque

A análise a seguir é baseada na transação 0xae4e9b...da46fc.

  • Passo 1: Na transação 0x4da591...df1af7, o atacante implantou uma fábrica CREATE2 para que o endereço do contrato de ataque 0x5711...c978 pudesse ser calculado antes de o contrato ser implantado.

  • Passo 2: Na transação 95F43742...6A885BA, o atacante usou MsgCreateVestingAccount para transferir e bloquear uma unidade base (1utac) naquele futuro endereço do contrato, dando a ele um saldo total no Bank que poderia passar na verificação de delegação enquanto mantinha seu saldo disponível em 0.

  • Passo 3: Na transação 0x2400f8...c57c81, o atacante usou CREATE2 para implantar o contrato de ataque no mesmo endereço 0x5711...c978, tornando-o simultaneamente uma conta de vesting da Cosmos e um contrato EVM, de modo que uma conta externa pudesse pagar o gás enquanto o endereço do contrato permanecia como o delegador com spendable=0.
  • Passo 4: O contrato de ataque delegou a unidade base bloqueada através do precompilado de staking. O fluxo de staking aceitou a delegação com base no saldo total no Bank, e a gravação de retorno do saldo então fez o saldo na EVM do contrato retornar a MAX_UINT256.

  • Passo 5: O saldo retornado de MAX_UINT256 não poderia ser liquidado de volta ao livro-razão da Cosmos como estava, então o contrato de ataque primeiro o trouxe para um valor liquidável. Ele enviou quase todo o saldo para bonded_tokens_pool, a maior conta da cadeia e a que detinha todo o TAC em staking, escolhendo o valor de modo que o saldo do pool sofresse overflow na adição e retornasse a 0. Como esse valor foi deduzido do próprio MAX_UINT256 do contrato de ataque, o contrato ficou com exatamente o antigo saldo do pool, sem criação de novo suprimento. Zerar o pool consome todo o seu saldo, o máximo que esse overflow poderia render, e é por isso que o atacante mirou na maior conta da cadeia em primeiro lugar.

  • Passo 6: O contrato de ataque então transferiu esse saldo, 2.985.651.403,40 TAC, para o endereço do atacante.

Nossa análise em nível de transação corresponde ao post-mortem oficial da Cosmos, que descreve duas vulnerabilidades encadeadas: o underflow acima produz o saldo anômalo, e o overflow no Passo 5 da Análise do Ataque o converte nos fundos reais do pool [1]. Um post-mortem anterior da KiiChain, publicado antes desse relatório oficial, sustentava que pelo menos três defeitos upstream estavam envolvidos e que apenas o underflow havia sido corrigido [2]. A cobertura independente da imprensa resumiu o mesmo desacordo sobre quantos defeitos upstream permanecem [5].

Conclusão

A causa raiz da Série de Explorações da Cosmos EVM foi uma inconsistência na forma como as camadas EVM e Cosmos contabilizavam o mesmo saldo, combinada com uma aritmética que nunca teve seus limites verificados. Quando dois ambientes de execução compartilham um único livro-razão, eles precisam concordar sobre a semântica de saldo até o nível de cada conta individual, e toda mudança de saldo deve ser verificada quanto a overflow e underflow. Como a falha residia em um módulo compartilhado, e não no código de uma única cadeia, um único defeito expôs todas as cadeias que o executavam, e é isso que transformou um único bug em um evento multi-cadeia. Além do código, o incidente é uma lição sobre avaliação de severidade. A vulnerabilidade foi inicialmente julgada como não ameaçadora aos fundos em produção, então sua correção foi lançada como um patch público silencioso; quando esse julgamento foi corrigido, o patch já era público, e a divulgação do caminho de exploração por um terceiro transformou o erro de avaliação em seis cadeias exploradas. Um bug de infraestrutura compartilhada capaz de movimentar fundos reais precisa de distribuição privada e coordenada desde o início, não de um patch público silencioso que qualquer um possa decodificar.

Comece a Usar o Phalcon Explorer

Mergulhe nas Transações para Agir com Sabedoria

Experimente grátis agora

Mais Incidentes Desta Semana


Moonwell

Em 27/08/2026, a Moonwell na Base foi explorada por cerca de $9,1M, combinando a inflação da contabilidade de garantias com a manipulação do preço do oráculo do MAMO, um ativo de baixa liquidez listado em seu Core Market. Além de elevar o preço do MAMO, o atacante transferiu MAMO diretamente para o contrato do mercado mMAMO sem cunhar cotas (shares), o que elevou o lastro de cada cota e inflou o valor da garantia por cima do movimento de preço. Contra a garantia duplamente inflada, o atacante realizou empréstimos brutos de aproximadamente $11,03M em cbBTC, WETH, USDC e wstETH, deixando cerca de $9,13M em obrigações residuais após as liquidações [6][7].

Análise da Vulnerabilidade

O Core Market da Moonwell é executado sobre o código do Compound v2, onde uma cota do mercado (mMAMO) vale (cash + totalBorrows - totalReserves) / totalSupply. Duas fraquezas se combinam aqui. Primeiro, o limite de suprimento do MAMO verifica apenas o caminho formal de cunhagem, então uma transferência direta de MAMO para o contrato mMAMO adiciona ao caixa do mercado sem cunhar cotas; isso eleva a taxa de câmbio calculada (exchangeRateStored()) e aumenta o valor da garantia de cada cota existente, contornando totalmente o limite. Segundo, o MAMO foi listado como garantia com um fator de garantia de 50%, apesar da liquidez rasa, de modo que seu preço no oráculo pode ser movido com capital modesto. Como o valor da garantia é calculado como cotas vezes taxa de câmbio vezes preço do oráculo, tanto a taxa de câmbio quanto o preço são superfícies manipuláveis, e inflá-las juntas multiplica o poder de empréstimo contra ativos muito mais líquidos.

Análise do Ataque

A análise a seguir é baseada na transação 0x09687d...395593e. A operação foi iniciada com cerca de $1,947M (799 ETH convertidos em USDC e transferidos via bridge para a Base); o volume bruto de compra de MAMO chegou a cerca de $7,50M assim que os ativos emprestados foram reciclados.

  • Passo 1: O atacante forneceu MAMO formalmente para cunhar mMAMO, depois transferiu 53.393.290 MAMO diretamente para o contrato mMAMO em duas transações que não emitiram nenhum evento Mint, elevando a taxa de câmbio calculada do mercado em cerca de 3,68x e reavaliando as cotas mMAMO que o atacante acabara de cunhar, junto com as de todos os outros detentores.

  • Passo 2: O atacante comprou MAMO em pools de DEX enquanto a liquidez estava rasa, elevando o feed de MAMO/USD de cerca de $0,0106 para cerca de $0,43.

  • Passo 3: Com a taxa de câmbio e o preço ambos inflados, o atacante concluiu 18 empréstimos em cbBTC, WETH, USDC e wstETH (cerca de $11,03M brutos), depois converteu e consolidou os lucros, transferindo via bridge cerca de 8,729M USDC para a Ethereum através do CCTP e convertendo-os em cerca de 8,728M DAI.

Conclusão

A causa raiz foi que a Moonwell avaliava um ativo de garantia de baixa liquidez em duas superfícies independentemente manipuláveis ao mesmo tempo: seu preço no oráculo, que a liquidez rasa permitiu ao atacante mover com capital modesto, e a taxa de câmbio de seu token de recibo, que uma transferência direta do ativo subjacente inflou porque o limite de suprimento protegia apenas o caminho formal de cunhagem. Como o valor da garantia multiplica os dois, inflar ambos juntos multiplicou o poder de empréstimo contra ativos muito mais líquidos. Um mercado de empréstimos deve tratar tanto o preço quanto a contabilidade de cotas da garantia como manipuláveis: excluir transferências não solicitadas do cálculo da taxa de câmbio, e combinar fatores de garantia conservadores com precificação sensível à liquidez e limites rígidos de suprimento e empréstimo para ativos rasos.

Ajna

Em 28/08/2026, a Ajna foi explorada por cerca de $775 mil em sete pools na Ethereum, através de uma falha de lógica de negócio em seu caminho de liquidação. A Ajna é um protocolo de empréstimos que não usa nenhum oráculo externo; ela precifica posições através do LUP (Lowest Utilized Price, Menor Preço Utilizado) derivado da liquidez dos buckets. O atacante primeiro manipulou o LUP para criar uma posição severamente insolvente, depois fez o protocolo liquidar uma posição controlada a um preço de leilão holandês que o protocolo ainda mantinha muito acima do mercado, de modo que as reivindicações de depósito do bucket foram consumidas pelo seu valor de face em token de cotação para pagar a dívida, enquanto o atacante recebia garantia real em troca [8].

Contexto

A Ajna é um protocolo sem permissão onde qualquer pessoa pode criar um pool de tokens pareados para fornecer e tomar empréstimos, similar a um pool da Uniswap. Cada pool é dividido em buckets, similares aos ticks de preço na Uniswap V3, onde cada tick representa uma quantidade fixa de token de cotação por unidade de garantia. Os credores escolhem um bucket de acordo com sua relação preferida entre empréstimo e valor (loan-to-value) e fornecem liquidez ali, recebendo LP (uma reivindicação sobre os depósitos daquele bucket) em troca. Como não há oráculo externo, o LUP é derivado diretamente da distribuição de depósitos dos buckets e da dívida total.

Uma posição se torna liquidável assim que seu threshold price (sua dívida dividida por sua garantia) sobe acima do LUP. Qualquer pessoa pode então dar um kick nela, colocando-a em liquidação através do depósito de um bond, o que abre um leilão holandês. O preço do leilão não está atrelado ao mercado à vista: começa em 32 vezes um preço de referência e reduz pela metade a cada hora, e é mantido constante durante a primeira hora (um período de cura) antes de começar a decair.

A garantia leiloada pode ser adquirida de duas formas. Um chamador pode fazer take() da garantia, pagando token de cotação ao preço atual do leilão, ou chamar bucketTake(), que usa os depósitos de token de cotação de um bucket escolhido para comprar a garantia leiloada e pagar o tomador do empréstimo. Em um bucketTake(), a garantia é creditada naquele bucket, então são seus credores que a adquirem; para um bucket take de arbitragem, o chamador é adicionalmente pago com recompensas em LP (uma reivindicação sobre aquele bucket) sobre a diferença entre o preço do bucket e o preço do leilão, como incentivo para acionar a liquidação. O design assume que um tomador só entra em ação quando o preço do leilão caiu até ou abaixo do que a garantia realmente vale: sem oráculo, esse leilão descendente é a forma como a Ajna descobre um preço justo, e os credores de um bucket lucram ao adquirir garantia abaixo do preço de seu próprio bucket. O leilão em si só termina quando a dívida do tomador é quitada ou o empréstimo volta a estar garantido; um caminho de liquidação separado resolve um leilão após um período de carência de 72 horas, ou antes, assim que nenhuma garantia restar, absorvendo qualquer dívida ruim residual e devolvendo a garantia remanescente ao tomador.

Três detalhes de implementação desse mecanismo determinam os valores que um take produz. Primeiro, o preço do leilão não é derivado do mercado; decai como 32 * max(kickMomp, neutralPrice) * 2^(-max(elapsedHours - 1, 0)), então permanece constante durante o período de cura da primeira hora e só então começa a cair, permanecendo próximo a 32 vezes seu preço de referência logo após esse período.

Segundo, um kick só é admitido quando o tomador já está subgarantido com base em sua dívida pré-penalidade: _kick() executa a verificação _isCollateralized() e reverte com BorrowerOk() antes de adicionar a penalidade de juros de três meses, de modo que essa penalidade eleva a dívida pós-kick registrada sem ter ajudado a posição a se qualificar.

Terceiro, o primeiro take em um leilão adiciona uma penalidade de 7% à dívida do tomador antes que os valores de pagamento e de garantia para o take sejam calculados, de modo que um único bucketTake() não precisa, por si só, quitar a dívida.

Análise da Vulnerabilidade

O contrato com o bug é o pool da Ajna (0xad24...178e); a lógica de liquidação descrita aqui é extraída dessa fonte verificada. Dentro do bucketTake(), o protocolo usa as reivindicações de depósito de um bucket pelo seu valor de face em token de cotação para pagar a dívida de um tomador em leilão, e, em um bucket take de arbitragem, paga ao tomador (taker) recompensas em LP com base na diferença entre o preço do bucket e o preço do leilão.

Ele nunca verifica o valor real recuperável dessas reivindicações de depósito, nem se o preço do leilão é economicamente razoável; simplesmente assume que as reivindicações poderão ser resgatadas depois pelo valor de face. Os dois preços dos quais depende são calculados de forma independente, e nenhum deles é validado em relação ao outro: o LUP segue a distribuição de depósitos dos buckets e a dívida total do pool, enquanto o preço do leilão decai puramente com base no tempo decorrido a partir de um preço de referência fixado no kick. O valor de face on-chain de uma reivindicação pode, portanto, divergir do que ela realmente pode recuperar, e o bucketTake() ainda assim quita a dívida com base nesse valor de face.

Internamente, o bucketTake() passa por _takeBucket() até _rewardBucketTake(), onde, em um take de arbitragem, a recompensa em LP do tomador é calculada como a garantia retirada multiplicada pela diferença entre o preço do bucket e o preço do leilão.

Análise do Ataque

O que segue é baseado na análise on-chain do pool cbETH, um dos sete afetados, usando as transações 0x8a8793...016e64 e 0x12dfde...14e4f5.

  • Passo 1: Na transação de preparação, o atacante adicionou cerca de 49,343 WETH de liquidez de cotação ao bucket de índice 2000, um preço muito acima do mercado. Apoiado por esse bucket inflado, o atacante abriu uma posição quase sem garantia, dando como colateral cerca de 0,001 cbETH para tomar emprestado cerca de 49,319 WETH. Essa posição detém quase nenhuma garantia, então não é o prêmio; ela serve a dois propósitos de preparação. Primeiro, como uma dívida insolvente, ela arrasta o LUP para baixo assim que o mercado retorna ao normal. Segundo, como quase todo o valor de 49,343 WETH depositado no bucket 2000 já foi tomado emprestado de volta, deixando apenas uma quantia residual, o bucket 2000 fica com uma reivindicação de depósito cujo valor de face, cerca de 49,343 WETH, excede em muito o que ele realmente pode recuperar; essa reivindicação prejudicada é a munição que o atacante mais tarde gasta pelo valor de face no Passo 4.
  • Passo 2: Na mesma transação de preparação, o atacante usou um segundo endereço controlado, 0x02d329...6f5f, para abrir uma posição de tomador de empréstimo no nível de preço normal, dando como colateral cerca de 48,128 cbETH e tomando emprestado cerca de 49,319 WETH contra isso. Isso trouxe o preço de mercado de volta a uma faixa normal, deixando a primeira posição severamente insolvente, enquanto a segunda posição ficava bem próxima de seu limite de saúde. Essa segunda posição, que detém a garantia real, é aquela que o atacante realmente pretende drenar; a primeira posição insolvente é apenas a alavanca que movimentou o LUP.

  • Passo 3: O atacante então deu um kick na segunda posição (a aberta por 0x02d329...6f5f) para o leilão, em vez da primeira, insolvente. Um kick só é aceito se a posição já estiver subgarantida com base em sua dívida pré-penalidade em relação ao LUP atual, e a primeira posição insolvente já havia, a essa altura, arrastado o LUP o suficiente para que a dívida acumulada da segunda posição atingisse esse limite. Somente após a verificação de elegibilidade o protocolo adiciona uma penalidade de kick equivalente a três meses de juros, razão pela qual a dívida pós-kick registrada chega a cerca de 49,362 WETH. O kick abriu o leilão holandês da posição.

  • Passo 4: O atacante esperou até logo após o período de cura de uma hora, quando o preço do leilão havia apenas começado a decair e ainda estava próximo de 32 vezes o preço de referência. Chamando bucketTake() na segunda posição no bucket 2000, o mesmo bucket que agora mantinha a reivindicação prejudicada, ele a liquidou naquele preço ainda alto, muito acima do mercado. Como esse foi o primeiro take no leilão, o protocolo adicionou uma penalidade de 7% à dívida do tomador antes de calcular os valores de pagamento e garantia do take. A garantia é precificada ao preço do leilão, então o consumo de cerca de 49,34 WETH dos depósitos do bucket 2000 pagou apenas parte dessa dívida e removeu apenas cerca de 1,51 cbETH de garantia, deixando a maior parte da garantia intocada, enquanto o chamador coletava uma grande quantidade de recompensas em LP sobre a diferença.
  • Passo 5: O atacante resgatou essas recompensas em LP através de removeCollateral(), retirando parte do lucro em forma de garantia.

  • Passo 6: Para fechar a operação, o atacante tomou um flash loan na Balancer e chamou o take() da Ajna para pagar a dívida residual que o bucketTake() havia deixado, dessa vez com token de cotação real, até que a dívida do tomador chegasse a zero e o leilão fosse encerrado. Uma chamada final a repayDebt() então retirou a garantia agora desonerada, cerca de 46,51 cbETH, com quoteRepaid=0: nenhum token de cotação foi devolvido em troca. O ganho veio às custas do pool. O déficit não desapareceu, apenas se deslocou: com a segunda posição encerrada, o empréstimo ainda não pago e quase sem garantia da primeira posição permaneceu no pool como dívida ruim, arcada pelos demais credores.

Conclusão

A causa raiz é que o caminho de liquidação da Ajna quita a dívida de um tomador em leilão contra as reivindicações de depósito de um bucket pelo valor de face, sem verificar seu valor real recuperável ou se o preço do leilão é economicamente razoável. Essa verificação ausente é o que permitiu que uma reivindicação prejudicada, forjada propositalmente, fosse gasta pelo valor de face para drenar garantia real, deixando o déficit no pool como dívida ruim. O caminho de liquidação deveria avaliar as reivindicações de depósito por seu valor real recuperável, em vez do valor de face, confirmar se o preço do leilão é economicamente razoável antes de concluir um take, e se proteger contra o consumo de reivindicações que a dívida ruim já existente de um pool tenha prejudicado. Os contratos do pool afetado são imutáveis e não expõem nenhuma pausa administrativa, então, uma vez que a drenagem começou, não havia como interrompê-la; os usuários só podiam sair por conta própria.

A Série de Explorações do Contrato do Rain Card (Rastreada na Avici)

Em 28/08/2026 (UTC), uma versão desatualizada do programa compartilhado de garantia de cartão da Rain na Solana foi explorada através de um bypass na verificação de assinatura Ed25519. Ao fazer o programa aceitar uma aprovação de administrador fabricada, o atacante assumiu o controle das contas de garantia dos usuários e drenou os saldos de tokens mantidos nelas. Como a falha estava no código do programa compartilhado entre os programas de cartão movidos pela Rain, um único bug expôs todos eles de uma vez: a exploração drenou uma estimativa de ~$1,1M no total, sendo Avici e Tria os dois maiores, divulgando cerca de $500.859 (1.685 usuários) e $431.945 (636 usuários), respectivamente [9].

A análise abaixo usa a Avici como exemplo resolvido.

Contexto

Na Solana, as verificações de assinatura são realizadas por um precompilado nativo Ed25519 como parte do processamento da transação: se uma assinatura referenciada for inválida, toda a transação falha. Um programa de negócio não recebe esse resultado diretamente; ele inspeciona as outras instruções na mesma transação (através do sysvar de Instructions) e confia que a validação foi aprovada. Quando um usuário recarrega um cartão movido pela Rain (como no caso da Avici), o saldo é mantido em uma conta de garantia por usuário, gerenciada pelo programa compartilhado da Rain, e a autoridade de token dessa conta é derivada do programa, de modo que o administrador da conta pode movimentar os ativos nela através do fluxo de transferência do programa.

Alterar o administrador de uma conta de garantia exige duas assinaturas. Uma deve vir do administrador designado pelo protocolo; o outro assinante não tem nenhum requisito especial de identidade. Esse design depende de autorização fora da cadeia (off-chain): uma vez que o administrador do protocolo assinou a mensagem de mudança de administrador, o programa a trata como aprovada.

Uma instrução de verificação Ed25519 começa com um byte de contagem das assinaturas a serem verificadas e um byte de preenchimento, seguido por uma estrutura Ed25519SignatureOffsets para cada assinatura. Cada estrutura especifica não apenas os offsets de byte da assinatura, da chave pública e da mensagem, mas também o índice da instrução de onde cada um desses deve ser lido [10]. Esses índices são um recurso intencional: eles permitem que uma verificação leia suas entradas a partir dos dados de qualquer instrução indexada na transação, por exemplo, para verificar uma assinatura sobre os dados de outra instrução sem copiá-los. O verificador nativo simplesmente lê o que quer que os offsets e índices de instrução apontem.

Análise da Vulnerabilidade

A falha está no programa de garantia (3zVB...yBzDuc): ele confia em uma chave pública sem confirmar que essa chave é a que o verificador realmente checou. Para registrar o signatário que aprova a mudança de administrador, o programa lê uma chave pública de uma posição fixa dentro da instrução de verificação Ed25519 que inspeciona, e então toma a mera presença de uma verificação bem-sucedida como prova de que o portador dessa chave assinou a mensagem de mudança de administrador.

O que ele nunca confirma é para onde essa verificação realmente apontou. Como esses campos de índice de instrução são controlados pelo chamador, e o runtime entrega os dados de cada instrução ao verificador nativo, a instrução de verificação pode conter uma chave pública em seu próprio corpo, enquanto seus índices de instrução direcionam o verificador para uma instrução diferente, verificando uma assinatura sob uma chave pública diferente sobre uma mensagem obtida de outra origem.

Assim, existem duas leituras da "chave do administrador" sem nada que as vincule: o programa confia na chave presente na instrução que ele inspeciona, enquanto o verificador só checou o que quer que os índices apontassem. A chave do administrador pode estar presente na transação como dado, mesmo que nenhuma assinatura válida sob essa chave tenha sido realmente verificada. Esse desacoplamento é a vulnerabilidade, e a verificação ausente é a vinculação que forçaria a chave pública e a mensagem que o verificador realmente verificou a serem a mesma chave e mensagem em que o programa confia.

Análise do Ataque

A análise a seguir é baseada na transação ZmpBgn...mqWL.

  • Passo 1: O atacante construiu uma transação com duas instruções de verificação Ed25519 seguidas de SubmitSignatures. A instrução 0 continha a própria chave pública do atacante (cafa…53db) e uma assinatura real sobre a mensagem de mudança de administrador, dando à transação uma verificação Ed25519 genuinamente válida.
  • Passo 2: A instrução 1 colocou a chave pública do administrador do protocolo (a2fc…959a) no slot de chave pública, mas preencheu o slot de assinatura com bytes falsos 0x09, de modo que essa instrução falharia se seus próprios dados fossem realmente verificados.

  • Passo 3: O atacante definiu o cabeçalho Ed25519 da instrução 1 como 01003000000010000000700020000000. Somente os três campos de índice de instrução realizam o redirecionamento, e, decodificados, todos são zero, então a assinatura, a chave pública e a mensagem são todas lidas da instrução 0 (os offsets de byte ainda apontam dentro dos dados dessa instrução):

    num_signatures      = 1,    padding = 0
    signature_offset    = 48,   signature_instruction_index   = 0
    public_key_offset   = 16,   public_key_instruction_index  = 0
    message_data_offset = 112,  message_data_size = 32,  message_instruction_index = 0

    A segunda verificação, portanto, releu a instrução 0 e reverificou a própria assinatura válida do atacante, em vez do payload inválido 0x09 na instrução 1.

  • Passo 4: O atacante chamou SubmitSignatures. O programa viu duas verificações Ed25519 bem-sucedidas, mas, ao registrar o segundo signatário, leu a chave pública do administrador do protocolo embutida na instrução 1, então aceitou essa chave de administrador como um segundo signatário aprovado.

  • Passo 5: Com essa aprovação falsa registrada, o atacante usou o fluxo de mudança de administrador para definir o atacante como administrador da conta de garantia da vítima, convertendo o bypass de verificação em controle direto dessa conta.

  • Passo 6: Tendo validado o chamador como o administrador da garantia, o programa moveu os ativos para fora da conta de token de garantia do cartão do usuário através de seu fluxo de transferência, invocando a transferência com seu próprio endereço derivado do programa (PDA) como a autoridade dessa conta de token.

Conclusão

A causa raiz da Série de Explorações do Contrato do Rain Card foi que o programa de garantia confirmava que uma verificação Ed25519 nativa havia sido executada, mas nunca confirmava que a chave pública e a mensagem em que confiava eram as que o verificador realmente havia checado. Um programa que depende da verificação nativa de assinatura deve vincular os dados de autorização que consome a essa verificação: ou exigir um layout Ed25519 autocontido que leia suas entradas da instrução atual, ou resolver toda instrução referenciada e comparar a chave pública e a mensagem verificadas, byte a byte, com os dados sobre os quais atua. O fato de a mesma falha ter permanecido sem correção em muitos programas de cartão movidos pela Rain é o que transformou um único bug em um evento multi-programa.

Tectonic

Em 30/08/2026, a Tectonic, um protocolo de empréstimos ao estilo Compound na Cronos, foi explorada porque seu token de governança de baixa liquidez, TONIC, foi permitido como garantia com um fator de garantia de 20%. O atacante inflou a garantia denominada em TONIC em duas superfícies ao mesmo tempo: uma manipulação por transferência direta da taxa de câmbio do tTONIC, combinada com compras em DEX que elevaram o preço no oráculo. Contra a garantia duplamente inflada, o atacante tomou empréstimos em vários mercados de empréstimo. Cerca de $6,29M (2.592 ETH) foram transferidos via bridge para a Ethereum e representam a perda realizada; a maior parte da drenagem permaneceu na Cronos e foi eliminada quando os validadores reverteram a cadeia a um estado anterior à exploração. Nem a Tectonic nem a Cronos confirmaram um valor final de perda [11].

Análise da Vulnerabilidade

A causa raiz foi listar o TONIC, um token de governança de baixa liquidez, como garantia com um fator de garantia de 20%. Apesar de sua função de governança, o TONIC tinha liquidez on-chain extremamente rasa, então sua avaliação podia ser movida de forma acentuada com capital limitado. Assim como na exploração da Moonwell, duas superfícies eram manipuláveis ao mesmo tempo: o preço no oráculo TONIC/USD, e a taxa de câmbio do token de recibo tTONIC, que uma simples transferência de TONIC para o mercado eleva sem cancelar a dívida correspondente [11]. Qualquer fator de garantia diferente de zero então converteu essa avaliação inflada em poder de empréstimo contra ativos muito mais líquidos.

Análise do Ataque

A reconstrução do ataque abaixo é baseada em inteligência on-chain e em uma reconstrução detalhada a partir de nó de arquivo (archive node) [11][12].

  • Passo 1: O atacante forneceu cerca de 5M USDC à Tectonic como garantia. Enquanto o oráculo TONIC/USD ainda avaliava o token em cerca de $1,06e-8, uma conta controlada tomou emprestado aproximadamente 376,54 trilhões de TONIC e o moveu para uma segunda conta.

  • Passo 2: A segunda conta forneceu cerca de 41,87 trilhões de TONIC normalmente, recebendo tTONIC (o token de recibo do mercado) em troca.

  • Passo 3: O atacante transferiu a maior parte do restante do TONIC emprestado diretamente para o contrato do mercado tTONIC, sem pagar a dívida original de TONIC, elevando a taxa de câmbio do tTONIC e aumentando o valor da garantia do tTONIC mantido pela segunda conta.

  • Passo 4: Usando a garantia inflada em tTONIC, o atacante tomou emprestados 200.000 USDC e cerca de 6,96M CRO, depois comprou aproximadamente 16,23 trilhões de TONIC adicionais nos pools TONIC/USDC, TONIC/WCRO e TONIC/VVS, e os direcionou de volta para o mercado. Isso elevou ainda mais a taxa de câmbio e impulsionou o preço à vista na DEX; o feed off-chain TONIC/USD da Tectonic então aceitou uma série de cotações em rápida ascensão (de cerca de $1,06e-8 às 12:19 UTC para cerca de $2,08e-6 às 12:49 UTC). Essa é a superfície externa.

  • Passo 5: Empréstimos adicionais de cerca de 3,31M USDC e 21,21M CRO compraram mais 7,68 trilhões de TONIC, novamente direcionados de volta ao mercado, reforçando tanto a taxa de câmbio inflada quanto o preço manipulado, imediatamente antes da extração.

  • Passo 6: Na transação final, o atacante sacou contra a garantia duplamente inflada em vários mercados de empréstimo, extraindo aproximadamente 55,24M USDC, 45,65M USDT, 98 WBTC, 1.895 WETH, 16,75M CRO, e outros ativos. Cerca de $6,29M foram transferidos via bridge para a Ethereum (~2.592 ETH) antes que os validadores interrompessem a cadeia. A produção de blocos foi retomada em 30/08/2026 às 23:49 UTC (anunciado em 31/08) depois que os validadores reverteram o estado para um bloco anterior à exploração, eliminando o saldo na Cronos; apenas os ~$6,29M transferidos via bridge representam a perda realizada.

Conclusão

Assim como na exploração da Moonwell no início desta semana, este foi um ataque de manipulação de preço em um mercado ao estilo Compound que aceitava um token de baixa liquidez como garantia, inflando o valor da garantia em duas superfícies ao mesmo tempo: o preço no oráculo e a taxa de câmbio do token de recibo. Os protocolos de empréstimo devem evitar listar ativos de baixa liquidez como garantia, aplicar limites rígidos de suprimento e empréstimo onde eles precisarem ser suportados, e usar precificação ponderada pelo tempo ou sensível à liquidez, para que um mercado à vista raso não possa mover o oráculo. A superfície da taxa de câmbio precisa de sua própria proteção: a contabilidade de garantia de um mercado deve excluir transferências não solicitadas do ativo subjacente, para que uma transferência direta não possa inflar a taxa de câmbio do token de recibo enquanto a dívida correspondente permanece pendente. O monitoramento on-chain de atividades anormais de preço e de empréstimo pode reduzir ainda mais a janela de resposta antes que o valor seja transferido via bridge para fora.

Comece a Usar o Phalcon Security

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

Experimente grátis agora

Referências

Sobre a BlockSec

A BlockSec é uma provedora full-stack de segurança blockchain e conformidade cripto. Desenvolvemos produtos e serviços que ajudam os clientes a realizar auditoria 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 diversos artigos de segurança blockchain em conferências de prestígio, reportou vários ataques de dia zero (zero-day) em aplicações DeFi, bloqueou múltiplos ataques 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