DEV Community

ohmygod
ohmygod

Posted on

The Truebit $26M Heist: How a Silent Integer Overflow in a Bonding Curve Drained an Entire Protocol

TL;DR

On January 8, 2026, Truebit Protocol lost ~$26.2 million (8,535 ETH) to a single-transaction exploit. The attacker found an integer overflow in the buy-side pricing function of Truebit's bonding curve, compiled with Solidity 0.6.10 — a version that doesn't enforce overflow checks. By supplying a carefully crafted purchase amount, the computed price wrapped around to zero, allowing the attacker to mint ~240 million TRU tokens for free and immediately sell them back for ETH at fair market value. The entire attack was a textbook lesson in why legacy contracts without SafeMath are ticking time bombs.


The Protocol: What Truebit Was Supposed to Do

Truebit provides off-chain computation services for Ethereum, using interactive verification to ensure correctness. Its TRU token serves as the economic backbone — staking, payments, and protocol coordination all flow through it.

The token economics relied on a bonding curve model:

  • Buying TRU uses a convex pricing curve (the more you buy, the more expensive each additional unit)
  • Selling TRU uses linear redemption (proportional share of reserves)

This asymmetry was by design: making large buy→sell arbitrage unattractive under normal conditions.

The key word there? Normal.

The Vulnerability: When Math Wraps Around

The _getPurchasePrice() function calculates how much ETH you need to pay for a given amount of TRU. The formula looks like this:

purchasePrice = (100 × amount² × reserve + 200 × totalSupply × amount × reserve) / ((100 - θ) × totalSupply²)
Enter fullscreen mode Exit fullscreen mode

Where:

  • amount = TRU tokens to purchase
  • reserve = contract's ETH reserves
  • totalSupply = current TRU supply
  • θ = coefficient fixed at 75

The numerator involves multiplying several large uint256 values together. For a sufficiently large amount, the intermediate computation 100 × amount² × reserve exceeds 2^256.

In Solidity ≥ 0.8, this would revert. But Truebit was compiled with Solidity 0.6.10. No automatic overflow checks. No SafeMath on this critical operation. The value silently wraps around modulo 2^256.

The Proof

>>> _reserve = 0x1ceec1aef842e54d9ee   # ~8,535 ETH in wei
>>> 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)  # Truncates to integer
0
Enter fullscreen mode Exit fullscreen mode

A purchase price of zero ETH for 240 million TRU tokens. The math literally broke.

The Attack: Systematic Drainage in One Transaction

The attacker executed everything in a single transaction, repeating a simple cycle:

  1. Query getPurchasePrice() with a crafted amount
  2. Buy via buyTRU() — paying 0 ETH (or near-zero in later rounds)
  3. Sell via sellTRU() — receiving proportional ETH from reserves

Round 1: The Free Lunch

  • Input: 240,442,509.45 TRU (carefully chosen to trigger overflow)
  • Purchase price computed: 0 ETH
  • Sold for: 5,105 ETH (~$15.3M at the time)

The sell function worked correctly — it's linear, simply dividing amount × reserve / totalSupply. No overflow, honest math. The attacker got paid the fair proportional value of the reserves they didn't pay for.

Subsequent Rounds

The attacker repeated the cycle multiple times. As reserves decreased and total supply changed, some rounds required small non-zero payments, but the overflow consistently kept purchase prices far below the corresponding sell returns. After several iterations, the contract was empty.

Total extracted: 8,535 ETH (~$26.2M)

The funds were subsequently routed through Tornado Cash.

Why This Was Devastating

1. The Asymmetric Design Backfired

The convex buy / linear sell model was meant to discourage speculation. But when the buy-side breaks (computing zero instead of expensive), the sell-side still works perfectly. The attacker exploited exactly this asymmetry — the buy price was broken, the sell price was honest.

2. Unverified Source Code

The implementation contract's source code was never publicly verified on Etherscan. Security researchers had to work from decompiled bytecode. This means:

  • No public audit trail
  • Community couldn't review the math
  • The overflow went unnoticed for the contract's entire lifetime

3. Legacy Solidity Without SafeMath

The contract used Solidity 0.6.10. Prior to Solidity 0.8 (released in December 2020), arithmetic overflow and underflow were silent — no revert, no error, just wrong numbers. The SafeMath library was the standard mitigation, but Truebit's critical pricing function used raw arithmetic for the numerator addition.

