Back to Blog

Análisis en Profundidad: El Incidente de Truebit

Code Auditing
January 14, 2026
11 min read

El 8 de enero de 2026, el Protocolo Truebit en Ethereum fue explotado, resultando en aproximadamente $26 millones en pérdidas [1]. La causa raíz fue un desbordamiento de enteros en la lógica de precios de compra del token TRU. Debido a que el contrato fue compilado con Solidity v0.6.10, que no aplica verificaciones de desbordamiento por defecto, un valor intermedio grande en el cálculo del costo de compra se desbordó hacia un número mucho menor. Como resultado, un atacante podía comprar una cantidad muy grande de TRU por poco o incluso cero ETH, y luego vender inmediatamente el TRU adquirido de vuelta al contrato por ETH a una tasa favorable, drenando las reservas del protocolo.

0x0 Antecedentes

Truebit proporciona servicios de cómputo para Ethereum mediante cómputo fuera de cadena y verificación interactiva [2]. Dentro del protocolo, los tokens TRU sirven como el instrumento económico central para coordinar incentivos, incluyendo el staking y los pagos relacionados con tareas.

El protocolo expone dos funciones públicas para comprar y canjear TRU:

  • buyTRU() ejecuta las compras de TRU. El costo requerido en ETH es calculado por una función de precios interna que también es utilizada por getPurchasePrice(), por lo que getPurchasePrice() refleja la lógica de precios exacta en cadena aplicada durante la ejecución de la compra.

  • sellTRU() ejecuta las ventas de TRU (canjes). El pago esperado en ETH puede consultarse mediante getRetirePrice().

Un aspecto clave del diseño es la asimetría de precios:

  • Las compras utilizan una curva de bonificación convexa (el precio marginal aumenta a medida que aumenta la oferta).
  • Las ventas utilizan una regla de canje lineal (proporcional a las reservas).

Debido a que el código fuente del contrato de implementación no es público, el siguiente análisis se basa en bytecode descompilado.

Lógica de compra

La función buyTRU() (y la función getPurchasePrice()) delega los precios a una función privada _getPurchasePrice() que calcula el ETH requerido para comprar amount TRU.

function buyTRU(uint256 amount) public payable {
    require(msg.data.length - 4 >= 32);
    v0 = _getPurchasePrice(amount); // obtener el precio de compra
    require(msg.value == v0, Error('ETH payment does not match TRU order'));
    v1 = 0x18ef(100 - _setParameters, msg.value);
    v2 = _SafeDiv(100, v1);
    v3 = _SafeAdd(v2, _reserve);
    _reserve = v3;
    require(bool(stor_97_0_19.code.size));
    v4 = stor_97_0_19.mint(msg.sender, amount).gas(msg.gas);
    require(bool(v4), 0, RETURNDATASIZE()); // verifica el estado de la llamada, propaga datos de error en caso de error
    return msg.value;
}

function getPurchasePrice(uint256 amount) public nonPayable {
    require(msg.data.length - 4 >= 32);
    v0 = _getPurchasePrice(amount); // obtener el precio de compra
    return v0;
}

function _getPurchasePrice(uint256 amount) private { 
    require(bool(stor_97_0_19.code.size));
    v0, /* uint256 */ v1 = stor_97_0_19.totalSupply().gas(msg.gas);
    require(bool(v0), 0, RETURNDATASIZE()); // verifica el estado de la llamada, propaga datos de error en caso de error
    require(RETURNDATASIZE() >= 32);
    v2 = 0x18ef(v1, v1)
    v3 = 0x18ef(_setParameters, v2);
    v4 = 0x18ef(v1, v1);
    v5 = 0x18ef(100, v4);
    v6 = _SafeSub(v3, v5);// denominador = 100 * totalSupply**2 - _setParameters * totalSupply**2 
    v7 = 0x18ef(amount, _reserve);
    v8 = 0x18ef(v1, v7);
    v9 = 0x18ef(200, v8);// numerador_2 = 200 * totalSupply * amount * _reserve
    v10 = 0x18ef(amount, _reserve);
    v11 = 0x18ef(amount, v10);
    v12 = 0x18ef(100, v11);// numerador_1 = 100 * amount**2 * _reserve
    v13 = _SafeDiv(v6, v12 + v9); // precioDeCompra = (numerador_1 + numerador_2) / denominador
    return v13;
}

A partir de la lógica descompilada, el precio de compra puede expresarse como una función estilo curva de bonificación de:

