Back to Blog

~$7,04M Perdidos: GiddyDefi, Volo Vault e Mais | BlockSec Semanal

Code Auditing
April 29, 2026
26 min read
Key Insights

Durante a semana passada (26/04/2020 - 26/04/2026), a BlockSec detectou e analisou oito incidentes de ataque, com perdas totais estimadas em ~$7,04M. A tabela abaixo resume esses incidentes, e análises detalhadas de cada caso são fornecidas nas subseções a seguir.

Data Incidente Tipo Perda Estimada
19/04/2026* Contrato Rebalanceador Personalizado Chamada Arbitrária ~$64K
20/04/2026 REVLoans (Juicebox) Validação Imprópria ~$50,7K
22/04/2026 Volo Vault / Navi Comprometimento de Chave ~$3,5M
22/04/2026 Kipseli Router Validação Imprópria ~$72,35K
23/04/2026 GiddyDefi Validação de Assinatura Incompleta ~$1,3M
25/04/2026 Purrlend Comprometimento de Chave ~$1,5M
26/04/2026 SingularityFinance Configuração Incorreta de Oráculo ~$413K
26/04/2026 Scallop Falha de Contabilidade ~$142,7K

*O incidente do Contrato Rebalanceador Personalizado não foi coberto no relatório da semana passada e está incluído aqui para completude.

Comece a usar o Phalcon Explorer

Mergulhe nas Transações para Agir com Sabedoria

Experimente gratuitamente

Destaque da Semana: GiddyDefi

O atacante não quebrou a assinatura, não usou empréstimo flash e não manipulou nenhum preço. Ele repetiu uma assinatura legítima com seus campos não assinados substituídos pelo próprio contrato. "Use sua própria assinatura contra você" é a demonstração mais clara de como a cobertura parcial do EIP-712 transforma uma assinatura válida em uma permissão genérica.

Em 23 de abril de 2026, o GiddyVaultV3 na Ethereum foi explorado em aproximadamente $1,3M. O esquema de assinatura cobria apenas SwapInfo.data, deixando aggregator, fromToken, toToken e amount fora do hash EIP-712, de modo que uma assinatura válida poderia ser repetida com esses campos adulterados. O atacante apontou aggregator para um contrato malicioso e fromToken para o token LP da estratégia, drenando aproximadamente $1,3M.

Contexto

GiddyVaultV3 (0x5f0a...4318) é um contrato de vault de yield farming onde os usuários depositam e retiram fundos via deposit() e withdraw(). Toda operação deve carregar uma estrutura de autorização VaultAuth assinada pelo backend, que inclui uma assinatura EIP-712 e um array SwapInfo[ ] descrevendo as rotas de troca de tokens. Ao executar uma troca, o contrato chama GiddyLibraryV3.executeSwap(), que realiza um forceApprove em swap.fromToken concedendo permissão para swap.aggregator, e então executa a troca via aggregator.call(swap.data). O contrato de estratégia subsequentemente gerencia os fundos de acordo com a estratégia configurada.

EIP-712 é um padrão para assinar dados estruturados fora da cadeia: o protocolo que consome a assinatura reconstrói a mesma estrutura on-chain, faz o hash sob um separador de domínio acordado e recupera o endereço do assinante. A segurança de qualquer fluxo EIP-712 depende, portanto, do hash on-chain cobrir todos os campos cujo valor afeta a execução. No design da Giddy, o backend assina um VaultAuth que contém tanto a intenção do usuário quanto as instruções de roteamento para quaisquer trocas necessárias, e _validateAuthorization() reconstrói essa estrutura para verificar a assinatura antes que a estratégia possa mover fundos.

Análise de Vulnerabilidade

A vulnerabilidade está na função _validateAuthorization() do GiddyVaultV3. Ao construir o payload assinado, apenas o campo data de cada SwapInfo é hasheado; aggregator, fromToken, toToken e amount são todos excluídos da assinatura. Isso significa que qualquer pessoa em posse de uma assinatura válida pode substituir livremente os campos restantes do SwapInfo e ainda assim passar na verificação de assinatura.

Cada campo excluído é uma alavanca separada que a assinatura deixa livre: aggregator torna-se tanto o gastador quanto o alvo da chamada via forceApprove e aggregator.call(swap.data); fromToken seleciona qual ativo da estratégia é aprovado; amount define o teto da permissão; toToken apenas alimenta uma verificação returnAmount > 0. O data assinado não restringe nenhum desses campos porque nenhum desses alvos é referenciado dentro dele.

Análise do Ataque

