Truebit Purchase Bonding-Curve Overflow / Zero-Cost Mint ETH Drain (2026-01-08)

  • Name: Truebit Purchase bonding-curve overflow / zero-cost mint ETH drain
  • Date: 2026-01-08
  • Protocol: Truebit Purchase contract / TRU token buyback path
  • Chain: Ethereum
  • Category: unchecked arithmetic / bonding-curve pricing failure / legacy-contract exploit / reserve drain
  • Loss: about 8,535 ETH, roughly $26.2M-$26.6M at the time
  • Summary: Truebit’s long-lived Ethereum Purchase contract let users buy TRU against an ETH reserve and redeem TRU back into that reserve. Public reverse-engineering showed the buy-side pricing path still relied on legacy unchecked arithmetic in a decompiled _getPurchasePrice() routine. By asking to buy an absurdly large amount of TRU, the attacker made the final addition of two huge numerator terms in the purchase-cost calculation overflow and wrap to a tiny value, so getPurchasePrice() and buyTRU() accepted near-zero or zero ETH for enormous mints. The economically decisive failure sat in that private pricing helper itself: higher-level mint logic treated _getPurchasePrice() as the canonical proof that a giant TRU purchase still deserved reserve-backed issuance. The sell-side path did not share that failure: it still redeemed linearly against the real ETH reserve. The attacker therefore inverted Truebit’s intended spread, minting huge TRU balances almost for free and immediately burning them back into real ETH until the reserve was drained.
  • Trigger / entry point:
    • On 2026-01-08, the attacker targeted Truebit’s Ethereum Purchase contract at 0x764c64b2a09b09acb100b80d8c505aa6a0302ef2.
    • The relevant implementation source was not publicly verified on Etherscan, so early analysis relied on decompilation and trace review rather than a published repo or audit trail.
    • Public reverse-engineering by BlockSec, Cantina, and others converged on the same core issue: the buy-side price quote from getPurchasePrice() / buyTRU() could be made to underflow economically because a large arithmetic term wrapped under legacy unchecked math.
    • The attacker deployed exploit contract 0x1de399967b206e446b4e9aeeb3cb0a0991bf11b8 and executed the drain in one main transaction.
  • Root cause:
    • The decisive failure was not a flash-loan trick or oracle manipulation. It was unchecked arithmetic inside the protocol’s own solvency-critical pricing function.
    • Decompilation of the buy path showed that most multiplications were routed through helper math, but the final addition combining the two large numerator terms in _getPurchasePrice() remained unchecked.
    • Under pre-0.8-style arithmetic semantics, sufficiently large requested purchase amounts wrapped that term modulo 2^256, collapsing the quoted cost for huge buys to zero or near zero.
    • That made _getPurchasePrice() a lying reserve-boundary helper: buyTRU() still treated the returned quote as sufficient proof that the requested issuance was paid for, even though the arithmetic had stopped certifying any economically real cost.
    • The sell path remained economically live, so Truebit’s reserve logic stopped enforcing the intended invariant mint cost should always exceed or at least track the ETH redeemable from the newly minted TRU.
    • The protocol therefore advertised fake solvency on the buy side while honoring real reserve withdrawals on the sell side.
  • Exploit path:
    1. The attacker called getPurchasePrice() with an attacker-chosen massive TRU amount until the purchase-cost calculation wrapped and returned a trivial quote.
    2. They called buyTRU() with that same amount and supplied little or no ETH, minting an enormous block of new TRU.
    3. They approved the Purchase contract to spend the freshly minted TRU.
    4. They immediately called sellTRU(), which still used the live reserve-based redemption logic and paid out real ETH.
    5. They repeated the buy-then-sell cycle several times inside one transaction, each time extracting more ETH from the reserve than they had paid into the purchase path.
    6. By the end of the main exploit transaction, roughly 8,535 ETH had left the contract and only a small residue remained.
  • What the attacker needed to be true:
    • The buy-side pricing contract had to still rely on legacy unchecked arithmetic for at least one large intermediate term.
    • The sell-side redemption path had to remain usable and funded by a real ETH reserve.
    • The contract needed no meaningful per-transaction cap, mint throttle, or reserve-drain circuit breaker that would stop repeated same-transaction buy/sell cycling.
    • The pricing bug had to be reachable with attacker-controlled input size alone; no privileged role or offchain dependency was required.
    • The old contract had to remain live and sufficiently funded despite years of ecosystem drift and absent public source verification.
  • Technical anatomy worth remembering:
    • BlockSec’s decompilation showed buyTRU() and getPurchasePrice() both depended on a private pricing helper that computed the ETH cost for a requested TRU amount.
    • In BlockSec’s decompiled notation, the dangerous step was the unchecked v12 + v9 addition, where v12 captured 100 * amount**2 * reserve and v9 captured 200 * totalSupply * amount * reserve. For extreme amount values, that sum wrapped under uint256 arithmetic and the final integer division returned 0 or another economically nonsensical low quote.
    • Cantina’s reconstruction is useful here because it makes the trust boundary obvious: the protocol carefully routed multiplication and subtraction through helper math, but still let one raw v12 + v9 addition decide whether a giant mint was paid for.
    • BlockSec highlighted a first attacker-selected amount around 240,442,509.453545333947284131 TRU that priced to 0 ETH after wraparound and truncation, then redeemed for about 5,105 ETH on the first sell leg alone.
    • Because sellTRU() redeemed against actual reserve balance rather than the broken purchase quote, the attacker could mint huge TRU supply almost for free and then turn it back into real ETH.
    • Public analysis described roughly five mint-and-burn rounds in the main transaction, with the first round alone cashing out about 5,105 ETH.
    • This was not just old Solidity had overflow risk; the economically important lesson is that pricing helpers are reserve-boundary proof objects once mint logic starts treating their output as sufficient payment truth, and asymmetric buy/sell curves become reserve-drain primitives when only one side of the spread keeps honoring reality.
  • Representative transactions and addresses:
    • Main victim contract: 0x764c64b2a09b09acb100b80d8c505aa6a0302ef2
    • Main exploit transaction: 0xcd4755645595094a8ab984d0db7e3b4aabde72a5c87c4f176a030629c47fb014
    • Exploit contract: 0x1de399967b206e446b4e9aeeb3cb0a0991bf11b8
    • Attacker EOA highlighted in public traces: 0x6c8ec8f14be7c01672d31cfa5f2cefeab2562b50
    • BlockSec explorer trace for the main transaction: https://app.blocksec.com/explorer/tx/eth/0xcd4755645595094a8ab984d0db7e3b4aabde72a5c87c4f176a030629c47fb014
  • Response / mitigation:
    • Truebit publicly acknowledged the incident and warned users not to interact with the affected contract.
    • The exploit nevertheless completed before intervention because the reserve drain happened inside one transaction.
    • Public postmortems focused on the latent-risk lesson: old, lightly maintained, or unverified contracts remain live attack surfaces even after years without incident.
    • The incident also renewed scrutiny on legacy Ethereum code that predates modern overflow protections, continuous review, and the ecosystem’s current testing expectations.

