Back to Blog

#8 Incidente Bunni: Retiros pequeños repetidos convierten un error de redondeo en una pérdida de $8.4M

Code Auditing
February 12, 2026
8 min read

El 2 de septiembre de 2025, el protocolo Bunni V2 sufrió un exploit sofisticado [1]. Un atacante aprovechó una vulnerabilidad crítica en su mecanismo de contabilidad de liquidez para extraer aproximadamente 8,4 millones de USD de dos pools de liquidez: el pool USDC/USDT en Ethereum [2] y el pool weETH/ETH en Unichain [3].

La causa raíz fue un error de redondeo en la actualización de los saldos inactivos del pool durante la eliminación de liquidez. Este error provocó una subvaloración significativa de la liquidez total en el contrato, creando una discrepancia explotable entre la liquidez teórica y la real. El atacante ejecutó entonces un ataque sandwich preciso para beneficiarse de esta disparidad.

Este incidente resultó directamente en graves pérdidas financieras para el protocolo Bunni, que posteriormente declaró la quiebra el 23 de octubre de 2025 [4].

Antecedentes

Bunni V2 es un protocolo de Creador de Mercado Automatizado (AMM) construido sobre Uniswap V4. Implementa su lógica central a través del mecanismo de hooks e introduce innovaciones sobre el algoritmo de liquidez concentrada de Uniswap V3, con el objetivo de proporcionar una mayor eficiencia de capital para los Proveedores de Liquidez (LPs) [5].

Específicamente, el protocolo mejora los retornos de los LPs principalmente a través de una función de Rehipotecación y un mecanismo de Rebalanceo. La primera asigna liquidez a protocolos externos generadores de rendimiento, asegurando la liquidez base mientras captura rendimiento externo adicional. El segundo optimiza continuamente la distribución de la liquidez entre rangos de precios, aumentando la utilización activa del capital para incrementar los ingresos por comisiones. Estos dos mecanismos forman las innovaciones principales del protocolo sobre el modelo fundamental de liquidez concentrada.

Rehipotecación

Para mejorar los retornos de los Proveedores de Liquidez, Bunni V2 emplea una estrategia de Rehipotecación. Esta estrategia asigna fondos a diferentes posiciones:

  • rawBalance: Una porción de las reservas del pool para un token se almacena directamente dentro del contract PoolManager de Uniswap V4. Esto sirve como liquidez disponible al instante para facilitar los swaps.
  • reserves: El resto se deposita en una bóveda ERC4626 especificada. Esto permite a los usuarios obtener rendimiento externo adicional sobre estos activos.

Por lo tanto, el total de activos para un pool se define como: Activos del Pool = rawBalance + cantidad subyacente de reserves.

Rebalanceo

Para aumentar los ingresos por comisiones, Bunni V2 implementa un mecanismo de rebalanceo que monitorea el precio promedio ponderado en el tiempo. Cuando el cambio de precio supera un umbral, la liquidez se redistribuye entre diferentes rangos de precios según la Función de Distribución de Liquidez (LDF).

Esta reasignación puede modificar la proporción de tokens requerida por la LDF, dejando un excedente en un token. Este excedente se define como el saldo inactivo.

Por lo tanto, la liquidez se divide en dos partes:

  • Saldo Activo: La porción asignada por la LDF que participa en el cálculo de liquidez.
  • Saldo Inactivo: El excedente no utilizado para liquidez activa.

Por lo tanto, Activos del Pool = Saldo Activo + Saldo Inactivo.

Funciones clave: cálculo de liquidez y retiros

Este ataque explota dos funciones críticas: queryLDF() y withdraw(). La función queryLDF() calcula la liquidez del pool para los swaps, mientras que la función withdraw() permite a los usuarios retirar una liquidez proporcional.

Funciones queryLDF()