A análise a seguir é baseada na transação 0x5edb66...5482e5.

  • Passo 1: O atacante obteve on-chain uma assinatura VaultAuth legitimamente autorizada pelo backend, mantendo o campo data intacto. Como toda chamada anterior de deposit() ou withdraw() transmite o payload completo de VaultAuth on-chain, qualquer transação histórica era uma fonte gratuita de uma assinatura reutilizável; o atacante precisava apenas de uma cujo campo data fosse adequado para a chamada de troca pretendida.

  • Passo 2: Usando a assinatura obtida, o atacante manteve a signature, o nonce e o data originais inalterados enquanto adulterava os campos restantes. fromToken foi definido como o Token LP mantido pelo contrato de estratégia (um ativo real), para que o forceApprove concedesse permissão sobre um token que o protocolo realmente possuía. aggregator foi substituído pelo contrato malicioso do atacante, de modo que tanto a aprovação quanto o aggregator.call() subsequente foram direcionados para código de propriedade do atacante. Como esses campos estavam fora do escopo de validação da assinatura, _validateAuthorization() aceitou a estrutura adulterada sem alterações. Para contornar a verificação final require(returnAmount > 0, "SWAP_NO_TOKENS_RECEIVED"), o agregador malicioso implementou uma função de mint que cunhava tokens falsos de volta para o protocolo, satisfazendo a verificação sem realizar nenhuma troca real.

  • Passo 3: Como o agregador malicioso havia recebido aprovação no passo 2, o atacante chamou transferFrom para mover os tokens LP do vault diretamente para o agregador malicioso, completando o roubo. Este passo estava completamente fora do caminho de execução protegido do protocolo; quando executeSwap() retornou, a permissão já havia sido escrita e a verificação de saldo pós-chamada já havia passado, de modo que o protocolo não tinha mais oportunidade de intervir.

Conclusão

A causa raiz deste ataque foi a cobertura incompleta da assinatura EIP-712. Os campos centrais do SwapInfo que governam diretamente o fluxo de fundos foram deixados desprotegidos, permitindo que o atacante substituísse a rota de troca e o endereço do agregador enquanto apresentava uma assinatura válida. Desenvolvedores que integram agregadores externos devem:

  • Garantir que as assinaturas EIP-712 cubram todos os campos que afetam os resultados da execução, incluindo aggregator, fromToken, toToken e amount.

  • Aplicar uma lista branca de agregadores para impedir chamadas a contratos externos não auditados.

  • Restringir toToken a tokens base esperados para evitar que tokens falsos contornem verificações de saldo.

De forma mais ampla, o EIP-712 em qualquer arquitetura de aprovação-e-chamada deve fazer o hash de todos os campos que influenciam o estado on-chain resultante, não apenas a intenção voltada ao usuário. Sempre que uma assinatura de backend for o único guardião entre parâmetros fornecidos pelo usuário e uma ação de contrato privilegiada, cada parâmetro que flui para essa ação (alvo da chamada, ativo, valor, destinatário) deve estar dentro da estrutura assinada. Tratar data como um substituto para a identidade da chamada é um erro categórico: a identidade da chamada é a tupla de todos os seus parâmetros, e qualquer parâmetro deixado fora da assinatura é, por definição, controlado por quem submete a transação.

Melhor Auditor de Segurança para Web3

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


Mais Incidentes desta Semana


Contrato Rebalanceador Personalizado

Em 19 de abril de 2026, um contrato rebalanceador de sAVAX na Avalanche foi explorado para extrair aproximadamente $64K (~7.000 WAVAX) da delegação de crédito Aave V3 de um usuário. Uma função pública executou um target.call(data) arbitrário enquanto ainda detinha a delegação do usuário, de modo que o atacante pôde invocar o borrow() da Aave com onBehalfOf definido como a vítima. Um bot whitehat fez frontrun da exploração e recuperou os fundos antes de qualquer retirada.

Contexto

O contrato rebalanceador (0x7a7b...a8c9) expõe uma função b2a13230() projetada para rebalancear a posição alavancada de um usuário na Aave. A função opera em nome do usuário via delegação de crédito Aave V3: o usuário concede ao rebalanceador permissão para tomar empréstimos em seu nome, e o rebalanceador combina esses empréstimos com fundos fornecidos pelo usuário para ajustar a posição (por exemplo, um fluxo de trabalho de empréstimo + fornecimento).

Análise de Vulnerabilidade

