
A compiler caching bug silently swapped opcodes in production for 18 months. Tests passed. Audits passed. Formal verification passed. Nobody noticed.
~8 min read
There’s a class of software bug that sits below every tool designed to find bugs. It lives in the compiler, the program that translates your source code into the instructions your machine actually executes. When the compiler has a bug, your source code can be perfect, your tests can pass, your code review can be thorough, and your deployed binary can still be wrong.
This isn’t hypothetical. In February 2026, a team called Hexens, a blockchain security firm that specializes in compiler-level research, found exactly this kind of bug in the Solidity compiler. Solidity is the dominant programming language for Ethereum smart contracts, programs that manage billions of dollars in financial transactions on a public blockchain. The bug, which Hexens named TSTORE Poison, caused the compiler to silently swap storage instructions in the compiled output. When a developer wrote code to clear a temporary variable, the compiler could instead clear a permanent one, wiping out contract ownership, access control flags, or token approval records. When a developer wrote code to clear a permanent variable, the compiler could instead perform a no-op, leaving token approvals active indefinitely.
The bug had been shipping in production compiler releases for eighteen months across six versions. It produced wrong bytecode silently. No warnings. No errors. No test failures. No audit findings.
The Solidity team patched it within a week. The full technical analysis is on the Hexens research page, and the Solidity team’s official acknowledgment is here. Hexens discovered the first high-severity Solidity compiler bug since 2016.
I’m writing this for developers who don’t work in blockchain. The bug is interesting on its own, but the underlying pattern (a memoization cache that doesn’t capture all dimensions of variation) is universal. If you’ve ever written a cache key, this story is relevant to you.
The Setup: Memorization in a Code Generator
Most compilers have an intermediate representation (IR) step. Source code gets translated into an IR, optimizations happen on the IR, and then the IR gets lowered to machine code (or in Solidity’s case, EVM bytecode).
During the source-to-IR step, the Solidity compiler generates small helper functions for repetitive operations: zeroing a storage slot, encoding a value, copying an array. To avoid generating duplicate code, these helpers are cached by name. If the compiler needs a helper called storage_set_to_zero_t_address, it checks the cache. If it exists, return the cached version. If not, generate it, store it, return the name.
If you’ve ever written something like this, you’ve used the same pattern:
python
_cache = {}
def get_or_create(name, generator):
if name not in _cache:
_cache[name] = generator()
return _cache[name]It’s correct as long as name uniquely identifies the output of generator(). Two calls with different expected outputs must produce different names. If they don’t, you get a cache collision: the first call’s output gets reused for the second call, silently.
This is the pattern that broke.
What Happened in Solidity
In 2024, Ethereum added a new storage domain called “transient storage.” Think of it as temporary storage that gets wiped at the end of every transaction, useful for flags, locks, and intermediate calculations that don’t need to persist. The Solidity compiler added native support for it in version 0.8.28: developers could declare transient variables and use standard operations like delete on them.
Under the hood, persistent storage and transient storage use different CPU instructions (or “opcodes” in Ethereum terminology): sstore writes to persistent storage, tstore writes to transient storage. When the compiler generates a helper function that zeroes a storage slot, it needs to emit the right opcode for the right storage domain.
The function that generates this helper accepted two parameters: the variable’s type and the storage location (persistent or transient). It used both parameters to generate the correct code. But the cache key, the function name, only included the type:
name = "storage_set_to_zero_" + type
// location is NOT in the name
// but location IS used in the generated code
So storage_set_to_zero_t_address could mean “zero a persistent address slot using sstore” or “zero a transient address slot using tstore,” depending on which one was generated first. The second call would always get the first call’s version.
A different function in the same codebase, doing essentially the same job for a different operation, already included the storage location in its cache key:
name = ("transient_" if location == transient else "") +
"update_storage_value_" + typeThe correct pattern was right there. It just wasn’t applied everywhere.
Why Tests Didn’t Catch It
This is the part that’s relevant to every developer, regardless of whether you’ve ever touched blockchain code.
Unit tests verify behavior at the source level. You write a test that calls a function and checks the output. But the bug isn’t in the function’s logic. The source code is correct. The bug is in the compiler’s translation of that source code into executable instructions. Your test runs the compiled bytecode and checks the result, but the result might look plausible even with the wrong opcode. A storage slot getting zeroed when it shouldn’t might not cause an immediate failure. It might cause a subtle state corruption that manifests transactions later, or only under specific conditions.
Integration tests have the same blind spot. They exercise the compiled code, not the compilation process. If the compiler silently swaps one storage instruction for another, the test framework has no mechanism to detect it.
Code review operates on source code. No reviewer, no matter how experienced, can catch a bug that only exists in the compiled output. The Solidity source looks correct, because it is correct. The bug is in the transformation.
Static analysis tools parse source code or analyze compiled bytecode against known patterns. This bug doesn’t match any known vulnerability pattern. The bytecode is structurally valid. The only difference is a single opcode value at one point in execution: 0x55 (sstore) versus 0x5d (tstore). Same slot, same value, wrong storage domain.
Formal verification (tools like Certora and Halmos in the blockchain world) models the program’s behavior at the source level and mathematically proves properties about it. But it assumes the compiler correctly translates source-level operations. The formal model says “delete on a transient variable produces a transient store of zero.” If the compiler emits a persistent store instead, the model doesn’t know. The compiler is outside the verification boundary.
This is a fundamental limitation, not a tool-specific one. Formal verification proves properties of a model. If the model assumes compiler correctness and the compiler is incorrect, the proof is valid for the model and invalid for the executable. This is true for Solidity, and it’s true for C programs verified against CompCert, and it’s true for any system where the verification happens above the compilation boundary.
The Cache Key Problem Is Universal
Strip away the blockchain context, and the bug is a cache key that doesn’t capture all dimensions of variation. This pattern shows up everywhere:
Web applications: A template rendering cache keyed by template name but not by locale. English gets cached first, every subsequent language renders in English.
Build systems: A compilation cache keyed by source file hash but not by compiler flags. A debug build gets cached, release builds get debug symbols.
API responses: A response cache keyed by endpoint but not by user role. An admin response gets cached, regular users see admin data.
ORM query caches: A query cache keyed by the query string but not by the database connection. A query against the read replica gets cached, writes go to the read replica.
The Solidity case is more consequential (wrong opcodes in financial infrastructure versus wrong locale in a web app) but the structural pattern is identical. Every memoization system has the same invariant: the cache key must encode every parameter that affects the cached value. When a new parameter is added to an existing system (like “storage location” was added to the Solidity code generator), every cache key that depends on the new dimension must be updated. If one is missed, you get a silent collision.
The insidious part is that these bugs don’t fail loudly. The cache returns a value. It’s just not the right value. The system continues operating with plausible-looking but incorrect behavior. In a web app, someone eventually notices the wrong language. In a compiler for financial infrastructure, the wrong opcode might sit in production for 18 months.
The Blast Radius of a Compiler Bug
One thing that makes this case study particularly interesting from a software engineering perspective is the blast radius.
When you find a bug in an application, you know the scope: it’s that application. When you find a bug in a library, the scope expands to every application that imports it. When you find a bug in a compiler, the scope is every program compiled with the affected version, and the developers of those programs have no way to know they’re affected without recompiling and diffing the output.
Hexens used Glider by Hexens, their smart contract analysis engine, to scan over 20 million deployed smart contracts across Ethereum and other compatible blockchains. Of roughly 500,000 compiled with affected compiler versions, four were identified as potentially vulnerable. All were notified privately before any public disclosure.
The scanning problem is unique to blockchain: every deployed smart contract is public and immutable on-chain, which means it’s possible (though computationally expensive) to check every single one. In traditional software, a compiler bug at this severity level would be a CVE and a hope that package managers propagate the fix. In blockchain, you can actually enumerate every affected deployment, if you have the tooling to do it.
What the Solidity Team Did Right
I want to highlight the response because it’s a model for how compiler bugs in critical infrastructure should be handled.
Hexens reported the bug on February 11, 2026. The Solidity team confirmed it. A coordinated disclosure process involved a SEAL 911 warroom, a cross-team emergency response group in the Ethereum ecosystem, that brought together the Solidity team, Hexens, Dedaub (another security firm with contract search infrastructure), and other researchers. Every identified affected project was privately notified with specific remediation steps. The patch shipped on February 18, seven days later, as solc v0.8.34.
No funds were lost. No public exploit. No drama. Just professionals coordinating a response to an infrastructure-level vulnerability.
The Takeaway
If you write caching code (and most of us do, whether it’s template caching, query caching, build caching, or memoization) this bug is a useful reminder.
Every time you add a new parameter to a system that uses memoized results, ask yourself: does the cache key need to change? Is there a codepath where the new parameter affects the output but doesn’t affect the key? Is there an existing function that was written before this parameter existed and hasn’t been updated?
In the Solidity compiler, a function written for a single-storage-domain world was extended with a storage location parameter. The function body was updated. The cache key wasn’t. The result was a silent miscompilation that shipped for 18 months in the compiler for a multi-billion-dollar financial ecosystem.
The fix was adding one string prefix to one cache key. The lesson is bigger than the fix.
The full technical analysis of TSTORE Poison, the Solidity compiler bug described in this article, is available at https://hexens.io/research/solidity-compiler-bug-tstore-poison
Solidity team official disclosure: https://soliditylang.org/blog/2026/02/18/transient-storage-clearing-helper-collision-bug/
Discovered and reported by Hexens — https://hexens.io
Solidity First High-Severity Compiler Bug in 10 Years was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.