Debido a la estrategia de Rehipotecación, la cantidad de activos subyacentes es dinámica, y Bunni V2 no almacena un valor fijo de "liquidez total". En cambio, el protocolo proporciona la función queryLDF() para obtener la liquidez en tiempo real cuando ocurre un swap [6]. El proceso de ejecución de esta función consta de los siguientes cuatro pasos:

  1. Consulta de Densidad de Liquidez:

    1. Invocar la función de densidad de liquidez ldf.query() que obtiene la densidad de liquidez fuera del rango de ticks del precio actual.

    2. Invocar LiquidityAmounts.getAmountsForLiquidity() para obtener la densidad dentro del rango de tick actual.

    3. Calcular la densidad total de liquidez de token0 y token1 en ambas direcciones, denotada como totalDensity0 y totalDensity1.

    Cabe destacar que la función LiquidityAmounts.getAmountsForLiquidity() utiliza redondeo hacia arriba para garantizar que las cantidades de tokens calculadas sean de forma conservadora no menores que los valores teóricos.

  2. Calcular el Saldo Disponible

    Los saldos disponibles utilizados para los cálculos de liquidez se denotan como balance0 y balance1. El saldo inactivo se deduce del saldo total del token correspondiente, excluyendo los fondos que no participan en los cálculos de liquidez.

    En este ataque, donde los fondos inactivos del pool consistían en token0, las fórmulas de cálculo son:

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

    • balance1=rawBalance1+reserve1balance1 = rawBalance1 + reserve1

  3. Estimar la Liquidez Efectiva

    1. Estimar la liquidez que cada token puede soportar en función de su saldo disponible real (balance0 o balance1) y la densidad total calculada (totalDensity0 o totalDensity1).

    2. Seleccionar el menor de las dos estimaciones como la liquidez total efectiva final.

    La fórmula es la siguiente:

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

  4. Calcular los Saldos Activos

En función de la liquidez total determinada, el protocolo calcula la cantidad real de tokens disponibles para el trading. Esto se define como el Saldo Activo.

Función withdraw()

Bunni V2 proporciona la función withdraw() para retirar liquidez. Los usuarios retiran liquidez proporcional a su participación en los fondos totales del pool. El protocolo actualiza el rawBalance, reserves y el idleBalance en la misma proporción. La fórmula de ajuste es la siguiente:

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

Donde:

  • shares es el número de participaciones de liquidez que el usuario retira;
  • totalSupply es la oferta total de tokens de liquidez para ese pool.

Análisis de Vulnerabilidad

La vulnerabilidad se origina en el cálculo del monto de ajuste del saldo inactivo por parte de la función withdraw(), que utiliza redondeo hacia abajo (es decir, truncamiento). Esto resulta en una sobreestimación del saldo inactivo.

Recordando la fórmula del saldo disponible**,** balance=rawBalance+reservesaldo inactivobalance = rawBalance + reserve - saldo\ inactivo. Un saldo inactivo sobreestimado causa directamente que el saldo disponible (balance0) utilizado para los cálculos de liquidez sea subestimado. En consecuencia, la liquidez total efectiva estimada también queda subvalorada. Según el Post Mortem del Exploit de Bunni [7], esta dirección de redondeo en los cálculos de liquidez fue empleada intencionalmente. Un valor de liquidez calculado más bajo conduce a un mayor impacto de precio durante los swaps.

Este diseño se basa en un supuesto crítico: la proporción de saldo entre los dos tokens se mantiene relativamente equilibrada. En condiciones normales con liquidez adecuada, los valores de liquidez total estimados por separado para cada token suelen estar cerca. El impacto del error de redondeo está, por lo tanto, limitado. Sin embargo, cuando el saldo disponible del token que lleva un saldo inactivo se vuelve extremadamente bajo, el fallo emerge. En este escenario, el error de redondeo hacia abajo se amplifica significativamente.

El atacante explotó esta vulnerabilidad realizando una serie de pequeños retiros, reduciendo por redondeo hacia abajo el saldo disponible de token0 de 28 wei a 4 wei. Esta caída superó con creces la proporción de participaciones de liquidez realmente quemadas. Mientras tanto, el saldo disponible de token1 se mantuvo en un nivel relativamente normal. Este desequilibrio creó una ventana de arbitraje significativa. El siguiente capítulo proporciona un análisis numérico detallado.

Análisis del Ataque

Tomando la transacción de Ethereum [2] como ejemplo, el atacante ejecutó un ataque en tres etapas:

  • En la primera etapa, el atacante realizó una manipulación de precios para agotar significativamente el saldo disponible de USDC (token0). Esto creó las condiciones iniciales necesarias para amplificar el posterior error de redondeo.
  • En la segunda etapa, se realizó el exploit principal mediante una serie de pequeños retiros, haciendo que el protocolo subestimara la liquidez real del pool.
  • En la tercera etapa, el atacante ejecutó dos swaps direccionales para arbitrar la discrepancia entre la liquidez subestimada del protocolo y la liquidez real del pool, extrayendo finalmente beneficios.