A causa raiz é que b2a13230() inclui uma etapa target.call(data) cujo alvo e calldata são ambos controlados pelo chamador. Essa chamada é executada enquanto o contrato ainda opera sob a delegação de crédito Aave V3 do usuário, portanto qualquer lógica invocada durante essa etapa herda o poder de empréstimo do usuário. Não há lista de permissões de alvos permitidos nem restrição de formato no calldata, de modo que a chamada pode invocar qualquer método de contrato, incluindo o borrow() da Aave com onBehalfOf definido como o usuário.

Análise do Ataque

A análise a seguir é baseada na transação: 0xaaa1b2...35001b.

  • Passo 1: O atacante realizou um empréstimo flash de uma quantidade de sAVAX e USDC. Em seguida, forneceu o USDC emprestado à Aave V3 via contrato rebalanceador para estabelecer colateral suficiente para empréstimos. Enquanto isso, o sAVAX emprestado foi transferido diretamente para o contrato rebalanceador para preparar a etapa de fornecimento subsequente após o empréstimo.

  • Passo 2: O atacante invocou a função b2a13230(). A função primeiro realizou uma operação de empréstimo normal, depois chegou à seção de chamada arbitrária. Nesse ponto, o atacante criou a chamada para invocar diretamente a função borrow() da Aave V3 com onBehalfOf definido como o endereço da vítima. Como a vítima havia concedido delegação de crédito ao contrato rebalanceador, o empréstimo foi bem-sucedido. O WAVAX emprestado foi transferido para o contrato rebalanceador.

  • Passo 3: O atacante invocou a função b2a13230() novamente, desta vez usando o rebalanceador para tomar emprestado WAVAX em seu próprio nome. O contrato então usou o WAVAX previamente emprestado (originário da posição da vítima) para fornecer à posição do atacante e pagar, permitindo que o atacante extraísse lucro.

Conclusão

O defeito é a combinação de uma chamada externa arbitrária dentro de um contexto privilegiado que detém crédito delegado. Qualquer camada isolada seria segura: uma chamada externa restrita não pode usar indevidamente a delegação, e uma chamada arbitrária sem delegação não pode mover os fundos do usuário. Contratos que detêm delegação de crédito nunca devem expor uma chamada externa arbitrária; se tais chamadas forem necessárias, os alvos devem ser fixados em uma lista de permissões e o formato do calldata deve ser verificado.


REVLoans (Juicebox)

Em 20 de abril de 2026, o REVLoans, uma extensão de empréstimo no Juicebox, foi explorado na Ethereum por aproximadamente $50,7K. borrowFrom() aceitava uma fonte de contabilidade fornecida pelo chamador sem verificar se ela estava registrada no protocolo; um contexto forjado com 36 casas decimais acionou um atalho de mesma moeda que redimensionou incorretamente os saldos por 1e18. Duas transações — uma para propagar a entrada de contabilidade inflada e outra para tomar emprestado contra o pool legítimo ao preço de cota inflado — drenaram 21,77 ETH.

Contexto

Juicebox é um protocolo híbrido de captação de recursos e empréstimos na Ethereum. Cada projeto tem seu próprio token ERC20 de participação (referido aqui como REV) e um tesouro dividido em um ou mais terminais, onde um terminal é o contrato que custodia fisicamente um subconjunto dos ativos do projeto e atua como ponto de entrada/saída voltado ao usuário. Um projeto pode ter vários terminais registrados nele em JBDirectory, e cada tripla (terminal, projeto, token) carrega um JBAccountingContext declarando o (decimals, currency) usado para a contabilidade desse token dentro desse terminal. REV é, portanto, uma reivindicação sobre a união dos excedentes em todos os terminais do projeto, não uma reivindicação contra nenhum terminal único.

Um usuário pode depositar um ativo em um terminal em troca de REV recém-cunhado, ou resgatar REV em um terminal por uma parcela proporcional do seu excedente (com um imposto de saque configurável que deixa algum valor para os detentores restantes). REVLoans (0x2db6...1846), um contrato separado construído sobre isso, adiciona uma facilidade de empréstimo: um usuário queima REV como colateral e obtém um empréstimo contra um dos terminais do projeto, com o empréstimo reembolsável mais tarde em troca da remintagem do colateral. O valor do empréstimo é precificado com exatamente a mesma matemática que um resgate, portanto um empréstimo é economicamente equivalente a sacar o mesmo colateral.

O preço da cota de REV é (totalSurplus + totalBorrowed) / (REV.totalSupply + totalCollateral). Incluir totalBorrowed no numerador mantém empréstimo/pagamento neutro em preço; também significa que um totalBorrowed inflado eleva diretamente o preço da cota e permite que um pequeno colateral seja sacado de forma desproporcional.

