Back to Blog

#8 Incidente Bunni: Saques Pequenos Repetidos Amplificam um Erro de Arredondamento em uma Perda de US$ 8,4 Milhões

Code Auditing
February 12, 2026
8 min read

Em 2 de setembro de 2025, o protocolo Bunni V2 sofreu um exploit sofisticado [1]. Um atacante explorou uma vulnerabilidade crítica em seu mecanismo de contabilidade de liquidez para extrair aproximadamente 8,4 milhões de dólares de dois pools de liquidez: o pool USDC/USDT na Ethereum [2] e o pool weETH/ETH na Unichain [3].

A causa raiz foi um erro de arredondamento na atualização dos saldos ociosos do pool pelo protocolo durante a remoção de liquidez. Esse erro levou a uma subavaliação significativa da liquidez total no contrato, criando uma discrepância explorável entre a liquidez teórica e a real. O atacante então executou um ataque sandwich preciso para lucrar com essa disparidade.

Este incidente resultou diretamente em graves perdas financeiras para o protocolo Bunni, que subsequentemente declarou falência em 23 de outubro de 2025 [4].

Contexto

Bunni V2 é um protocolo de Formador de Mercado Automatizado (AMM) construído sobre o Uniswap V4. Ele implementa sua lógica central por meio do mecanismo de hook e introduz inovações sobre o algoritmo de liquidez concentrada do Uniswap V3, com o objetivo de fornecer melhor eficiência de capital para Provedores de Liquidez (LPs) [5].

Especificamente, o protocolo aprimora os retornos dos LPs principalmente por meio de uma funcionalidade de Rehipotecação e um mecanismo de Rebalanceamento. O primeiro aloca liquidez para protocolos externos geradores de rendimento, garantindo liquidez base enquanto captura rendimento externo adicional. O segundo otimiza continuamente a distribuição de liquidez entre faixas de preço, aumentando a utilização ativa do capital para impulsionar a receita de taxas. Esses dois mecanismos formam as inovações centrais do protocolo sobre o modelo fundamental de liquidez concentrada.

Rehipotecação

Para aumentar os retornos dos Provedores de Liquidez, o Bunni V2 emprega uma estratégia de Rehipotecação. Essa estratégia aloca fundos em diferentes posições:

  • rawBalance: Uma parte das reservas do pool para um token é armazenada diretamente no contract PoolManager do Uniswap V4. Isso serve como liquidez imediatamente disponível para facilitar as trocas.
  • reserves: O restante é depositado em um vault ERC4626 especificado. Isso permite que os usuários obtenham rendimento externo adicional sobre esses ativos.

Portanto, o total de ativos de um pool é definido como: Ativos do Pool = rawBalance + valor subjacente de reserves.

Rebalanceamento

Para aumentar a receita de taxas, o Bunni V2 implementa um mecanismo de rebalanceamento, que monitora o preço médio ponderado pelo tempo. Quando a variação de preço excede um limiar, a liquidez é redistribuída entre diferentes faixas de preço de acordo com a Função de Distribuição de Liquidez (LDF).

Essa realocação pode modificar a proporção de tokens exigida pela LDF, deixando um excedente em um dos tokens. Esse excedente é definido como o saldo ocioso.

Assim, a liquidez é dividida em duas partes:

  • Saldo Ativo: A parte alocada pela LDF que participa do cálculo de liquidez.
  • Saldo Ocioso: O excedente não utilizado para liquidez ativa.

Portanto, Ativos do Pool = Saldo Ativo + Saldo Ocioso.

Funções principais: cálculo de liquidez e remoções

Este ataque explora duas funções críticas: queryLDF() e withdraw(). A função queryLDF() calcula a liquidez do pool para as trocas, enquanto a função withdraw() permite que os usuários removam uma liquidez proporcional.

Funções queryLDF()

