Back to Blog

Análisis post-mortem del exploit de zkLend: Descifrando los detalles y aclarando malentendidos del ataque de préstamo flash de $10M

February 20, 2025
9 min read

El 12 de febrero de 2025, zkLend [1], un protocolo de préstamos en StarkNet, fue explotado por aproximadamente $10M mediante una sofisticada manipulación de su mecanismo de acumulador. El atacante aprovechó préstamos flash y vulnerabilidades de redondeo para inflar artificialmente los valores de colateral, tomando prestados otros activos del protocolo para obtener ganancias.

Sin embargo, sigue existiendo una falta de análisis técnico detallado y preciso desde una perspectiva de seguridad. A pesar de los análisis existentes por parte de otros investigadores de seguridad, que proporcionaron información valiosa, persisten algunos malentendidos, especialmente en lo que respecta al análisis del ataque. La posterior publicación del post-mortem oficial de zkLend [2] ofrece una descripción simplificada pero carece de un análisis técnico detallado. En este blog, nuestro objetivo es proporcionar un examen exhaustivo para aclarar el incidente.

Puntos Clave (TL;DR)

  • La causa raíz de este incidente proviene de la combinación de los siguientes tres problemas:

    • La inicialización del mercado vacío permite depósitos arbitrarios de activos.
    • El mecanismo específico de donación en el préstamo flash de zkLend permite la manipulación del acumulador, una variable global como factor de escala para ajustar dinámicamente los saldos de colateral de los usuarios.
    • La pérdida de precisión ocurre debido al truncamiento. A diferencia de la pérdida de precisión clásica en la división, el denominador comienza en 1 pero fue inflado a un valor muy grande, causando una subestimación durante la quema del token de participación.
  • El atacante no obtuvo ganancias del wstETH depositado por otros usuarios. En cambio, el atacante aprovechó las vulnerabilidades para manipular el saldo de colateral, utilizando una pequeña cantidad de wstETH como capital inicial para aumentar el saldo de colateral hasta más de 7,000 wstETH, permitiendo así el préstamo de otros activos del mercado.

En las secciones siguientes, primero ofreceremos información de contexto crucial sobre zkLend. Posteriormente, realizaremos un análisis en profundidad de los problemas y el ataque asociado.

0x1 Contexto: Entendiendo el Protocolo Central de zkLend

zkLend es un proyecto de préstamos en StarkNet que admite protocolos de préstamos comunes como préstamos con colateral y préstamos flash. Profundicemos en los detalles de implementación de estos dos protocolos.

0x1.1 Préstamos con Colateral

Un préstamo con colateral se refiere al proceso en el que los usuarios depositan activos específicos en el protocolo como garantía a cambio de tomar prestados otros activos. El valor del colateral se utiliza para determinar la capacidad de endeudamiento. Es importante destacar que los protocolos de préstamos típicamente no almacenan el valor del activo del colateral directamente; en cambio, lo calculan usando la fórmula:

collateral_balance = lending_accumulator * raw_balance

Específicamente, el lending_accumulator es un factor de escala que ajusta dinámicamente el valor del colateral de cada usuario, mientras que raw_balance representa la participación real que el usuario posee en el mercado. El raw_balance se deriva del collateral_balance usando el lending_accumulator.

¿Cuál es el propósito de este diseño? Permite al protocolo gestionar eficientemente el valor del colateral mientras incentiva a los usuarios a depositar activos. Al asignar una parte de las ganancias del protocolo a los proveedores de colateral, el lending_accumulator aumenta, amplificando así el valor del colateral de todos los usuarios de forma proporcional y simultánea.

0x1.2 Préstamos Flash en zkLend

Un préstamo flash es un tipo de préstamo sin colateral donde los usuarios pueden tomar prestados activos del protocolo por un período muy corto, típicamente dentro de una sola transacción. Si el prestatario no logra devolver el préstamo o cumplir las condiciones especificadas, la transacción completa se revierte y el préstamo no se ejecuta.

En la implementación de préstamos flash de zkLend, existe un mecanismo de donación único. Específicamente, cuando los usuarios devuelven activos, no solo regresan la cantidad mínima requerida, sino que también pueden contribuir fondos adicionales como donación. El protocolo rastrea estos fondos donados y actualiza el lending_accumulator en consecuencia. Este proceso está implementado en la función thesettle_extra_reserve_balance(). La fórmula para actualizar el lending_accumulator es la siguiente:

new_accumulator = (reserve_balance + totaldebt - amount_to_treasury) / ztoken_supply

  • reserve_balance: La cantidad total del token subyacente (p. ej., wstETH) mantenido en el contrato, que incluye la cantidad de tokens donados por los usuarios.
  • totaldebt: La deuda total de todos los usuarios que toman préstamos.
  • amount_to_treasury: La cantidad de ingresos del protocolo.
  • ztoken_supply: El suministro total del token de participación (p. ej., zwstETH). Cuando los usuarios depositan wstETH, el contrato ztoken de zkLend acuña una cantidad equivalente de zwstETH.

