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
~$15.9M Lost: Trusted Volumes & More | BlockSec Weekly
Security Insights

~$15.9M Lost: Trusted Volumes & More | BlockSec Weekly

This BlockSec bi-weekly security report covers 11 notable attack incidents identified between April 27 and May 10, 2026, across Sui, Ethereum, BNB Chain, Base, Blast, and Berachain, with total estimated losses of approximately $15.9M. Three incidents are analyzed in detail: the highlighted $1.14M Aftermath Finance exploit on Sui, where a signed/unsigned semantic mismatch in the builder-fee validation allowed an attacker to inject a negative fee that was converted into positive collateral during settlement; the $5.87M Trusted Volumes RFQ authorization mismatch on Ethereum; and the $5.7M Wasabi Protocol infrastructure-to-contract-control compromise across multiple EVM chains.

Newsletter - April 2026
Security Insights

Newsletter - April 2026

In April 2026, the DeFi ecosystem experienced three major security incidents. KelpDAO lost ~$290M due to an insecure 1-of-1 DVN bridge configuration exploited via RPC infrastructure compromise, Drift Protocol suffered ~$285M from a multisig governance takeover leveraging Solana's durable nonce mechanism, and Rhea Finance incurred ~$18.4M following a business logic flaw in its margin-trading module that allowed circular swap path manipulatio

~$7.04M Lost: GiddyDefi, Volo Vault & More | BlockSec Weekly
Security Insights

~$7.04M Lost: GiddyDefi, Volo Vault & More | BlockSec Weekly

This BlockSec weekly security report covers eight attack incidents detected between April 20 and April 26, 2026, across Ethereum, Avalanche, Sui, Base, HyperLiquid, and MegaETH, with total estimated losses of approximately $7.04M. The highlighted incident is the $1.3M GiddyDefi exploit, where the attacker did not break any cryptography or use a flash loan but simply replayed an existing on-chain EIP-712 signature with the unsigned `aggregator` and `fromToken` fields swapped out for a malicious contract, demonstrating how partial signature coverage turns any historical signature into a generic permit. Other incidents include a $3.5M Volo Vault operator key compromise on Sui, a $1.5M Purrlend privileged-role takeover, a $413K SingularityFinance oracle misconfiguration, a $142.7K Scallop cross-pool index injection, a $72.35K Kipseli Router decimal mismatch, a $50.7K REVLoans (Juicebox) accounting pollution, and a $64K Custom Rebalancer arbitrary-call exploit.

Best Security Auditor for Web3

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

BlockSec Audit