Devido à estratégia de Rehipotecação, a quantidade de ativos subjacentes é dinâmica, e o Bunni V2 não armazena um valor fixo de "liquidez total". Em vez disso, o protocolo fornece a função queryLDF() para recuperar a liquidez em tempo real quando uma troca ocorre [6]. O processo de execução dessa função consiste nas seguintes quatro etapas:

  1. Consultando a Densidade de Liquidez:

    1. Invocar a função de densidade de liquidez ldf.query(), que obtém a densidade de liquidez fora da faixa de tick de preço atual.

    2. Invocar LiquidityAmounts.getAmountsForLiquidity() para obter a densidade dentro da faixa de tick atual.

    3. Calcular a densidade de liquidez total do token0 e token1 em ambas as direções, denotadas como totalDensity0 e totalDensity1.

    Notavelmente, a função LiquidityAmounts.getAmountsForLiquidity() usa arredondamento para cima para garantir que as quantidades de tokens calculadas sejam conservadoramente não inferiores aos valores teóricos.

  2. Calcular o Saldo Disponível

    Os saldos disponíveis usados para os cálculos de liquidez são denotados como balance0 e balance1. O saldo ocioso é deduzido do saldo total do token correspondente, excluindo os fundos que não participam dos cálculos de liquidez.

    Neste ataque, onde os fundos ociosos do pool consistiam em token0, as fórmulas de cálculo são:

    • balance0=rawBalance0+reserve0idleBalancebalance0 = rawBalance0 + reserve0 - idleBalance

    • balance1=rawBalance1+reserve1balance1 = rawBalance1 + reserve1

  3. Estimar a Liquidez Efetiva

    1. Estimar a liquidez que cada token pode suportar com base em seu saldo disponível real (balance0 ou balance1) e na densidade total calculada (totalDensity0 ou totalDensity1).

    2. Selecionar o menor dos dois valores estimados como a liquidez total efetiva final.

    A fórmula é a seguinte:

    L=min(balance0totalDensity0,balance1totalDensity1)L= min(\frac{balance0}{totalDensity0},\frac{balance1}{totalDensity1})

  4. Calcular os Saldos Ativos

Com base na liquidez total determinada, o protocolo calcula a quantidade real de tokens disponíveis para negociação. Isso é definido como o Saldo Ativo.

Função withdraw()

O Bunni V2 fornece a função withdraw() para remoção de liquidez. Os usuários removem liquidez proporcional à sua participação no total de fundos do pool. O protocolo atualiza o rawBalance, reserves e idleBalance na mesma proporção. A fórmula de ajuste é a seguinte:

(rawBalance,reserves,idleBalance)=(rawBalance,reserves,idleBalance)×(1sharestotalSupply)(rawBalance, reserves, idleBalance) \\= (rawBalance, reserves, idleBalance) \times (1-\frac{shares}{totalSupply})

Onde:

  • shares é o número de cotas de liquidez que o usuário remove;
  • totalSupply é o fornecimento total de tokens de liquidez para aquele pool.

Análise de Vulnerabilidade

A vulnerabilidade se origina no cálculo do valor de ajuste do saldo ocioso pela função withdraw(), que utiliza arredondamento para baixo (ou seja, truncamento). Isso resulta em uma superestimação do saldo ocioso.

Recordando a fórmula do saldo disponível, balance=rawBalance+reservesaldo ociosobalance = rawBalance + reserve - saldo\ ocioso. Um saldo ocioso superestimado causa diretamente a subestimação do saldo disponível (balance0) usado para os cálculos de liquidez. Consequentemente, a liquidez total efetiva estimada também é subavaliada. De acordo com o Post Mortem do Exploit do Bunni [7], essa direção de arredondamento nos cálculos de liquidez foi empregada intencionalmente. Um valor de liquidez calculado menor leva a um impacto de preço maior durante as trocas.

Esse design depende de uma suposição crítica: a proporção de saldo entre os dois tokens permanece relativamente equilibrada. Em condições normais com liquidez adequada, os valores de liquidez total estimados separadamente para cada token são tipicamente próximos. O impacto do erro de arredondamento é, portanto, limitado. No entanto, quando o saldo disponível do token que carrega um saldo ocioso torna-se extremamente baixo, a falha emerge. Nesse cenário, o erro de arredondamento para baixo é amplificado significativamente.

O atacante explorou essa vulnerabilidade realizando uma série de pequenas retiradas, arredondando para baixo o saldo disponível do token0 de 28 wei para 4 wei. Essa queda excedeu em muito a proporção de cotas de liquidez efetivamente queimadas. Enquanto isso, o saldo disponível do token1 permaneceu em um nível relativamente normal. Esse desequilíbrio criou uma janela de arbitragem significativa. O próximo capítulo fornece uma análise numérica detalhada.

Análise do Ataque

Tomando a transação na Ethereum [2] como exemplo, o atacante executou um ataque em três estágios:

  • No primeiro estágio, o atacante realizou manipulação de preço para esgatar significativamente o saldo disponível de USDC (token0). Isso criou as condições iniciais necessárias para amplificar o erro de arredondamento subsequente.
  • No segundo estágio, o exploit central foi realizado por meio de uma série de pequenas retiradas, fazendo com que o protocolo subestimasse a liquidez real do pool.
  • No terceiro estágio, o atacante executou duas trocas direcionais para arbitrar a discrepância entre a liquidez subestimada pelo protocolo e a liquidez real do pool, extraindo lucro ao final.