Análise de Vulnerabilidade

A causa raiz é a entrada não verificada no parâmetro source. borrowFrom() aceita um REVLoanSource source fornecido pelo chamador (uma estrutura com campos .terminal e .token) sem verificar se esse par está registrado para o revnetId fornecido. Ambos os campos fluem diretamente para a matemática de saque, portanto o contexto de contabilidade retornado por source.terminal é totalmente controlado pelo chamador. Quando o campo currency desse contexto corresponde ao do terminal de destino, o protocolo toma um atalho de mesma moeda, pula o oráculo de preços e trata os decimais e valores de saldo fornecidos como autoritativos.

O source não validado é então escrito em _loanSourcesOf[revnetId] e totalBorrowedFrom[revnetId][source.terminal][source.token] por _addTo(), que também não realiza nenhuma verificação de registro.

Uma vez que (source, revnetId) está na contabilidade, _borrowableAmountFrom() é a função que traduz uma solicitação de empréstimo em um valor a ser pago. Ela constrói surplus = totalSurplus + totalBorrowed a partir de _totalBorrowedFrom(), depois passa esse excedente para JBCashOuts.cashOutFrom() junto com a contagem de colateral do chamador e o fornecimento de cotas.

O bug de decimais está um nível mais abaixo, em _totalBorrowedFrom(). Ele itera _loanSourcesOf e combina cada entrada via mulDiv(tokensLoaned, 10**decimals, pricePerUnit). No caminho de mesma moeda, pricePerUnit = 10**decimals (a precisão de 18 decimais do alvo), portanto a fórmula reduz para tokensLoaned inalterado, e um saldo armazenado sob contabilidade de 36 decimais cai na soma de ETH de 18 decimais 1e18 vezes maior.

A amplificação acontece em cashOutFrom(). base = mulDiv(surplus, cashOutCount, totalSupply): com surplus dominado por totalBorrowed inflado, mesmo um cashOutCount (colateral) minúsculo mapeia para um pagamento desproporcionalmente grande.

Análise do Ataque

O ataque usa duas transações. A primeira contamina a contabilidade do REVLoans: 0xc46cb7...dead1f. A segunda drena o pool contra um terminal legítimo: 0x9adbd6...a8f938.

  • Passo 1: O atacante chamou borrowFrom() com terminal e token na fonte do empréstimo apontando para um contrato falso, depositando uma pequena quantidade de REV como colateral. O REVLoans não verificou se o terminal fornecido está registrado para o revnet, nem se o token é reconhecido por ele.
  • Passo 2: O REVLoans consultou o terminal falso por um contexto de contabilidade, que retornou um (decimals=36, currency=ETH-code(61166)) forjado. Como as moedas de origem e destino correspondiam, o REVLoans tomou um atalho de mesma moeda e pulou o oráculo de preços, depois executou a matemática de saque sobre os excedentes reais de ETH dos terminais legítimos reexpressos na unidade alvo de 36 decimais do atacante, inflando o valor por 1e18.
  • Passo 3: O REVLoans registrou (terminal falso, token falso) em _loanSourcesOf e escreveu o valor inflado em totalBorrowedFrom. O terminal falso "pagou" simplesmente confirmando o recebimento; nenhum ETH real foi movido. A primeira transação terminou com totalBorrowed manipulado para cima e apenas o pequeno colateral de REV queimado.
  • Passo 4: O atacante chamou borrowFrom() novamente, desta vez passando o terminal ETH legítimo como fonte do empréstimo e um colateral mínimo de REV. A matemática de saque foi executada em unidades reais de ETH com 18 decimais.
  • Passo 5: Ao calcular totalBorrowed, o REVLoans iterou _loanSourcesOf e encontrou a entrada do passo 3. Como a currency dessa entrada ainda correspondia a ETH, o atalho de mesma moeda foi acionado novamente e o saldo armazenado com 36 decimais foi incorporado à soma de ETH com 18 decimais 1e18 vezes maior. totalBorrowed era agora dominado por dívida falsa e o numerador do preço da cota estava massivamente inflado.
  • Passo 6: A matemática de saque retornou um valor de empréstimo dimensionado para o numerador inflado, que o atacante havia pré-ajustado para ficar logo abaixo do excedente real do terminal legítimo. O terminal legítimo pagou, drenando quase todo o pool para o atacante.

Conclusão