Habiendo comprendido el protocolo central de zkLend, ahora explicaremos formalmente cómo el atacante manipuló sus activos de colateral mediante la manipulación de las variables lending_accumulator y raw_balance.

0x2 Análisis del Ataque

El atacante explotó los siguientes mecanismos y vulnerabilidades en el contrato de zkLend para manipular el valor del colateral:

  • Manipulación de lending_accumulator
    • Mercado vacío: Antes del ataque, el mercado zkLend para tokens wstETH estaba vacío, proporcionando las condiciones perfectas para la manipulación. Además, el contrato del Mercado zkLend permite a cualquiera depositar cualquier cantidad de activos en un mercado vacío. El atacante depositó una pequeña cantidad de activos para inflar significativamente el valor del lending_accumulator.
    • Mecanismo de donación: La función flash_loan() del contrato del Mercado zkLend presenta un mecanismo de donación único. Específicamente, cuando un usuario devuelve un préstamo flash, el contrato del Mercado calcula el exceso de fondos devueltos e incrementa la variable global lending_accumulator, amplificando así los valores de colateral para todos los usuarios en el contrato.
  • Manipulación de raw_balance
    • Comportamiento de redondeo: La operación de división durante el proceso de quema del token de participación utiliza truncamiento, lo que lleva a una subestimación del cambio en el raw_balance del usuario durante las retiradas.

Al manipular ambas variables, el atacante pudo aumentar el saldo de colateral a más de 7,000 wstETH y tomar prestados otros activos del mercado para obtener ganancias.

0x2.1 Manipulando la Variable lending_accumulator

0x2.1.1 Inicialización del Mercado Vacío

Al examinar el registro de transacciones del contrato del Mercado previo al ataque, podemos observar que el atacante inicialmente deposita 1 wei de wstETH en el contrato del Mercado wstETH. Al revisar las llamadas internas de esta transacción, es evidente que el contrato del Mercado wstETH mantenía 0 wstETH, y el suministro total de zwstETH también era 0.

Por lo tanto, podemos confirmar que no había depósitos ni préstamos previos en el mercado wstETH de zkLend. Tanto el reserve_balance como el ztoken_supply estaban en sus valores iniciales de 0, y el valor inicial del lending_accumulator era 1. Este escenario de mercado vacío creó las condiciones para el ataque posterior, permitiendo al atacante amplificar significativamente el lending_accumulator con una cantidad mínima de wstETH.

0x2.1.2 Manipulando lending_accumulator mediante Préstamo Flash

A continuación, en esta transacción, el atacante llama a la función flash_loan(), tomando prestado 1 wei de wstETH y devolviendo 1000 wei de wstETH. El exceso de 999 wei se trata como una donación y se registra en el reserve_balance del contrato.

Según la fórmula para calcular el lending_accumulator, esta transacción hace que el lending_accumulator aumente de 1 a 851.0.

0x2.1.3 Ejecución Repetida de flash_loan()

El atacante ejecuta un total de 10 llamadas a flash_loan(), tomando prestado cada vez solo 1 wei de wstETH pero devolviendo una cantidad mayor. Como resultado, el lending_accumulator escala hasta un valor astronómico de 4,069,297,906,051,644,020 (4.069 × 10^18), que coincide con la precisión decimal de wstETH.

0x2.2 Manipulando la Variable raw_balance

Después de manipular el lending_accumulator a aproximadamente 4.069 × 10^18, el atacante llamó a la función deposit() del contrato del Mercado con 4.069297906051644020 wstETH. Basándose en el último valor del lending_accumulator, el raw_balance del contrato del ataque se convirtió en 2.

0x2.2.1 La Primera Transacción Manipulando raw_balance

En esta transacción, el atacante llamó a la función callflashloandraaan() del contrato de ataque. Aunque este contrato no es de código abierto, basándose en el rastro de llamadas internas, se puede especular que la lógica de esta función incluye un bucle que realiza las siguientes acciones:

  • Depositar: El atacante deposita una cierta cantidad de wstETH en el contrato del mercado.
  • Retirar: El atacante retira la cantidad específica de wstETH.

Análisis del Registro de Transferencias de Tokens

Se puede observar que la cantidad de wstETH que el atacante deposita es siempre un múltiplo entero del lending_accumulator, por ejemplo, 2 veces el valor (p. ej., 8.13859) del lending_accumulator.

Sin embargo, la cantidad de wstETH retirada es 1.5 veces el valor (p. ej., 6.10394) del lending_accumulator.

