Back to Blog

Análise Aprofundada: O Incidente Truebit

Code Auditing
January 14, 2026
11 min read

Em 8 de janeiro de 2026, o Protocolo Truebit na Ethereum foi explorado, resultando em aproximadamente $26 milhões em perdas [1]. A causa raiz foi um overflow de inteiro na lógica de precificação de compra de tokens TRU. Como o contrato foi compilado com Solidity v0.6.10, que não aplica verificações de overflow por padrão, um valor intermediário grande no cálculo do custo de compra foi reduzido para um número muito menor. Como resultado, um atacante pôde comprar uma quantidade muito grande de TRU por pouco ou até zero ETH, e então vender imediatamente o TRU adquirido de volta ao contrato por ETH a uma taxa favorável, drenando as reservas do protocolo.

0x0 Contexto

O Truebit fornece serviços de computação para a Ethereum por meio de computação off-chain e verificação interativa [2]. Dentro do protocolo, os tokens TRU servem como o principal instrumento econômico para coordenar incentivos, incluindo staking e pagamentos relacionados a tarefas.

O protocolo expõe duas funções públicas para comprar e resgatar TRU:

  • buyTRU() executa compras de TRU. O custo em ETH necessário é calculado por uma função interna de precificação que também é usada por getPurchasePrice(), portanto getPurchasePrice() reflete a lógica exata de precificação on-chain aplicada durante a execução da compra.

  • sellTRU() executa vendas de TRU (resgates). O pagamento esperado em ETH pode ser consultado via getRetirePrice().

Um aspecto-chave do design é a assimetria de precificação:

  • As compras usam uma curva de bonding convexa (o preço marginal aumenta conforme a oferta aumenta).
  • As vendas usam uma regra de resgate linear (proporcional às reservas).

Como o código-fonte do contrato de implementação não é público, a análise a seguir é baseada em bytecode decompilado.

Lógica de compra

A função buyTRU() (e a função getPurchasePrice()) delega a precificação a uma função privada _getPurchasePrice() que calcula o ETH necessário para comprar amount TRU.

function buyTRU(uint256 amount) public payable {
    require(msg.data.length - 4 >= 32);
    v0 = _getPurchasePrice(amount); // obtém o preço 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 o status da chamada, propaga dados de erro em caso de falha
    return msg.value;
}

function getPurchasePrice(uint256 amount) public nonPayable {
    require(msg.data.length - 4 >= 32);
    v0 = _getPurchasePrice(amount); // obtém o preço 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 o status da chamada, propaga dados de erro em caso de falha
    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); // preçoDeCompra = (numerador_1 + numerador_2) / denominador
    return v13;
}

A partir da lógica decompilada, o preço de compra pode ser expresso como uma função no estilo de curva de bonding de:

Onde,

  • amount: TRU a ser comprado
  • reserve (_reserve): as reservas de Ether do contrato
  • totalSupply: o fornecimento total de TRU
  • θ (_setParameters): um coeficiente, fixado em 75

Essa curva tem o objetivo de tornar compras grandes cada vez mais caras (crescimento de custo convexo), desestimulando a especulação e reduzindo a manipulação imediata pelo lado comprador.

Lógica de venda

A função sellTRU() (e a função getRetirePrice()) utiliza a função privada _getRetirePrice() para calcular o ETH pago ao resgatar 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 o status da chamada, propaga dados de erro em caso de falha
    require(RETURNDATASIZE() >= 32);
    require(v1 >= amount, Error('Insufficient TRU allowance'));
    v2 = _getRetirePrice(amount); // obtém o preço de resgate
    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 o status da chamada, propaga dados de erro em caso de falha
    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 o status da chamada, propaga dados de erro em caso de falha
    v7 = msg.sender.call().value(v2).gas(!v2 * 2300);
    require(bool(v7), 0, RETURNDATASIZE()); // verifica o status da chamada, propaga dados de erro em caso de falha
    return v2;
}