Estágio 1: Manipulando o preço e reduzindo o saldo do token alvo

O atacante executou três transações de troca, manipulando o preço do USDC (token0) em relação ao USDT (token1), levando-o de um tick inicial = -1 para tick = 5000. O principal objetivo era esgotar o saldo ativo de USDC do pool, reduzindo-o a um nível extremamente baixo de 28 wei. Isso criou as condições iniciais necessárias para amplificar o erro de arredondamento subsequente na fase seguinte.

Estágio 2: Explorando retiradas para amplificar discrepâncias de liquidez

O atacante iniciou 44 pequenas retiradas por meio da função withdraw(). Devido ao arredondamento para baixo usado por essa função ao atualizar o idleBalance, o saldo ocioso do protocolo tornou-se superestimado. Isso subestimou ainda mais o saldo disponível de USDC na função queryLDF(). Após essas operações repetidas, o saldo disponível de USDC foi anormalmente suprimido de 28 wei para 4 wei. Isso representou uma redução real de 85,7%, muito superior à proporção teórica correspondente às cotas de liquidez removidas (ou seja, 8,998105442969973e-07%). Neste ponto, a liquidez estimada a partir do USDC no pool estava severamente subestimada.

Estágio 3: Executando a arbitragem e realizando lucros

O atacante então executou duas trocas direcionais, constituindo uma operação semelhante a um ataque sandwich.

Passo 1: O atacante usou uma grande quantidade de USDT para trocar por USDC. Neste momento, o cálculo interno de liquidez estava severamente subavaliado com base no saldo subestimado de USDC. Essa grande troca empurrou o preço ao extremo, movendo o tick de 5.000 para 839.189.

Passo 2: Após a formação do preço extremo, o atacante imediatamente reverteu a operação, trocando uma parte do USDC de volta por USDT. Como o preço do pool estava agora severamente desalinhado, o valor de retorno da função queryLDF() para a densidade de liquidez do USDC caiu para 1. Isso fez com que o valor de liquidez estimado com base no USDC fosse maior do que o valor estimado com base no USDT.

De acordo com a lógica do protocolo de selecionar o menor valor, a liquidez total é determinada pelo saldo de USDT. Isso fez com que a liquidez calculada revertesse imediatamente de um estado subavaliado para um nível normal, resultando em um aumento repentino. O atacante explorou essa mudança, trocando uma quantidade mínima de USDC por uma grande quantidade de USDT, completando assim a arbitragem e realizando lucro.

Resumo

Este incidente foi causado, em última análise, por erros de arredondamento no ajuste dos saldos ociosos durante a remoção de liquidez. Embora esse design de função de truncamento tenha sido planejado como uma estratégia de segurança nos cálculos de liquidez, ele falhou em considerar adequadamente condições críticas de contorno. Especificamente, os erros de arredondamento são amplificados de forma não linear quando os saldos de tokens estão severamente desequilibrados.

Este incidente revela os riscos de acoplamento entre múltiplos módulos em protocolos DeFi complexos. Mesmo que as regras de arredondamento de componentes individuais sejam projetadas de forma conservadora, a falta de validação de segurança consistente em todo o sistema pode levar a vulnerabilidades críticas que podem ser exploradas em circunstâncias específicas.

Referências

  1. https://x.com/bunni_xyz/status/1962833866277744953

  2. https://etherscan.io/tx/0x1c27c4d625429acfc0f97e466eda725fd09ebdc77550e529ba4cbdbc33beb97b

  3. https://uniscan.xyz/tx/0x4776f31156501dd456664cd3c91662ac8acc78358b9d4fd79337211eb6a1d451

  4. https://x.com/bunni_xyz/status/1981160279871558114

  5. https://docs.bunni.xyz/docs/v2/overview

  6. https://github.com/Bunniapp/bunni-v2/blob/2b303b8c1b9f8afbb169d62ba52da93d6d2171fe/src/lib/QueryLDF.sol#L40

  7. https://blog.bunni.xyz/posts/exploit-post-mortem/


Sobre a BlockSec

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

A BlockSec publicou diversos artigos de segurança em blockchain em conferências de prestígio, reportou vários ataques zero-day em aplicações DeFi, bloqueou múltiplos 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