Mediante cálculos, podemos determinar que la cantidad de wstETH retirada supera la cantidad depositada. ¿Por qué ocurre esto?

Comportamiento de Redondeo

Al revisar la implementación de los métodos deposit() y withdraw(), podemos ver que estos dos métodos involucran la acuñación y quema de zwstETH, respectivamente. Así es como funciona:

Función `mint()` en el contrato del Mercado

Función `burn()` en el contrato del Mercado

Los procesos mint() y burn() incluyen ambos una lógica de reducción de escala. La lógica de reducción de escala involucra división entera con redondeo hacia abajo (truncamiento al entero más cercano), que juega un papel clave en el exploit.

Cuando el atacante quema una cierta cantidad de zwstETH, se aplica la lógica de reducción de escala. Debido al valor manipulado del lending_accumulator siendo excepcionalmente alto (alrededor de 4,069,297,906,051,644,020), esta división hace que el raw_balance del atacante disminuya solo 1 unidad, a pesar de quemar más de 6 zwstETH.

Los cambios en el raw_balance del atacante se resumen en la siguiente tabla:

Podemos observar que en esta transacción, el atacante ejecuta repetidamente la lógica Depositar - Retirar, explotando la pérdida de precisión durante la función withdraw(), lo que resulta en una subestimación de la diferencia en raw_balance. En última instancia, el raw_balance del usuario aumentó de 2 a 3, ganando una unidad adicional.

0x2.2.2 Proceso de Ataque Posterior

Las transacciones de ataque posteriores siguieron el mismo patrón que el primer ataque: el atacante cicla repetidamente a través de transacciones Depositar - Retirar para adquirir wstETH.

El wstETH adquirido se vuelve a depositar en el mercado, aumentando aún más el raw_balance, haciendo que el valor del colateral del atacante siga aumentando.

Explicación con ejemplo

Usamos la siguiente transacción como ilustración.

  • Se realizaron un total de 30 depósitos, con 4.069 wstETH depositados cada vez.
  • Se realizaron un total de 30 retiros, con 6.104 wstETH retirados cada vez.
  • Después de este ciclo, el atacante extrajo exitosamente 61.39 wstETH, según los cálculos.

Además, vale la pena señalar que entre estas transacciones de ataque, se llamaron varios métodos increase(). Estos métodos se utilizaron para transferir una cantidad específica de wstETH desde la cuenta del atacante al contrato de ataque, que luego proporcionó los fondos para depósitos posteriores en el contrato del Mercado.

Estas acciones impulsan el valor de raw_balance, permitiendo al atacante continuar aumentando el valor del colateral. Eventualmente, el raw_balance del atacante alcanzó 1,724, con un valor de 7,015.4 wstETH, lo que fue suficiente para tomar prestados otros activos del mercado.

0x3 Análisis de Ganancias

0x3.1 Préstamo de Otros Tipos de Fondos

Después de manipular el valor del colateral, el atacante tomó prestados otros tipos de fondos del mercado y procedió con las siguientes transacciones (extracto):

0x3.2 Transferencia de los Fondos Prestados a Capa 1

Al inspeccionar las transacciones de puente del contrato del atacante, se puede observar que el atacante transfirió parte de los fondos prestados a la Capa 1.

0x4 Conclusión

En resumen, este ataque al protocolo zkLend resalta varias implicaciones importantes para el diseño y la seguridad de los protocolos de préstamos descentralizados:

  • Inicialización del Mercado y Condiciones de Depósito de Activos: El mercado vacío al inicio permitió al atacante depositar una pequeña cantidad de wstETH y manipular el lending_accumulator, obteniendo apalancamiento para el exploit. Garantizar una base de liquidez suficiente o limitar las donaciones de activos en las etapas tempranas del mercado podría ayudar a prevenir ataques similares.
  • Importancia de los Mecanismos de Acumulador Adecuados: El atacante explotó el mecanismo de donación en la función flash_loan() para manipular el lending_accumulator, inflando los valores de colateral para todos los usuarios. Los protocolos con mecanismos basados en acumuladores deben protegerse contra la fácil manipulación de los factores de escala.
  • Comportamiento de Redondeo y Pérdida de Precisión: Un problema de redondeo durante las quemas de tokens zwstETH llevó a la pérdida de precisión y subestimación del raw_balance, permitiendo al atacante manipular el raw_balance. Los protocolos deben usar mayor precisión o comprobaciones de validación para prevenir tales exploits.

Una vez más, este incidente subraya la importancia de las notificaciones oportunas respecto al estado de inicialización y operación, así como la prevención proactiva de amenazas para mitigar pérdidas potenciales.

Referencias

[1] https://zklend.com/

[2] Post-mortem del incidente de seguridad de zkLend: https://drive.google.com/file/d/10i1dh_J89tPPw7KRcmFIVM6iNrJZAyfi/view