function getRetirePrice(uint256 amount) public nonPayable {
    require(msg.data.length - 4 >= 32);
    v0 = _getRetirePrice(amount); // obtém o preço de resgate
    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 o status da chamada, propaga dados de erro em caso de falha
    require(RETURNDATASIZE() >= 32);
    v1 = v2.length;
    v3 = v2.data;
    v4 = 0x18ef(_reserve, amount);// numerador = _reserve * amount
    if (v1 > 0) {
        assert(v1);
        return v4 / v1;// preçoDeResgate = numerador / totalSupply
    } else {
    // ...
}

A regra de resgate é linear:

O preço de resgate é proporcional à fração do fornecimento total sendo resgatada (ou seja, amount / totalSupply) multiplicada pela reserve.

Essa assimetria deliberada cria um spread amplo: a compra é convexa (cara em escala), enquanto a venda é linear (resgata apenas uma parcela proporcional das reservas). Em condições normais, esse spread torna a arbitragem imediata compra→venda pouco atraente.

0x1 Análise de Vulnerabilidade

Apesar do design intencional de compras grandes são caras, _getPurchasePrice() contém um overflow de inteiro em sua aritmética. Como o contrato foi compilado com Solidity 0.6.10, operações aritméticas em uint256 podem sofrer overflow silencioso e envolver módulo 2^256 a menos que explicitamente protegidas (por exemplo, via 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 o status da chamada, propaga dados de erro em caso de falha
    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); // preçoDeCompra = (numerador_1 + numerador_2) / denominador
    return v13;
}

Em _getPurchasePrice(), um amount suficientemente grande aciona um overflow durante a adição de dois grandes termos do numerador (v12 + v9 no trecho decompilado). Quando esse overflow ocorre, o numerador é reduzido para um valor pequeno, o que faz com que a divisão final retorne um preço de compra artificialmente baixo, potencialmente zero.

Crucialmente, o overflow afeta apenas a precificação do lado comprador. A função do lado vendedor permanece linear e se comporta como esperado, portanto um atacante pode:

  • comprar uma grande quantidade de TRU a um custo subprecificado (ou zero), e então
  • resgatá-la por ETH via sellTRU() a uma taxa efetiva muito mais alta.

0x2 Análise do Ataque

O atacante realizou múltiplas rodadas de arbitragem dentro de uma única transação [3], repetindo: getPurchasePrice() -> buyTRU() -> sellTRU()

Primeira rodada: compra com custo zero, depois venda com lucro

Ao fornecer um valor de compra cuidadosamente escolhido (240.442.509.453.545.333.947.284.131), o atacante acionou um overflow em _getPurchasePrice(), reduzindo o preço de compra calculado para 0 ETH e permitindo a aquisição de ~240 milhões de TRU sem custo algum.

O código Python abaixo ilustra que o numerador excede 2^256 e, após o wrap, o preço de compra calculado se torna um valor fracionário ínfimo que é truncado para zero quando convertido para inteiro.

>>> _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

O atacante então chamou imediatamente sellTRU(), resgatando o TRU por 5.105 ETH das reservas do protocolo.

Rodadas subsequentes: compras de baixo custo, depois venda com lucro

O atacante repetiu o ciclo várias vezes. Compras posteriores nem sempre tinham custo estritamente zero, mas o overflow continuou mantendo os preços de compra muito abaixo dos retornos de venda correspondentes.

Ao longo dessas rodadas, o atacante extraiu uma quantidade substancial de ETH, e nossa investigação sugere que compras com custo zero adicionais ainda podem ter sido possíveis após a primeira rodada, embora o motivo pelo qual o atacante optou por algumas rodadas com custo não zero não esteja claro.

No total, o atacante drenou 8.535 ETH das reservas do Truebit.

0x3 Resumo

Este incidente foi causado, em última análise, por um overflow de inteiro não verificado na lógica de precificação do lado comprador do Truebit. Embora o modelo de precificação assimétrica de compra/venda do protocolo tivesse o objetivo de resistir à especulação, a compilação com uma versão mais antiga do Solidity (anterior à 0.8) sem proteção sistemática contra overflow comprometeu o design e possibilitou a drenagem das reservas.

Para qualquer contrato em produção que ainda use versões do Solidity abaixo da 0.8, os desenvolvedores devem:

  • Aplicar aritmética segura contra overflow (por exemplo, SafeMath ou verificações equivalentes) a cada operação relevante, ou
  • Preferencialmente migrar para o Solidity 0.8+ para se beneficiar das verificações de overflow padrão.

Referências

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

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

[3] Transação do ataque

Sobre a BlockSec

A BlockSec é uma provedora completa de segurança blockchain e conformidade cripto. Desenvolvemos produtos e serviços que ajudam os 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 blockchain em conferências de prestígio, reportou vários ataques de dia zero em aplicações DeFi, bloqueou múltiplos hacks 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