The irony: some parts of the contract did use safe math (_SafeAdd, _SafeSub, _SafeDiv appear in the decompiled code). But the critical numerator computation — where two large intermediate values are added — used a raw + operation.

The Broader Pattern: Legacy Contract Time Bombs

Truebit isn't alone. There's a growing class of "zombie contract" exploits targeting protocols that:

  • Deployed years ago with older Solidity versions
  • Hold significant value (sometimes forgotten value)
  • Have teams that have moved on or dissolved
  • Were never upgraded or migrated

The Hit List of Pre-0.8 Risks

Risk Pre-0.8 Behavior Post-0.8 Behavior
Integer overflow Silent wrap to 0 Automatic revert
Integer underflow Silent wrap to 2^256 Automatic revert
Division by zero Returns 0 Reverts

Every contract deployed before Solidity 0.8 that performs arithmetic on user-controlled inputs without explicit SafeMath is potentially vulnerable.

How to Find Them

  1. Check the compiler version: solc version in contract metadata
  2. Look for raw arithmetic: +, -, * without SafeMath wrappers
  3. Map user-controlled inputs to arithmetic paths: Can a user influence operands in multiplication chains?
  4. Test boundary values: What happens when inputs approach 2^256 / known_multiplier?
# Quick check using cast (Foundry)
cast call <CONTRACT> "getPurchasePrice(uint256)" \
  $(python3 -c "print(2**128)")
# If this returns 0 or suspiciously small → investigate
Enter fullscreen mode Exit fullscreen mode

Defense Patterns

For Existing Pre-0.8 Contracts

If you maintain a legacy contract that can't be easily upgraded:

  1. Audit every arithmetic operation against SafeMath usage
  2. Add input validation: Cap maximum amounts in user-facing functions
  3. Deploy a guardian contract: A wrapper that validates inputs before forwarding calls
  4. Consider migration: Move liquidity to a new, properly compiled contract

For New Development

  1. Always use Solidity ≥ 0.8 — automatic overflow checks are non-negotiable
  2. Even with 0.8+, use unchecked blocks sparingly and only with explicit justification
  3. Test arithmetic boundaries in your invariant tests:
// Foundry invariant test
function invariant_purchasePriceNeverZero() public {
    uint256 amount = handler.lastPurchaseAmount();
    if (amount > 0) {
        uint256 price = protocol.getPurchasePrice(amount);
        assertGt(price, 0, "Purchase price should never be zero");
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Verify and publish source code — always. Unverified contracts are untrusted contracts.

The $26M Checklist: Lessons Learned

  • Compiler version matters. Solidity < 0.8 is a red flag for any contract holding value.
  • SafeMath gaps are exploitable. Partial SafeMath usage is almost worse than none — it creates false confidence.
  • Asymmetric economic models amplify bugs. When buy and sell use different formulas, a bug in one side creates arbitrage.
  • Bonding curves need overflow analysis. Polynomial pricing functions multiply user inputs multiple times — overflow risk grows exponentially.
  • Unverified source = unauditable. If the community can't read your code, nobody's watching for bugs.
  • Legacy contracts need monitoring. If a contract holds value and the team has moved on, someone should still be watching.

Timeline

Date Event
Pre-2021 Truebit deploys Purchase contract (Solidity 0.6.10)
Jan 8, 2026 Attacker drains 8,535 ETH in a single transaction
Jan 8, 2026 CertiK, BlockSec flag the exploit
Jan 8, 2026 Truebit acknowledges the incident
Jan 14, 2026 BlockSec publishes detailed technical analysis
Post-exploit Stolen funds routed through Tornado Cash; unrecovered

Final Thought

The Truebit exploit wasn't sophisticated. It was arithmetic. A single missing SafeMath wrapper on a + operation in a years-old contract turned a theoretical overflow into a $26 million withdrawal. The attacker didn't need a flash loan, didn't need to manipulate an oracle, didn't need to exploit cross-contract composability. They just needed a calculator and the patience to find the right input value.

Every Solidity < 0.8 contract with user-controlled inputs in multiplication chains is a potential Truebit. The question isn't whether more will be found. It's how much they're holding when someone looks.


References: BlockSec Technical Analysis, Truebit Official Statement, QuillAudits Analysis, SlowMist Analysis

Top comments (0)