A causa raiz são duas lacunas combinadas: pares (terminal, token) são aceitos sem verificar o registro no revnet, e o atalho de mesma moeda incorpora saldos de origem na soma de destino sem renormalizar para diferenças de decimais. Qualquer lacuna isolada seria menos perigosa; juntas, elas permitem que um chamador injete uma entrada arbitrária em totalBorrowedFrom e a saque pelo valor nominal. Mitigação: validar (terminal, token) contra os terminais registrados do revnet, e renormalizar os saldos pela escala decimal armazenada da origem antes de incorporar.


Volo Vault

Em 22 de abril de 2026, a Volo, um vault de rendimento na Sui que obtém rendimento de empréstimos roteando depósitos de usuários para o protocolo de empréstimos Navi, perdeu aproximadamente $3,5M após o vazamento da chave privada do operador. O contrato do vault não tinha nenhum bug de código; o atacante simplesmente executou o caminho legítimo do operador com credenciais roubadas e drenou os depósitos da Volo no Navi.

Contexto

Volo é o vault voltado ao usuário (0xcd86...27fefa); Navi é o protocolo de empréstimos subjacente. O vault detém um AccountCap do Navi (um objeto de capacidade Sui que autoriza saques da conta Volo no Navi) e delega movimentos de estratégia a um papel de operador. Para depositar ou sacar no Navi, o operador chama start_op_with_bag_v2() para retirar o AccountCap do vault para uma bolsa temporária, depois deposit_with_account_cap() / withdraw_with_account_cap_v2() usam essa capacidade para mover fundos.

Análise de Vulnerabilidade

A causa raiz é uma falha operacional/de custódia de chave, em vez de uma vulnerabilidade no nível do contrato. O caminho de estratégia da Volo delega autoridade de saque a quem quer que detenha a chave privada do operador: start_op_with_bag_v2() realiza apenas duas verificações (assert_operator_not_freezed(operation, cap) e assert_single_vault_operator_paired(operation, vault.vault_id(), cap)), ambas verificando apenas que a capacidade fornecida é o operador registrado. withdraw_with_account_cap_v2() então aceita qualquer chamador que possa apresentar o AccountCap retirado. Qualquer pessoa em posse da chave privada do operador pode, portanto, executar o mesmo caminho que as operações legítimas usam, de forma indistinguível.

Análise do Ataque

A análise a seguir é baseada na transação AQw9wM...3RUS.

  • Passo 1: O atacante chamou start_op_with_bag_v2 em @volosui/volo-vault::operation com a chave de operador vazada, retirando o AccountCap do Navi para uma bolsa temporária.
  • Passo 2: O atacante usou bag::remove para extrair o AccountCap da bolsa temporária.

  • Passo 3: O atacante chamou withdraw_with_account_cap_v2 em @navi-protocol/lending::incentive_v3 com o AccountCap extraído, retirando os depósitos da Volo do Navi.

  • Passo 4: O atacante usou bag::add para devolver o AccountCap, encerrou a operação e transferiu os fundos para fora.

Conclusão

O defeito é estrutural: uma chave de operador, autoridade total de saque, sem segunda verificação. Três mudanças reduzem o dano de um comprometimento de chave. Dividir o papel do operador em um esquema multisig ou de limiar significa que uma chave vazada não pode autorizar um saque por conta própria. Adicionar um bloqueio de tempo em saques de saída dá às chamadas anormais uma janela contestável antes da liquidação. Limitar os poderes do operador apenas a depositar e rebalancear, com saques voltados ao usuário roteados por um caminho separado, impede que o papel do operador acesse os fundos dos usuários.


Kipseli Router

Em 22 de abril de 2026, o Kipseli Router na Base foi explorado por aproximadamente $72,35K. O roteador usa uma cotação retornada por um cotador externo exclusivo para USDC como o valor bruto de transferência do token de saída, sem verificar se o token de saída é igual ao token de cotação. Um atacante trocou 0,04 WETH por cbBTC em um caminho que o cotador não suporta de fato, recebendo o valor de retorno escalado em USDC do cotador (92.610.395) como unidades brutas de cbBTC (≈0,926 cbBTC).

Contexto

Kipseli Router (0x579f...9a07) é um contrato de execução de trocas respaldado por um sistema de cotação externo. O contrato não é de código aberto; a análise abaixo é baseada no bytecode decompilado, razão pela qual os nomes das funções aparecem como seletores de 4 bytes (0xcce096f3(), 0x592(), 0xd88()). Em vez de calcular preços de troca diretamente dos pools AMM on-chain, ele consulta o cotador para um valor de saída (amountOut) e então executa a transferência de token com base nesse valor. Na operação normal, o usuário envia tokenIn para a carteira do protocolo, e o roteador retira tokenOut da mesma carteira e o encaminha ao destinatário. O protocolo é configurado com um único QUOTE_TOKEN, e a lógica de cotação é denominada em USDC usando contabilidade de 6 decimais; o sistema é projetado apenas para suportar cotações denominadas em USDC.

