Back to Blog

Tiefgehende Analyse: Der Truebit-Vorfall

Code Auditing
January 14, 2026
10 min read

Am 8. Januar 2026 wurde das Truebit-Protokoll auf Ethereum ausgenutzt, was zu Verlusten von etwa 26 Millionen US-Dollar führte [1]. Die Hauptursache war ein Integer-Überlauf in der Kaufpreisberechnung des TRU-Tokens. Da der Vertrag mit Solidity v0.6.10 kompiliert wurde, das standardmäßig keine Überlaufprüfungen erzwingt, wickelte ein großer Zwischenwert in der Berechnung der Kaufkosten zu einer viel kleineren Zahl ab. Infolgedessen konnte ein Angreifer eine sehr große Menge TRU für wenig oder sogar null ETH kaufen, die erworbene TRU dann sofort zu einem günstigen Kurs zurück an den Vertrag verkaufen und so die Reserven des Protokolls erschöpfen.

0x0 Hintergrund

Truebit bietet Rechenservices für Ethereum durch Off-Chain-Berechnung und interaktive Verifizierung [2]. Innerhalb des Protokolls dienen TRU-Token als zentrales wirtschaftliches Instrument zur Koordinierung von Anreizen, einschließlich Staking und aufgabenbezogener Zahlungen.

Das Protokoll stellt zwei öffentliche Funktionen zum Kaufen und Einlösen von TRU bereit:

  • buyTRU() führt TRU-Käufe aus. Die erforderlichen ETH-Kosten werden durch eine interne Preisierungsfunktion berechnet, die auch von getPurchasePrice() verwendet wird, sodass getPurchasePrice() die genaue On-Chain-Preislogik widerspiegelt, die während der Kaufabwicklung angewendet wird.

  • sellTRU() führt TRU-Verkäufe (Einlösungen) aus. Die erwartete ETH-Auszahlung kann über getRetirePrice() abgefragt werden.

Ein wichtiges Designmerkmal ist die Preisierungsasymmetrie:

  • Käufe verwenden eine konvexe Bonding-Kurve (der Grenzkurs steigt mit zunehmendem Angebot).
  • Verkäufe verwenden eine lineare Einlösungsregel (proportional zu den Reserven).

Da der Quellcode des Implementierungsvertrags nicht öffentlich ist, basiert die folgende Analyse auf dekompiliertem Bytecode.

Kauflogik

Die Funktion buyTRU() (und die Funktion getPurchasePrice()) delegiert die Preisbildung an eine private Funktion _getPurchasePrice(), die die für den Kauf von amount TRU benötigten ETH berechnet.

function buyTRU(uint256 amount) public payable {
    require(msg.data.length - 4 >= 32);
    v0 = _getPurchasePrice(amount); // Kaufpreis ermitteln
    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()); // prüft den Aufrufstatus, leitet Fehlerdaten bei Fehler weiter
    return msg.value;
}

function getPurchasePrice(uint256 amount) public nonPayable {
    require(msg.data.length - 4 >= 32);
    v0 = _getPurchasePrice(amount); // Kaufpreis ermitteln
    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()); // prüft den Aufrufstatus, leitet Fehlerdaten bei Fehler weiter
    require(RETURNDATASIZE() >= 32);
    v2 = 0x18ef(v1, v1)
    v3 = 0x18ef(_setParameters, v2);
    v4 = 0x18ef(v1, v1);
    v5 = 0x18ef(100, v4);
    v6 = _SafeSub(v3, v5);// Nenner = 100 * totalSupply**2 - _setParameters * totalSupply**2
    v7 = 0x18ef(amount, _reserve);
    v8 = 0x18ef(v1, v7);
    v9 = 0x18ef(200, v8);// Zähler_2 = 200 * totalSupply * amount * _reserve
    v10 = 0x18ef(amount, _reserve);
    v11 = 0x18ef(amount, v10);
    v12 = 0x18ef(100, v11);// Zähler_1 = 100 * amount**2 * _reserve
    v13 = _SafeDiv(v6, v12 + v9); // Kaufpreis = (Zähler_1 + Zähler_2) / Nenner
    return v13;
}

Aus der dekompilierten Logik lässt sich der Kaufpreis als Funktion im Stil einer Bonding-Kurve ausdrücken:

Wo,

  • amount: zu kaufende TRU
  • reserve (_reserve): die Ether-Reserven des Vertrags
  • totalSupply: die Gesamtzahl der TRU
  • θ (_setParameters): ein Koeffizient, fest auf 75

Diese Kurve soll große Käufe immer teurer machen (konvexes Kostenwachstum), Spekulationen abschrecken und unmittelbare Käufermanipulationen reduzieren.

Verkaufslogik