Pattern tags

  • issuance-redemption-solvency-asymmetry
  • buyTRU() treated a wrapped bonding-curve quote as sufficient proof of paid-for issuance even after the final large-term addition in _getPurchasePrice() had stopped representing any economically real cost
  • The exploit was a clean broken buy side, still-honest sell side round trip: enormous TRU balances could be minted under arithmetic fiction and immediately burned back into the live ETH reserve under ordinary redemption logic
  • The reserve drain depended on legacy-contract liveness as much as on bad math: a long-lived, source-unverified contract with meaningful ETH inventory still exposed a public issue-and-redeem loop with no meaningful outflow brakes

Why the exploit worked

  • The decisive lie lived inside the protocol’s own buy-side proof object. Public reverse-engineering by BlockSec and Cantina converged on the same point: the attacker did not need an oracle, flash-loan distortion, or cross-protocol side effect. They only needed _getPurchasePrice() to stop certifying what a giant TRU mint should actually cost in ETH.
  • The technical failure was especially sharp because it happened late in the computation, not at the obvious formula exists layer. Most of the decompiled math still routed through helper operations, but the final composed addition of two huge numerator terms (v12 + v9 in BlockSec’s notation) remained unchecked under legacy uint256 semantics. For sufficiently large amount, that sum wrapped modulo 2^256, so the final integer division returned 0 or another absurdly small quote.
  • Once that quote existed, buyTRU() still treated it as canonical payment truth. Truebit therefore broke the issuance side of the balance sheet before the attacker ever touched the redemption side. The protocol itself had published the false statement this mint is paid for.
  • The sell path then monetized the fiction cleanly because it still honored real reserve reality. sellTRU() redeemed against actual ETH inventory rather than the broken issuance quote, so one same-state round trip turned arithmetic overflow directly into reserve withdrawal.
  • The age of the contract also mattered. This was not a brand-new deployment with obvious monitoring or kill-switch assumptions; it was a long-lived reserve-holding legacy surface. The exploit worked because unchecked math, public reachability, and meaningful dormant reserves all survived together.