Análise de Vulnerabilidade

O defeito abrange duas camadas que se combinam. No lado do roteador, a função 0xcce096f3() recupera uma cotação v0 via a função do cotador 0x592() e a passa inalterada para 0xd88() como tokenOut.transferFrom(_wallet, receiver, v0). O roteador nunca verifica se tokenOut é igual ao QUOTE_TOKEN do protocolo, portanto um valor escalado em USDC (precisão de 6 decimais) é transferido como se fosse uma quantidade de cbBTC (precisão de 8 decimais). No lado do cotador, o AMM PropAMM subjacente é projetado exclusivamente para pares token-USDC, mas aceita caminhos de roteamento não suportados (WETHcbBTC) sem reverter, ignorando silenciosamente tokenIn e retornando um valor escalado em USDC como se a troca fosse válida.

Análise do Ataque

A análise a seguir é baseada na transação 0x96edee...3db3bb.

  • Passo 1: O atacante chamou o roteador com tokenIn=WETH e tokenOut=cbBTC. O AMM subjacente não suportava esse caminho, mas não reverteu, e o cotador 0x592() retornou um valor escalado em USDC de 92.610.395 (≈92,61 USDC).
  • Passo 2: O roteador usou esse valor diretamente como o valor de transferência de cbBTC. 0,04 WETH (≈$95) entrou via transferFrom; 92.610.395 unidades brutas de cbBTC (≈0,926 cbBTC, ≈$72,35K) saíram da carteira do protocolo para o atacante.

Conclusão

A exploração ocorre porque dois pressupostos não são verificados em nenhum dos lados da chamada do cotador. O cotador assume que sua saída é consumida em seu próprio frame de 6 decimais de USDC; o roteador assume que qualquer retorno do cotador é denominado no tokenOut solicitado. Qualquer uma das correções remove o bug:

  • No roteador: afirmar que tokenOut == QUOTE_TOKEN, ou converter a cotação escalada em USDC em unidades de tokenOut via oráculo antes da transferência.

  • No cotador: reverter em caminhos de roteamento cujos tokens não estejam registrados para o conjunto de pares suportados, em vez de retornar silenciosamente um fallback escalado em USDC.


Purrlend

Em 25 de abril de 2026, a Purrlend, um protocolo de empréstimos na HyperLiquid e MegaETH, perdeu aproximadamente $1,5M após um comprometimento de chave privada. O atacante assumiu o papel de bridge e cunhou pTokens sem respaldo (tokens de recibo semelhantes aos da Aave da Purrlend), depois usou esses pTokens como colateral para tomar emprestado ativos reais do pool.

Contexto

Purrlend (0x81d5...a702) é um protocolo de empréstimos com um modelo de contabilidade semelhante ao da Aave. Quando os usuários fornecem ativos ao protocolo, eles recebem pTokens correspondentes, semelhantes aos aTokens da Aave, que representam sua posição fornecida e podem ser usados como colateral para tomar emprestado outros ativos.

O protocolo também inclui papéis privilegiados, incluindo pool admin, risk admin e bridge. O papel de bridge é destinado à contabilidade entre cadeias: ele pode cunhar pTokens para espelhar depósitos que ocorreram em uma cadeia contraparte. Os outros papéis de administração modificam parâmetros de risco e configuram ativos emprestáveis.

Análise de Vulnerabilidade

O gatilho imediato foi um comprometimento de chave privilegiada: o atacante obteve as chaves que controlam os papéis de administrador e bridge da Purrlend. Uma falha de design no nível do contrato amplificou o vazamento: o caminho de mint de pToken do papel bridge não está ancorado a nenhuma prova verificável de custódia entre cadeias. A função permite que um chamador com o papel de bridge emita pTokens para qualquer endereço, em qualquer quantidade, sem verificar se um depósito correspondente ocorreu na cadeia de origem. Em qualquer outro lugar no protocolo, os pTokens são tratados como colateral válido, e o caminho de empréstimo não reverifica o respaldo no momento do empréstimo. Portanto, um mint não autorizado pelo papel de bridge se traduz diretamente em poder de empréstimo, sem segunda barreira entre o mint e o saque de ativos.

Análise do Ataque