Die Funktion sellTRU() (und die Funktion getRetirePrice()) verwendet die private Funktion _getRetirePrice(), um die ETH zu berechnen, die beim Einlösen von TRU ausgezahlt wird.

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()); // prüft den Aufrufstatus, leitet Fehlerdaten bei Fehler weiter
    require(RETURNDATASIZE() >= 32);
    require(v1 >= amount, Error('Insufficient TRU allowance'));
    v2 = _getRetirePrice(amount); // Einlösepreis ermitteln
    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()); // prüft den Aufrufstatus, leitet Fehlerdaten bei Fehler weiter
    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()); // prüft den Aufrufstatus, leitet Fehlerdaten bei Fehler weiter
    v7 = msg.sender.call().value(v2).gas(!v2 * 2300);
    require(bool(v7), 0, RETURNDATASIZE()); // prüft den Aufrufstatus, leitet Fehlerdaten bei Fehler weiter
    return v2;
}

function getRetirePrice(uint256 amount) public nonPayable {
    require(msg.data.length - 4 >= 32);
    v0 = _getRetirePrice(amount); // Einlösepreis ermitteln
    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()); // prüft den Aufrufstatus, leitet Fehlerdaten bei Fehler weiter
    require(RETURNDATASIZE() >= 32);
    v1 = v2.length;
    v3 = v2.data;
    v4 = 0x18ef(_reserve, amount);// Zähler = _reserve * amount
    if (v1 > 0) {
        assert(v1);
        return v4 / v1;// Einlösepreis = Zähler / totalSupply
    } else {
    // ...
}

Die Einlösungsregel ist linear:

Der Einlösepreis ist proportional zum Anteil der eingelösten Gesamtmenge (d. h. amount / totalSupply) multipliziert mit dem reserve.

Diese bewusste Asymmetrie schafft eine große Spanne: Kaufen ist konvex (teuer in großem Umfang), während Verkaufen linear ist (nur ein proportionaler Anteil der Reserven wird eingelöst). Unter normalen Bedingungen macht diese Spanne sofortiges Kaufen-Verkaufen-Arbitrage unattraktiv.

0x1 Schwachstellenanalyse

Trotz des beabsichtigten Designs, dass große Käufe teuer sind, enthält _getPurchasePrice() einen Integer-Überlauf in seiner Arithmetik. Da der Vertrag mit Solidity 0.6.10 kompiliert wurde, können arithmetische Operationen auf uint256 stillschweigend überlaufen und Modulo 2^256 berechnen, es sei denn, sie sind explizit geschützt (z. B. durch 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()); // prüft den Aufrufstatus, leitet Fehlerdaten bei Fehler weiter
    require(RETURNDATASIZE() >= 32);
    v2 = 0x18ef(v1, v1)
    v3 = 0x18ef(_setParameters, v2);
    v4 = 0x18ef(v1, v1);
    v5 = 0x18ef(100, v4);
    v6 = _SafeSub(v3, v5);// Nenner = 100 * totalSupply**2 - _setParameters * totalSupply**2
    v7 = 0x18ef(amount, _reserve);
    v8 = 0x18ef(v1, v7);
    v9 = 0x18ef(200, v8);// Zähler_2 = 200 * totalSupply * amount * _reserve
    v10 = 0x18ef(amount, _reserve);
    v11 = 0x18ef(amount, v10);
    v12 = 0x18ef(100, v11);// Zähler_1 = 100 * amount**2 * _reserve
    v13 = _SafeDiv(v6, v12 + v9); // Kaufpreis = (Zähler_1 + Zähler_2) / Nenner
    return v13;
}

In _getPurchasePrice() löst ein ausreichend großer amount einen Überlauf während der Addition zweier großer Zählerterme (v12 + v9 im dekompilierten Snippet) aus. Wenn dieser Überlauf auftritt, wickelt der Zähler zu einem kleinen Wert ab, was dazu führt, dass die abschließende Division einen künstlich niedrigen Kaufpreis ergibt, potenziell null.

Entscheidend ist, dass der Überlauf nur die Kaufpreisbildung betrifft. Die Verkaufsfunktion bleibt linear und verhält sich wie beabsichtigt, sodass ein Angreifer:

  • eine große Menge TRU zu einem unterbewerteten (oder null) Preis kaufen und dann
  • diese über sellTRU() zu einem viel höheren effektiven Kurs gegen ETH einlösen kann.

0x2 Angriffsanalyse

Der Angreifer führte mehrere Runden von Arbitrage innerhalb einer einzigen Transaktion durch [3], wiederholend: getPurchasePrice() -> buyTRU() -> sellTRU()

Erste Runde: Nullkosten-Kauf, dann Verkauf mit Gewinn

Durch die Angabe eines sorgfältig ausgewählten Kaufbetrags (240.442.509,453.545.333.947.284.131) löste der Angreifer einen Überlauf in _getPurchasePrice() aus, reduzierte den berechneten Kaufpreis auf 0 ETH und ermöglichte so den Erwerb von ~240 Millionen TRU kostenlos.

Der folgende Python-Code-Check zeigt, dass der Zähler 2^256 überschreitet und nach dem Abwickeln der berechnete Kaufpreis ein winziger Bruchteil wird, der beim Umwandeln in eine Ganzzahl auf null gekürzt wird.

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

Der Angreifer rief dann sofort sellTRU() auf und löste die TRU für 5.105 ETH aus den Reserven des Protokolls ein.