Donde,

  • amount: TRU a comprar
  • reserve (_reserve): las reservas de Ether del contrato
  • totalSupply: el suministro total de TRU
  • θ (_setParameters): un coeficiente, fijo en 75

Esta curva está diseñada para hacer que las compras grandes sean cada vez más costosas (crecimiento de costo convexo), desalentando la especulación y reduciendo la manipulación inmediata del lado comprador.

Lógica de venta

La función sellTRU() (y la función getRetirePrice()) utiliza la función privada _getRetirePrice() para calcular el ETH pagado al canjear TRU.

function sellTRU(uint256 amount) public nonPayable {
    require(msg.data.length - 4 >= 32);
    require(bool(stor_97_0_19.code.size));
    v0, /* uint256 */ v1 = stor_97_0_19.allowance(msg.sender, address(this)).gas(msg.gas);
    require(bool(v0), 0, RETURNDATASIZE()); // verifica el estado de la llamada, propaga datos de error en caso de error
    require(RETURNDATASIZE() >= 32);
    require(v1 >= amount, Error('Insufficient TRU allowance'));
    v2 = _getRetirePrice(amount); // obtener el precio de retiro
    v3 = _SafeSub(v2, _reserve);
    _reserve = v3;
    require(bool(stor_97_0_19.code.size));
    v4, /* uint256 */ v5 = stor_97_0_19.transferFrom(msg.sender, address(this), amount).gas(msg.gas);
    require(bool(v4), 0, RETURNDATASIZE()); // verifica el estado de la llamada, propaga datos de error en caso de error
    require(RETURNDATASIZE() >= 32);
    require(bool(stor_97_0_19.code.size));
    v6 = stor_97_0_19.burn(amount).gas(msg.gas);
    require(bool(v6), 0, RETURNDATASIZE()); // verifica el estado de la llamada, propaga datos de error en caso de error
    v7 = msg.sender.call().value(v2).gas(!v2 * 2300);
    require(bool(v7), 0, RETURNDATASIZE()); // verifica el estado de la llamada, propaga datos de error en caso de error
    return v2;
}

function getRetirePrice(uint256 amount) public nonPayable {
    require(msg.data.length - 4 >= 32);
    v0 = _getRetirePrice(amount); // obtener el precio de retiro
    return v0;
}

function _getRetirePrice(uint256 amount) private { 
    require(bool(stor_97_0_19.code.size));
    v0, /* uint256 */ v1 = stor_97_0_19.totalSupply().gas(msg.gas);
    require(bool(v0), 0, RETURNDATASIZE()); // verifica el estado de la llamada, propaga datos de error en caso de error
    require(RETURNDATASIZE() >= 32);
    v1 = v2.length;
    v3 = v2.data;
    v4 = 0x18ef(_reserve, amount);// numerador = _reserve * amount
    if (v1 > 0) {
        assert(v1);
        return v4 / v1;// precioDeRetiro = numerador / totalSupply
    } else {
    // ...
}

La regla de canje es lineal:

El precio de retiro es proporcional a la fracción del suministro total que se está canjeando (es decir, amount / totalSupply) multiplicada por la reserve.

Esta asimetría deliberada crea una amplia diferencia: la compra es convexa (costosa a escala), mientras que la venta es lineal (canjea solo una parte proporcional de las reservas). En condiciones normales, esa diferencia hace que el arbitraje inmediato de compra→venta sea poco atractivo.

0x1 Análisis de Vulnerabilidad

A pesar del diseño previsto de las compras grandes son costosas, _getPurchasePrice() contiene un desbordamiento de enteros en su aritmética. Debido a que el contrato fue compilado con Solidity 0.6.10, las operaciones aritméticas sobre uint256 pueden desbordarse silenciosamente y envolverse módulo 2^256 a menos que estén explícitamente protegidas (p. ej., mediante SafeMath).

function _getPurchasePrice(uint256 amount) private { 
    require(bool(stor_97_0_19.code.size));
    v0, /* uint256 */ v1 = stor_97_0_19.totalSupply().gas(msg.gas);
    require(bool(v0), 0, RETURNDATASIZE()); // verifica el estado de la llamada, propaga datos de error en caso de error
    require(RETURNDATASIZE() >= 32);
    v2 = 0x18ef(v1, v1)
    v3 = 0x18ef(_setParameters, v2);
    v4 = 0x18ef(v1, v1);
    v5 = 0x18ef(100, v4);
    v6 = _SafeSub(v3, v5);// denominador = 100 * totalSupply**2 - _setParameters * totalSupply**2 
    v7 = 0x18ef(amount, _reserve);
    v8 = 0x18ef(v1, v7);
    v9 = 0x18ef(200, v8);// numerador_2 = 200 * totalSupply * amount * _reserve
    v10 = 0x18ef(amount, _reserve);
    v11 = 0x18ef(amount, v10);
    v12 = 0x18ef(100, v11);// numerador_1 = 100 * amount**2 * _reserve
    v13 = _SafeDiv(v6, v12 + v9); // precioDeCompra = (numerador_1 + numerador_2) / denominador
    return v13;
}

En _getPurchasePrice(), un amount suficientemente grande provoca un desbordamiento durante la suma de dos términos grandes del numerador (v12 + v9 en el fragmento descompilado). Cuando ocurre este desbordamiento, el numerador se envuelve hacia un valor pequeño, lo que hace que la división final devuelva un precio de compra artificialmente bajo, potencialmente cero.

Es fundamental que el desbordamiento afecta únicamente a la fijación de precios del lado de la compra. La función del lado de la venta permanece lineal y se comporta según lo previsto, por lo que un atacante puede:

  • comprar una gran cantidad de TRU a un costo infravalorado (o cero), y luego
  • canjearlo por ETH mediante sellTRU() a una tasa efectiva mucho más alta.

0x2 Análisis del Ataque

El atacante realizó múltiples rondas de arbitraje dentro de una sola transacción [3], repitiendo: getPurchasePrice() -> buyTRU() -> sellTRU()

Primera ronda: compra sin costo, luego venta con beneficio

Al suministrar una cantidad de compra cuidadosamente elegida (240,442,509.453,545,333,947,284,131), el atacante provocó un desbordamiento en _getPurchasePrice(), reduciendo el precio de compra calculado a 0 ETH y permitiendo la adquisición de ~240 millones de TRU sin costo alguno.

El siguiente código de verificación en Python ilustra que el numerador supera 2^256 y, tras el desbordamiento, el precio de compra calculado se convierte en un valor fraccional diminuto que se trunca a cero al convertirse a entero.

>>> _reserve = 0x1ceec1aef842e54d9ee
>>> totalSupply = 161753242367424992669183203
>>> amount = 240442509453545333947284131
>>> numerator = int(100 * amount * _reserve * (amount + 2 * totalSupply))
>>> numerator > 2**256
True
>>> denominator = (100 - 75) * totalSupply**2
>>> purchasePrice = (numerator - 2**256) / denominator
>>> purchasePrice
0.00025775798757211426
>>> int(purchasePrice)
0

El atacante luego llamó inmediatamente a sellTRU(), canjeando el TRU por 5,105 ETH de las reservas del protocolo.

Rondas posteriores: compras de bajo costo, luego venta con beneficio

El atacante repitió el ciclo múltiples veces. Las compras posteriores no siempre tuvieron un costo estrictamente cero, pero el desbordamiento continuó manteniendo los precios de compra muy por debajo de los retornos de venta correspondientes.

A lo largo de estas rondas, el atacante extrajo una cantidad sustancial de ETH, y nuestra investigación sugiere que compras adicionales sin costo podrían haber seguido siendo posibles después de la primera ronda, aunque la razón por la que el atacante optó por algunas rondas con costo no nulo no está clara.

En total, el atacante drenó 8,535 ETH de las reservas de Truebit.

0x3 Resumen

Este incidente fue causado en última instancia por un desbordamiento de enteros no verificado en la lógica de precios del lado de la compra de Truebit. Aunque el modelo de precios asimétrico de compra/venta del protocolo estaba destinado a resistir la especulación, compilar con una versión anterior de Solidity (anterior a la 0.8) sin protección sistemática contra desbordamientos socavó el diseño y permitió el drenaje de reservas.

Para cualquier contrato en producción que todavía utilice versiones de Solidity inferiores a la 0.8, los desarrolladores deberían:

  • Aplicar aritmética segura contra desbordamientos (p. ej., SafeMath o verificaciones equivalentes) a cada operación relevante, o
  • Preferiblemente migrar a Solidity 0.8+ para beneficiarse de las verificaciones de desbordamiento por defecto.

Referencias

[1] https://x.com/Truebitprotocol/status/2009328032813850839

[2] https://docs.truebit.io/v1docs

[3] Transacción del ataque

Acerca de BlockSec

BlockSec es un proveedor integral de seguridad blockchain y cumplimiento de 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