A análise a seguir é baseada na transação 0xb96cff...dbbf24 no MegaETH.

  • Passo 1: O atacante, detendo chaves privilegiadas comprometidas, usou um lote MultiSendCallOnly via GnosisSafeProxy para se definir como pool admin, risk admin, bridge e emergency admin através do ACLManager, depois habilitou WETH como ativo emprestável e definiu seu BorrowCap como 200.
  • Passo 2: Atuando como bridge, o atacante cunhou uma grande quantidade de pTokens para seu próprio endereço. O caminho de mint do bridge não realizou nenhuma verificação de custódia entre cadeias, portanto os novos pTokens não tinham ativos subjacentes como respaldo.

  • Passo 3: O atacante usou os pTokens sem respaldo como colateral. Como o caminho de empréstimo trata qualquer saldo de pToken como uma posição de fornecimento válida sem reverificar o respaldo, a verificação de colateral passou e WETH foi emprestado do pool.

Conclusão

Este foi um comprometimento de chave privada amplificado por uma falha de design no nível do contrato. As chaves vazadas deram ao atacante apenas a autoridade pretendida do papel de bridge, mas essa autoridade incluía o mint irrestrito de pToken, que se traduz diretamente em colateral emprestável. Cada camada pode ser reforçada independentemente. Na camada operacional, dividir o papel de bridge em um esquema multisig ou de limiar impede que um único vazamento de chave o execute. Na camada do contrato, exigir que o mint do bridge carregue uma prova verificável de custódia (por exemplo, um compromisso de mensagem de um verificador entre cadeias confiável) e reverter quando nenhuma prova for fornecida. Verificar a prova no momento do mint é a correção mais durável porque elimina a dependência da custódia de chaves completamente.


SingularityFinance

Em 26 de abril de 2026, o vault dynBaseUSDCv3 da SingularityFinance na Base perdeu aproximadamente $413K. O vault foi configurado com um nível de taxa Uniswap V3 inválido (42, que não existe no V3), portanto o oráculo de preços de cada ativo não USDC resolvia para um pool inexistente. A função de precificação retornou 0 silenciosamente em vez de reverter, o vault avaliou suas reservas não USDC em zero, e um atacante cunhou quase todo o fornecimento de cotas depositando uma quantidade mínima de USDC, depois resgatou pelos ativos subjacentes reais.

Contexto

O vault dynBaseUSDCv3 (0x67b9...4dcd) detém múltiplos tokens com rendimento e precifica reservas não USDC através do Uniswap V3. O auxiliar de precificação getPrice(base, fee, quote, amount) resolve a tupla (base, quote, fee) para um pool Uniswap V3 via fábrica, depois lê o TWAP desse pool. O totalAssets() do vault agrega as reservas precificadas; as proporções de mint e resgate de cotas são derivadas desse total.

Análise de Vulnerabilidade

O defeito está no ramo de retorno antecipado de getPrice(). Quando IUniswapV3Factory.getPool(base, quote, fee) retorna address(0) (nenhum pool existe para o nível de taxa fornecido), a função passa adiante e retorna sua variável price inicializada com zero em vez de reverter. O vault foi implantado com fee=42, que não é um dos níveis suportados pelo Uniswap V3 (500/3000/10000), portanto a consulta de cada token não USDC acessa esse ramo. totalAssets() soma portanto aproximadamente apenas o saldo de USDC do vault, enquanto os tokens com rendimento reais contribuem com zero. As proporções de mint e resgate que dependem de totalAssets() são calculadas contra esse denominador quase zero.

Análise do Ataque

A análise a seguir é baseada na transação 0x00b949...8d3732.

  • Passo 1: O atacante obteve um empréstimo flash de aproximadamente 100K USDC.

  • Passo 2: O atacante depositou o USDC no vault. Como totalAssets() contava apenas o saldo de USDC, o vault se avaliou em aproximadamente o valor do depósito e o atacante recebeu quase 100% do fornecimento de cotas.

  • Passo 3: O atacante resgatou as cotas, que distribuem as reservas subjacentes proporcionalmente à propriedade de cotas. O atacante recebeu uma grande fração de cada token com rendimento que o vault detinha.

  • Passo 4: O atacante reembolsou o empréstimo flash e manteve os tokens com rendimento drenados como lucro.

Conclusão

Duas verificações estavam ausentes. A implantação não validou fee=42 contra os níveis suportados pelo Uniswap V3 (500/3000/10000); getPrice() retornou 0 em um pool inexistente em vez de reverter. Qualquer uma das correções é suficiente: validar os parâmetros do oráculo no momento da configuração, ou reverter quando getPool() == address(0). Como defesa em profundidade, a lógica de mint de cotas deve fazer uma verificação de sanidade de totalAssets() contra uma referência externa antes de aceitar depósitos.


