Forem

ohmygod
ohmygod

Posted on

The Truebit Integer Overflow: How a Forgotten SafeMath Gap Turned a 2021 Contract Into a $26M ATM

On January 8, 2026, a single transaction drained 8,535 ETH (~$26 million) from the Truebit Purchase contract. The vulnerability? An integer overflow in a function that calculated token prices — deployed in 2021, never patched, hiding in plain sight for five years.

This wasn't a flash loan attack. It wasn't an oracle manipulation. It was first-semester computer science: what happens when you multiply two big numbers in a language that doesn't check for overflow.

The Setup: A Bonding Curve With a Fatal Flaw

Truebit's Purchase contract used a bonding curve to price TRU tokens. As supply increased, the ETH cost per token rose. The critical function was getPurchasePrice(uint256 amount):

// Simplified — Solidity 0.5.x (NO automatic overflow checks)
function getPurchasePrice(uint256 amount) public view returns (uint256) {
    uint256 currentSupply = totalMinted;
    uint256 newSupply = currentSupply + amount;
    uint256 price = _calculateArea(currentSupply, newSupply);
    return price;
}
Enter fullscreen mode Exit fullscreen mode

The contract was written in Solidity 0.5.x. Before version 0.8.0, Solidity did not automatically revert on arithmetic overflow. Numbers simply wrapped around: type(uint256).max + 1 = 0.

SafeMath was used elsewhere in the codebase. But this function — the one that determined how much ETH you needed to pay — was unprotected.

The Attack: Mint for Free, Burn for ETH

The attacker's strategy was elegant in its simplicity:

Step 1: Overflow the price to zero. Pass an astronomically large amount to getPurchasePrice(). The internal multiplication overflows, wrapping around to produce a price of 0 ETH.

getPurchasePrice(2^255 + carefully_chosen_offset) → 0 ETH
Enter fullscreen mode Exit fullscreen mode

Step 2: Mint massive TRU supply. With price = 0, call the mint function. Receive an enormous quantity of TRU tokens for free.

Step 3: Burn for real ETH. Call sellTRU() to burn the minted tokens back through the bonding curve. The sell function calculates the ETH to return based on the current curve — which, with the now-inflated supply, pays out real ETH from the contract's reserves.

Step 4: Repeat. Execute multiple mint-and-burn cycles within a single atomic transaction. Total extraction: 8,535 ETH.

┌─────────────────────────────────────────┐
│           SINGLE TRANSACTION            │
├─────────────────────────────────────────┤
│  1. getPurchasePrice(huge) → 0 ETH      │
│  2. mint(huge tokens) — cost: 0         │
│  3. sellTRU(tokens) → receive ETH       │
│  4. Repeat steps 1-3                    │
│  5. Total drained: 8,535 ETH (~$26M)   │
└─────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Why It Went Undetected for 5 Years

Three compounding failures:

1. Unverified Source Code

The Purchase contract's source wasn't verified on Etherscan. Anyone could interact with it, but reviewing the actual Solidity required decompiling bytecode. This reduced the surface area for community audits to nearly zero.

2. No Supply Caps or Input Validation

The mint function accepted any uint256 amount without bounds checking:

// What should have existed:
require(amount <= MAX_MINT_PER_TX, "Exceeds single-tx limit");
require(totalMinted + amount <= SUPPLY_CAP, "Exceeds supply cap");

// What actually existed:
// (nothing)
Enter fullscreen mode Exit fullscreen mode

3. Partial SafeMath Adoption

This is the most insidious pattern. SafeMath was present in the codebase — giving the false impression of overflow protection. But coverage was inconsistent. The price calculation function, arguably the most critical arithmetic in the entire contract, was the one left unprotected.

The Legacy Contract Problem

Truebit isn't unique. There are thousands of pre-0.8 contracts on Ethereum mainnet still holding significant value:

Solidity Version Overflow Behavior SafeMath Required?
< 0.8.0 Silent wraparound Yes (manual)
≥ 0.8.0 Automatic revert No (built-in)
≥ 0.8.0 with unchecked Silent wraparound Yes (intentional opt-out)

The danger zone isn't just old contracts — it's contracts that were partially upgraded. A codebase that imports SafeMath for some operations but not others is harder to audit than one that uses neither, because the presence of SafeMath creates a false sense of security.

Scanning for Vulnerable Contracts

A basic heuristic for identifying at-risk legacy contracts:

from web3 import Web3