Nachfolgende Runden: Niedrigkosten-Käufe, dann Verkauf mit Gewinn

Der Angreifer wiederholte den Zyklus mehrmals. Spätere Käufe waren nicht immer streng kostenfrei, aber der Überlauf hielt die Kaufpreise weiterhin weit unter den entsprechenden Verkaufserlösen.

In diesen Runden zog der Angreifer erhebliche Mengen an ETH ab, und unsere Untersuchung legt nahe, dass auch nach der ersten Runde noch weitere Nullkosten-Käufe möglich gewesen wären, obwohl unklar ist, warum der Angreifer sich für einige Runden mit nicht-null Kosten entschied.

Insgesamt hat der Angreifer 8.535 ETH aus den Reserven von Truebit abgezogen.

0x3 Zusammenfassung

Dieses Ereignis wurde letztendlich durch einen ungeprüften Integer-Überlauf in der Kaufpreislogik von Truebit verursacht. Obwohl das asymmetrische Kauf-/Verkaufsmodell des Protokolls darauf ausgelegt war, Spekulationen zu widerstehen, untergrub die Kompilierung mit einer älteren Solidity-Version (vor 0.8) ohne systematische Überlaufschutz das Design und ermöglichte die Entleerung der Reserven.

Für jeden Produktionsvertrag, der noch ältere Solidity-Versionen als 0.8 verwendet, sollten Entwickler:

  • eine überlaufsichere Arithmetik (z. B. SafeMath oder äquivalente Prüfungen) für jede relevante Operation anwenden oder
  • vorzugsweise auf Solidity 0.8+ migrieren, um von den standardmäßigen Überlaufprüfungen zu profitieren.

Referenz

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

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

[3] Angriffstransaktion

Über BlockSec

BlockSec ist ein Full-Stack-Anbieter für Blockchain-Sicherheit und Krypto-Compliance. Wir entwickeln Produkte und Dienstleistungen, die unseren Kunden helfen, Codeaudits (einschließlich Smart Contracts, Blockchain und Wallets) durchzuführen, Angriffe in Echtzeit abzufangen, Vorfälle zu analysieren, illegale Gelder zu verfolgen und AML/CFT-Verpflichtungen über den gesamten Lebenszyklus von Protokollen und Plattformen zu erfüllen.

BlockSec hat mehrere Papiere zur Blockchain-Sicherheit auf prestigeträchtigen Konferenzen veröffentlicht, mehrere Zero-Day-Angriffe auf DeFi-Anwendungen gemeldet, mehrere Hacks blockiert, um mehr als 20 Millionen Dollar zu retten, und Kryptowährungen im Wert von Milliarden gesichert.

Sign up for the latest updates
The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis
Security Insights

The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis

This BlockSec deep-dive analyzes the KelpDAO $290M rsETH cross-chain bridge exploit (April 18, 2026), attributed to the Lazarus Group, tracing a causal chain across three layers: how a single-point DVN dependency enabled the attack, how DeFi composability cascaded the damage through Aave V3 lending markets to freeze WETH liquidity exceeding $6.7B across Ethereum, Arbitrum, Base, Mantle, and Linea, and how the crisis forced decentralized governance to exercise centralized emergency powers. The article examines three parameters that shaped the cascade's severity (LTV, pool depth, and cross-chain deployment count) and provides an exclusive technical breakdown of Arbitrum Security Council's forced state transition, an atomic contract upgrade that moved 30,766 ETH without the holder's signature.

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026

This BlockSec weekly security report covers four attack incidents detected between April 13 and April 19, 2026, across multiple chains such as Ethereum, Unichain, Arbitrum, and NEAR, with total estimated losses of approximately $310M. The highlighted incident is the $290M KelpDAO rsETH bridge exploit, where an attacker poisoned the RPC infrastructure of the sole LayerZero DVN to fabricate a cross-chain message, triggering a cascading WETH freeze across five chains and an Arbitrum Security Council forced state transition that raises questions about the actual trust boundaries of decentralized systems. Other incidents include a $242K MMR proof forgery on Hyperbridge, a $1.5M signed integer abuse on Dango, and an $18.4M circular swap path exploit on Rhea Finance's Burrowland protocol.

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026

This BlockSec weekly security report covers four DeFi attack incidents detected between April 6 and April 12, 2026, across Linea, BNB Chain, Arbitrum, Optimism, Avalanche, and Base, with total estimated losses of approximately $928.6K. Notable incidents include a $517K approval-related exploit where a user mistakenly approved a permissionless SquidMulticall contract enabling arbitrary external calls, a $193K business logic flaw in the HB token's reward-settlement logic that allowed direct AMM reserve manipulation, a $165.6K exploit in Denaria's perpetual DEX caused by a rounding asymmetry compounded with an unsafe cast, and a $53K access control issue in XBITVault caused by an initialization-dependent check that failed open. The report provides detailed vulnerability analysis and attack transaction breakdowns for each incident.

Best Security Auditor for Web3

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

BlockSec Audit