Scallop

Em 26 de abril de 2026, o programa de recompensas de staking da Scallop na Sui perdeu aproximadamente $142,7K. A função que atualiza as recompensas acumuladas de um usuário não verificava se o objeto de rastreamento de recompensas passado correspondia à conta do usuário, permitindo que um atacante extraísse um saldo fictício de pontos de um objeto de rastreamento de recompensas abandonado e há muito inativo, e o resgatasse contra o pool de recompensas legítimo até que o saldo fosse drenado.

Contexto

Scallop é um protocolo de empréstimos na Sui. Além do seu produto de empréstimos, a Scallop opera um programa de spool: os usuários depositam um único ativo no mercado da Scallop para receber MarketCoin<T> (o recibo de empréstimo; para depósitos de SUI isso é MarketCoin<SUI>, a representação on-chain de "sSUI"), depois fazem staking desse MarketCoin em um Spool para ganhar pontos do protocolo ao longo do tempo, que depois resgatam contra um RewardsPool pareado por tokens de recompensa reais. Cada Spool é um objeto compartilhado Sui que rastreia um index global por cota; cada usuário detém um SpoolAccount pessoal registrando seu saldo em staking e points acumulados.

Análise de Vulnerabilidade

O defeito está em spool::user::update_points: a função não afirma account.spool_id == object::id(spool) (nem account.stake_type == spool.stake_type). Entradas irmãs stake, unstake e redeem_rewards realizam essa verificação de vinculação na entrada; apenas update_points a pula. Sem a verificação, spool_account::accrue_points calcula account.points += stake * (spool.index − account.index) / 1e9 contra qualquer Spool passado, tratando seu index como se fosse o próprio fluxo de recompensa desta conta.

O caminho torna-se explorável porque o Sui nunca coleta objetos compartilhados como lixo: um Spool Scallop abandonado cujo stakes decaiu para quase nada continua acumulando participação de recompensa (incremento por período 1e9 * reward / stakes), portanto seu index aumenta cumulativamente ao longo do tempo e pode atingir valores arbitrariamente grandes. Com a verificação de vinculação ausente, update_points pode usar esse index inflado para escrever um enorme delta de pontos em qualquer conta. Os points poluídos então são resgatados 1:1 contra o RewardsPool do spool alvo, porque a conta está legitimamente vinculada a esse spool alvo e a própria verificação de vinculação de redeem_rewards passa.

Análise do Ataque

A análise a seguir é baseada na transação 6WNDjC...NfVL.

  • Passo 1: Com 0,2 SUI como isca, o atacante cunhou MarketCoin<SUI>, depois chamou new_spool_account + stake contra o spool alvo para criar um SpoolAccount legitimamente vinculado com account.spool_id = target_spool.

  • Passo 2: O atacante chamou update_points<MarketCoin<SUI>>(donor_spool, account, clock) com donor_spool definido como um Spool abandonado. O index do doador (≈8,91e14) foi escrito na conta como points: points = stake * (8,91e14 − 1,19e9) / 1e9 ≈ 1,62e14.

  • Passo 3: O atacante chamou redeem_rewards<MarketCoin<SUI>, SUI>(target_spool, target_rp, account). As afirmações de vinculação aceitaram a conta vinculada ao alvo, o re-acúmulo interno retornou antecipadamente, e os points poluídos foram convertidos à taxa 1:1 do pool de recompensas até seu saldo: rewards = 150.098.061.595.978 unidades brutas de SUI.

  • Passo 4: O atacante chamou unstake e redeem para recuperar a isca de 0,2 SUI, depois TransferObjects para mover tudo para fora.

Conclusão

A correção é adicionar a mesma verificação assert!(account.spool_id == object::id(spool)) na entrada de update_points que stake, unstake e redeem_rewards já realizam. Como defesa em profundidade, o protocolo também poderia limitar o delta de index aceito por uma única chamada de accrue_points (rejeitar deltas maiores que um teto configurado), de modo que mesmo que a verificação de vinculação fosse contornada novamente no futuro, nenhuma chamada única pudesse creditar uma conta com uma quantidade de points desproporcional à sua duração real de staking.

Comece a usar o Phalcon Security

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

Experimente gratuitamente

Sobre a BlockSec

A BlockSec é um provedor completo de segurança blockchain e conformidade em criptoativos. 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 vários 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