Etapa 1: Manipular el precio y reducir el saldo del token objetivo

El atacante ejecutó tres transacciones de swap, manipulando el precio de USDC (token0) relativo a USDT (token1), llevándolo desde un tick inicial = -1 hasta tick = 5000. El propósito principal fue agotar el saldo activo de USDC del pool, reduciéndolo a un nivel extremadamente bajo de 28 wei. Esto creó las condiciones iniciales necesarias para amplificar el posterior error de redondeo en la siguiente fase.

Etapa 2: Explotar los retiros para amplificar las discrepancias de liquidez

El atacante inició 44 pequeños retiros a través de la función withdraw(). Debido al redondeo hacia abajo utilizado por esta función al actualizar el idleBalance, el saldo inactivo del protocolo quedó sobreestimado. Esto subestimó aún más el saldo disponible de USDC en la función queryLDF(). Tras estas operaciones repetidas, el saldo disponible de USDC fue suprimido de forma anormal de 28 wei a 4 wei. Esto representó una reducción real del 85,7%, muy superior a la proporción teórica correspondiente a las participaciones de liquidez eliminadas (es decir, 8,998105442969973e-07%). En ese momento, la liquidez estimada de USDC en el pool estaba gravemente subestimada.

Etapa 3: Ejecutar el arbitraje y realizar beneficios

El atacante ejecutó entonces dos swaps direccionales, constituyendo una operación similar a un ataque sandwich.

Paso 1: El atacante utilizó una gran cantidad de USDT para intercambiar por USDC. En ese momento, el cálculo de liquidez interno estaba gravemente subvalorado basándose en el saldo de USDC subestimado. Este gran swap empujó el precio a un extremo, moviendo el tick de 5.000 a 839.189.

Paso 2: Tras formarse el precio extremo, el atacante invirtió inmediatamente la operación, intercambiando una porción del USDC de vuelta por USDT. Dado que el precio del pool estaba ahora gravemente desalineado, el valor de retorno de la función queryLDF() para la densidad de liquidez de USDC cayó a 1. Eso hizo que el valor de liquidez estimado basado en USDC fuera mayor que el valor estimado basado en USDT.

Según la lógica del protocolo de seleccionar el valor menor, la liquidez total está determinada por el saldo de USDT. Esto provocó que la liquidez calculada revirtiera inmediatamente desde un estado subvalorado a un nivel normal, resultando en un aumento repentino. El atacante aprovechó este cambio, intercambiando una cantidad mínima de USDC por una gran cantidad de USDT, completando así el arbitraje y obteniendo beneficios.

Resumen

Este incidente fue causado en última instancia por errores de redondeo al ajustar los saldos inactivos durante la eliminación de liquidez. Si bien este diseño de función de truncamiento fue concebido como una estrategia de seguridad en los cálculos de liquidez, no consideró adecuadamente las condiciones de contorno críticas. Específicamente, los errores de redondeo se amplían de forma no lineal cuando los saldos de tokens están gravemente desequilibrados.

Este incidente revela los riesgos de acoplamiento entre múltiples módulos en los complejos protocolos DeFi. Incluso si las reglas de redondeo de los componentes individuales están diseñadas de forma conservadora, la falta de una validación de seguridad coherente en todo el sistema puede dar lugar a vulnerabilidades críticas que pueden ser explotadas en circunstancias específicas.

Referencia

  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/


Acerca de BlockSec

BlockSec es un proveedor integral de seguridad blockchain y cumplimiento normativo en criptomonedas. Desarrollamos productos y servicios que ayudan a los clientes a realizar auditorías de código (incluyendo contratos inteligentes, blockchain y billeteras), interceptar ataques en tiempo real, analizar incidentes, rastrear fondos ilícitos y cumplir con las obligaciones AML/CFT, a lo largo del ciclo de vida completo de protocolos y plataformas.

BlockSec ha publicado múltiples artículos de seguridad blockchain en conferencias de prestigio, ha reportado varios ataques de día cero en aplicaciones DeFi, ha bloqueado múltiples hackeos para rescatar más de 20 millones de dólares y ha asegurado miles de millones en criptomonedas.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit