Actualizado el 6 de noviembre de 2025: Balancer ha publicado su informe preliminar oficial [6], que confirma la causa raíz identificada en nuestro análisis.
El 3 de noviembre de 2025, los Composable Stable Pools de Balancer V2, junto con varios proyectos bifurcados en múltiples cadenas, sufrieron un exploit coordinado que resultó en pérdidas totales superiores a los $125 millones. BlockSec emitió una alerta en el momento más temprano posible [1] y posteriormente publicó un análisis inicial [2].
Este fue un ataque altamente sofisticado. Nuestra investigación revela que la causa raíz fue la manipulación de precios derivada de la pérdida de precisión en el cálculo del invariante, lo que a su vez distorsionó el cálculo del precio del BPT (Balancer Pool Token). Esta manipulación del invariante permitió al atacante obtener ganancias de un pool estable específico mediante un único batch swap. Si bien algunos investigadores han proporcionado análisis perspicaces, ciertas interpretaciones son engañosas, y la causa raíz y el proceso del ataque aún no han sido completamente esclarecidos. Este blog tiene como objetivo presentar un análisis técnico exhaustivo y preciso del incidente.
Puntos Clave (TL;DR)
Causa raíz: inconsistencia de redondeo y pérdida de precisión
- La operación de escalado ascendente utiliza redondeo unidireccional (redondeo hacia abajo), mientras que la operación de escalado descendente utiliza redondeo bidireccional (redondeo hacia arriba y hacia abajo).
- Esta inconsistencia genera pérdida de precisión que, cuando se explota a través de una ruta de intercambio cuidadosamente diseñada, viola el principio estándar de que el redondeo siempre debe favorecer al protocolo.
Ejecución del exploit
- El atacante diseñó deliberadamente los parámetros, incluido el número de iteraciones y los valores de entrada, para maximizar el efecto de la pérdida de precisión.
- El atacante utilizó un enfoque de dos etapas para evadir la detección: primero ejecutando el exploit principal dentro de una única transacción sin obtener ganancias inmediatas, y luego realizando las ganancias retirando activos en una transacción separada.
Impacto operacional y amplificación
- El protocolo no pudo ser pausado debido a ciertas restricciones [3]. Esta incapacidad para detener las operaciones agravó el impacto del exploit y permitió numerosos ataques posteriores o de imitación.
En las siguientes secciones, primero proporcionaremos información de contexto clave sobre Balancer V2, seguida de un análisis en profundidad de los problemas identificados y el ataque asociado.
0x1 Antecedentes
Composable Stable Pool de Balancer V2
El componente afectado en este ataque fue el Composable Stable Pool [4] del protocolo Balancer V2. Estos pools están diseñados para activos que se espera mantengan una paridad cercana a 1:1 (o que se negocien a una tasa de cambio conocida) y permiten grandes intercambios con un impacto mínimo en el precio, mejorando así significativamente la eficiencia de capital entre activos similares o correlacionados. Cada pool tiene su propio Balancer Pool Token (BPT), que representa la participación del proveedor de liquidez en el pool, junto con los activos subyacentes correspondientes.
- Este pool adopta Stable Math (basado en el modelo StableSwap de Curve), donde el invariante D representa el valor total virtual del pool.
- El precio del BPT puede aproximarse como:
De la fórmula anterior se puede observar que si D puede reducirse en papel (incluso sin ninguna pérdida real de fondos), el precio del BPT parecerá más barato.
batchSwap() y onSwap()
Balancer V2 proporciona la función batchSwap(), que permite intercambios de múltiples saltos dentro del Vault [5]. Existen dos tipos de intercambio determinados por un parámetro pasado a esta función:
- GIVEN_IN ("Dado de entrada"): el llamante especifica la cantidad exacta del token de entrada, y el pool calcula la cantidad de salida correspondiente.
- GIVEN_OUT ("Dado de salida"): el llamante especifica la cantidad de salida deseada, y el pool calcula la cantidad de entrada requerida.
Normalmente, un batchSwap() consiste en múltiples intercambios de token a token ejecutados a través de la función onSwap(). A continuación se describe la ruta de ejecución cuando a un SwapRequest se le asigna el tipo de intercambio GIVEN_OUT (nótese que ComposableStablePool hereda de BaseGeneralPool):
A continuación se muestra el cálculo de amount_in para el tipo de intercambio GIVEN_OUT, que involucra el invariante D.
Escalado y Redondeo
Para normalizar los cálculos entre diferentes balances de tokens, Balancer realiza las siguientes dos operaciones:
- Escalado ascendente: Escala los balances y las cantidades a una precisión interna unificada antes de realizar los cálculos.
- Escalado descendente: Convierte los resultados de vuelta a su precisión nativa, aplicando redondeo direccional (por ejemplo, las cantidades de entrada generalmente se redondean hacia arriba para garantizar que el pool no cobre de menos, mientras que las cantidades de salida frecuentemente se redondean hacia abajo).
La razón de esta inconsistencia no está clara. Según el comentario en la función _upscale(), los desarrolladores consideran que el impacto del redondeo en una sola dirección es mínimo.
// El redondeo en el escalado ascendente no necesariamente siempre iría en la misma dirección: en un intercambio, por ejemplo, el balance del
// token de entrada debería redondearse hacia arriba, y el del token de salida hacia abajo. Este es el único lugar donde redondeamos en
// la misma dirección para todas las cantidades, ya que se espera que el impacto de este redondeo sea mínimo (y no hay
// error de redondeo a menos que se sobreescriba_scalingFactor()).
0x2 Análisis de la Vulnerabilidad
El problema subyacente surge de la operación de redondeo hacia abajo realizada durante el escalado ascendente en la función BaseGeneralPool._swapGivenOut(). En particular, _swapGivenOut() redondea incorrectamente hacia abajo swapRequest.amount a través de la función _upscale(). El valor redondeado resultante se utiliza posteriormente como amountOut al calcular amountIn mediante _onSwapGivenOut(). Este comportamiento contradice la práctica estándar de que el redondeo debe aplicarse de manera que beneficie al protocolo.
Por lo tanto, para un pool dado (wstETH/rETH/cbETH), el amountIn calculado subestima la entrada realmente requerida. Esto permite a un usuario intercambiar una cantidad menor de un activo subyacente (por ejemplo, wstETH) por otro (por ejemplo, cbETH), reduciendo así el invariante D como resultado de una liquidez efectiva disminuida. En consecuencia, el precio del BPT correspondiente (wstETH/rETH/cbETH) queda artificialmente deflado, ya que precio del BPT = D / totalSupply.
0x3 Análisis del Ataque
El atacante ejecutó un ataque en dos etapas, probablemente para minimizar el riesgo de detección:
- En la primera etapa, el exploit principal se realizó dentro de una única transacción, sin obtener ganancias inmediatas.
- En la segunda etapa, el atacante realizó las ganancias retirando activos en una transacción separada.
La primera etapa puede dividirse a su vez en dos fases: cálculo de parámetros y batch swap. A continuación, ilustramos estas fases utilizando un ejemplo de transacción de ataque (TX) en Arbitrum.
La Fase de Cálculo de Parámetros
En esta fase, el atacante combinó cálculos fuera de la cadena con simulaciones en la cadena para ajustar con precisión los parámetros de cada salto en la siguiente fase (batch swap), basándose en el estado actual del Composable Stable Pool (incluidos los factores de escalado, el coeficiente de amplificación, la tasa del BPT, las comisiones de intercambio y otros parámetros). Curiosamente, el atacante también desplegó un contrato auxiliar para ayudar con estos cálculos, lo que puede haber tenido la intención de reducir la exposición al front-running.
Al inicio, el atacante recopila información básica sobre el pool objetivo, incluidos los factores de escalado de cada token, el parámetro de amplificación, la tasa del BPT y el porcentaje de comisión de intercambio. Luego calcula un valor clave llamado trickAmt, que es la cantidad manipulada del token objetivo utilizada para inducir pérdida de precisión.
Denotando el factor de escalado del token objetivo como sF, el cálculo es:
Para determinar los parámetros utilizados en el paso 2 de la siguiente fase (batch swap), el atacante realizó llamadas de simulación posteriores a la función 0x524c9e20 del contrato auxiliar con los siguientes calldata:
uint256[] balances; // Balances de los tokens del pool (excluyendo BPT)
uint256[] scalingFactors; // Factores de escalado para cada token del pool
uint tokenIn; // Índice del token de entrada para la simulación de este salto
uint tokenOut; // Índice del token de salida para la simulación de este salto
uint256 amountOut; // Cantidad deseada del token de salida
uint256 amp; // Parámetro de amplificación del pool
uint256 fee; // Porcentaje de comisión de intercambio del pool
Y los datos de retorno son:
uint256[] balances; // Balances de los tokens del pool (excluyendo BPT) después del intercambio
Específicamente, el balance inicial y el número de bucles de iteración se calcularon fuera de la cadena y se pasaron como parámetros al contrato del atacante (reportados como 100,000,000,000 y 25, respectivamente). Cada iteración realiza tres intercambios:
- Intercambio 1: Llevar la cantidad del token objetivo a trickAmt + 1, asumiendo que la dirección del intercambio es 0 → 1.
- Intercambio 2: Continuar intercambiando el token objetivo con trickAmt, lo que desencadena el redondeo hacia abajo en la invocación de _upscale().
- Intercambio 3: Ejecutar una operación de intercambio inverso (1 → 0), donde la cantidad a intercambiar se deriva del balance actual del token en el pool truncando los dos dígitos decimales más significativos, es decir, redondeando hacia abajo al múltiplo más cercano de , donde d es el número de dígitos decimales. Por ejemplo, 324,816 -> 320,000.
- Nótese que este paso puede fallar ocasionalmente debido al método de Newton–Raphson utilizado en el cálculo de StableMath. Para mitigar esto, el atacante implementa dos intentos de reintento, cada uno utilizando un valor de respaldo de 9/10 del valor original. El contrato auxiliar del atacante está derivado de la biblioteca StableMath de Balancer V2, como lo evidencia la inclusión de los mensajes de error personalizados al estilo "BAL".
La Fase de Batch Swap
Luego, la operación batchSwap() puede desglosarse en tres pasos:
-
Paso 1: El atacante intercambia BPT (wstETH/rETH/cbETH) por activos subyacentes para ajustar con precisión el balance de un token (cbETH) al límite de un umbral de redondeo (cantidad = 9). Esto establece las condiciones para la pérdida de precisión en el siguiente paso.
-
Paso 2: El atacante luego intercambia entre otro activo subyacente (wstETH) y cbETH usando una cantidad diseñada (= 8). Debido al redondeo hacia abajo al escalar las cantidades de tokens, el Δx calculado se vuelve ligeramente menor (de 8.918 a 8), lo que lleva a un Δy subestimado y, por tanto, a un invariante menor (D del modelo StableSwap de Curve). Dado que precio del BPT = D / totalSupply, el precio del BPT queda artificialmente deflado.
- Paso 3: El atacante intercambia de vuelta los activos subyacentes por BPT, restaurando el balance mientras obtiene ganancias del precio deflado del BPT.
0x4 Ataques y Pérdidas
Hemos resumido los ataques y sus pérdidas correspondientes en la tabla a continuación, con pérdidas totales superiores a $125 millones.
0x5 Conclusión
Este incidente involucró una serie de transacciones de ataque dirigidas al protocolo Balancer V2 y sus proyectos bifurcados, resultando en pérdidas financieras significativas. Tras el ataque inicial, se observaron numerosas transacciones posteriores y de imitación en múltiples cadenas. Este evento destaca varias lecciones críticas para el diseño y la seguridad de los protocolos DeFi:
-
Comportamiento de Redondeo y Pérdida de Precisión: El redondeo unidireccional (redondeo hacia abajo) utilizado en la operación de escalado ascendente difiere del redondeo bidireccional (redondeo hacia arriba y hacia abajo) utilizado en la operación de escalado descendente. Para prevenir vulnerabilidades similares, los protocolos deben emplear aritmética de mayor precisión e implementar controles de validación robustos. Es esencial mantener el principio estándar de que el redondeo siempre debe favorecer al protocolo.
-
Evolución de la Explotación: El atacante llevó a cabo un exploit sofisticado de dos etapas diseñado para evadir la detección. En la primera etapa, el atacante ejecutó el exploit principal dentro de una única transacción sin obtener ganancias inmediatas. En la segunda etapa, el atacante realizó las ganancias retirando activos en una transacción separada. Este incidente destaca una vez más la carrera armamentista en curso entre los investigadores de seguridad y los atacantes.
-
Conciencia Operacional y Respuesta a Amenazas: Este incidente subraya la importancia de las alertas oportunas sobre el estado de inicialización y operación, así como de los mecanismos proactivos de detección y prevención de amenazas para mitigar las pérdidas potenciales de ataques en curso o de imitación.
Mientras se mantiene la continuidad operativa y comercial, los participantes de la industria pueden aprovechar BlockSec Phalcon como la última línea de defensa para proteger sus activos. El equipo de expertos de BlockSec está listo para realizar una evaluación de seguridad integral para su proyecto.
Referencias
[1] https://x.com/Phalcon_xyz/status/1985262010347696312
[2] https://x.com/Phalcon_xyz/status/1985302779263643915
[3] https://x.com/Balancer/status/1985390307245244573
[4] https://docs-v2.balancer.fi/concepts/pools/composable-stable.html
[5] https://docs-v2.balancer.fi/reference/swaps/batch-swaps.html