Detection and audit pivots

  • Treat every private quote helper on a reserve-backed issuance path as solvency code, not as convenience math. If buy, mint, or add liquidity can proceed whenever that helper returns a number, the helper is already part of the custody boundary.
  • Inspect the final composed arithmetic, not only the sub-helpers. Truebit is a strong reminder that a curve can look well-structured until the last unchecked addition, subtraction, or scaling step collapses the economic meaning of the whole quote.
  • Differential-test issue -> redeem immediately across extreme input ranges on the same state snapshot. The reviewer question is not does the buy quote look monotonic? but can the resulting claim be sold back for more reserve value than just entered?
  • Treat dormant, legacy, or source-unverified reserve contracts as live attack surface until they are emptied or deauthorized. No one uses it anymore is not a control if the contract still holds reserves and exposes public issue / redeem verbs.
  • Add reserve-outflow and issuance caps sized for arithmetic failure, not just ordinary usage. Once the buy-side quote is wrong, the next useful question is how much ETH or other reserve inventory can still leave before operators can react.

Failure-prevention checklist

  • Keep checked arithmetic enabled across the entire reserve-critical quote path, including the final composed addition, subtraction, scaling, and division steps rather than only the obvious helper multiplications.
  • Bound attacker-supplied purchase amounts before they enter large-term curve math, and reject inputs whose intermediate terms would exceed the formula’s reviewed domain.
  • Prove a same-state round-trip invariant for the live contract: a buyTRU(amount) -> sellTRU(amount) cycle should never return more ETH than just entered once explicit fees and tolerated rounding are applied.
  • Treat quote helpers as custody code. If getPurchasePrice() is sufficient proof for minting, review it with the same threat model as reserve-withdrawal logic rather than as informational math.
  • Add reserve-outflow brakes sized for pricing helper is wrong right now: per-transaction issuance caps, reserve-drain ceilings, and scoped pause rails should all fire before one misquote can empty the treasury.
  • Decommission legacy reserve surfaces explicitly. Empty reserves, revoke issuance authority, disable public buy / sell entrypoints, and do not rely on low usage, absent front-end links, or source-verification gaps as substitutes for deauthorization.
  • Replay extreme-input tests against old contracts that still custody funds. Long-lived reserve rails need modern fuzzing and invariant checks even if they predate current tooling or active product ownership.

Audit prompts

  • Which exact arithmetic term is the last economically decisive composition step in the buy-side quote, and does it still run under checked semantics at maximum reachable input sizes?
  • Under one state snapshot, can any reachable buy -> sell round trip ever extract more ETH than entered without outside market movement?
  • If the contract quote became wrong by 10x or 10,000x for one block, what independent cap, pause, or reserve-release ceiling would stop treasury extinction?
  • Are any reserve-holding legacy contracts still callable through public selectors, forgotten routers, old UIs, or direct contract interaction even though the product is no longer actively maintained?
  • Which code path proves the buy-side quote is still a trustworthy payment certificate rather than merely a mathematical estimate, and what keeps that proof aligned with the sell-side reserve logic?
  • Have reviewers fuzzed the final numerator / denominator assembly directly, not just the helper math feeding it, across max-size purchase requests and near-overflow states?
  • If a contract is source-unverified or operationally dormant, who still owns the obligation to run modern invariant tests, reserve monitoring, and decommissioning work against it?

Lessons that should already be standard

  • A reserve-backed quote helper is already custody-critical once mint logic treats its output as sufficient payment truth.
  • The final arithmetic composition step is often the real solvency boundary. Safe-looking helper math does not matter if the last unchecked addition can still turn a giant purchase into a free mint.
  • Same-state round-trip testing catches failures that local formula review misses. Buy looks plausible and sell looks plausible are not enough when the protocol exposes both legs against one reserve.
  • Dormant reserve contracts are still production systems until reserves and authority are actually gone. Age, neglect, and missing source verification do not reduce exploitability.
  • Spread design is not a safety proof. Once the buy side prints fiction and the sell side keeps honoring reality, the protocol’s own curve becomes the withdrawal rail.

Why this incident matters

  • It preserves a clean buy-side arithmetic fiction, sell-side reserve reality exploit path rather than a more familiar oracle, bridge, or reentrancy story.
  • It shows how economic asymmetry itself becomes the cash-out rail once only one side of a protocol’s pricing logic breaks.
  • It is also a strong reminder that dormant or aging contracts are not merely operational debt; if they still hold reserves and expose public pricing or redemption logic, they remain live solvency surfaces.
  • The incident is especially useful as a contrast case to AMM and lending arithmetic bugs: the attacker did not need to distort a market or exploit a cross-protocol assumption, only to make one protocol’s own pricing helper stop proving reserve reality.

Internal linkages

Control surface