def check_legacy_risk(w3: Web3, address: str) -> dict:
    """Flag contracts that are pre-0.8 and hold significant value."""
    balance = w3.eth.get_balance(address)
    code = w3.eth.get_code(address)

    has_code = len(code) > 10

    # Rough heuristic: check if contract uses SafeMath
    safemath_sig = bytes.fromhex("771602f7")
    uses_safemath = safemath_sig in code

    # Check for checked arithmetic (0.8+ uses revert on overflow natively)
    has_panic_check = bx11 in code and bx4ex48x7bx71 in code

    return {
        "address": address,
        "balance_eth": w3.from_wei(balance, ether),
        "likely_pre_08": not has_panic_check,
        "has_safemath_references": uses_safemath,
        "risk": "HIGH" if (not has_panic_check and balance > w3.to_wei(100, ether)) else "LOW"
    }
Enter fullscreen mode Exit fullscreen mode

Defense Playbook

For Protocol Teams With Legacy Contracts

1. Migrate or proxy. If your pre-0.8 contract holds value and can be upgraded (proxy pattern, migration function), do it now. The Truebit contract sat for 5 years. Don't be year six.

2. Add circuit breakers. Even without a full migration, you can deploy a guardian contract that monitors for anomalous minting/withdrawal patterns and pauses operations:

contract TruebitGuardian {
    uint256 public constant MAX_MINT_PER_BLOCK = 1_000_000e18;
    mapping(uint256 => uint256) public mintedPerBlock;

    modifier guardedMint(uint256 amount) {
        mintedPerBlock[block.number] += amount;
        require(
            mintedPerBlock[block.number] <= MAX_MINT_PER_BLOCK,
            "Anomalous mint volume"
        );
        _;
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Verify your source code. Unverified contracts are invisible to security researchers. Verifying on Etherscan costs nothing and dramatically increases the chance someone spots a bug before an attacker does.

For Auditors

4. Grep for partial SafeMath. The most dangerous pattern is inconsistent protection. When auditing a pre-0.8 codebase, search for every arithmetic operation and verify SafeMath coverage is complete, not just present:

# Find all arithmetic in pre-0.8 Solidity files
grep -rn [-*/] contracts/ | grep -v SafeMath | grep -v // | grep -v pragma
Enter fullscreen mode Exit fullscreen mode

5. Test with extreme inputs. Overflow attacks use values near type(uint256).max. Your test suite should include:

function testOverflowProtection() public {
    uint256 maxVal = type(uint256).max;
    vm.expectRevert();
    purchaseContract.getPurchasePrice(maxVal);

    vm.expectRevert();
    purchaseContract.getPurchasePrice(maxVal / 2);
}
Enter fullscreen mode Exit fullscreen mode

For the Ecosystem

6. Solidity ≥ 0.8.0 isn't a silver bullet. The unchecked block reintroduces overflow risk. Protocols using unchecked for gas optimization in critical arithmetic (price calculations, exchange rates, shares-per-token) are recreating the exact same vulnerability class:

// This is the Truebit bug with extra steps
unchecked {
    uint256 price = basePrice * amount * multiplier; // Can overflow!
}
Enter fullscreen mode Exit fullscreen mode

Timeline

Date Event
2021 Truebit Purchase contract deployed (Solidity 0.5.x)
2021-2025 Contract holds ETH, source unverified, no security review
Jan 8, 2026 Attacker drains 8,535 ETH in single transaction
Jan 8, 2026 TRU price collapses from $0.16 to ~$0
Jan 8, 2026 Funds routed through Tornado Cash
Post-exploit Truebit coordinates with law enforcement; no recovery plan announced

Key Takeaways

  1. Legacy contracts are ticking time bombs. The DeFi ecosystem has $billions locked in pre-0.8 contracts with no overflow protection. The Truebit exploit is a template for attacking any of them.

  2. Partial SafeMath is worse than no SafeMath. It creates false confidence. If you're auditing a pre-0.8 codebase, assume nothing is protected until you verify every operation.

  3. Input validation is non-negotiable. No function that moves value should accept arbitrary uint256 inputs without bounds checking. This is true regardless of Solidity version.

  4. Verify your source code. An unverified contract is an unlocked door. Five years of obscurity is not security.

  5. unchecked blocks are the new pre-0.8. Every unchecked arithmetic operation on a value path is a potential Truebit replay. Treat them with the same scrutiny.


This analysis is part of the DeFi Security Deep Dives series. The Truebit exploit demonstrates that the simplest vulnerability classes — integer overflow, missing input validation — remain the most devastating when they hide in production contracts long enough.

Top comments (0)