
Yul Optimizer Knowledge Base Stale Bookkeeping Bug
Solidity advisory on a Yul optimizer bug in the KnowledgeBase, present in versions 0.8.19 to 0.8.37 and fixed in 0.8.38. Stale cached address offsets let LoadResolver and EqualStoreEliminator treat coinciding memory or storage slots as distinct, so loads return outdated values or needed stores are dropped. It includes a pure-Solidity reproducer for --via-ir, a Sourcify scan with no differing bytecode, and checks for deployed contracts. Rated low severity.
What it is
A Yul optimizer bug in the KnowledgeBase component, affecting Solidity 0.8.19 through 0.8.37 and fixed in 0.8.38. Daniel Boye reported it on August 17, 2026 through the Ethereum Foundation bug bounty. Surya Prakash Akula's report of September 28, 2026 showed it is reachable from plain Solidity with no inline assembly. Solidity rates it low severity.
How it works
LoadResolver and EqualStoreEliminator track the known contents of memory and storage. To decide which locations a store may overwrite, they ask the KnowledgeBase, which models each variable as a constant offset from a base variable. Two addresses with the same base are treated as distinct if their offsets differ (storage) or are at least 32 bytes apart (memory).
These representations have been cached per variable since 0.8.19, and the cache was invalidated lazily by comparing a variable's value expression on lookup. That check missed reassignments in two cases: the variable had no tracked value before and after (assigned via x, y := f(), or in a branch or loop after control flow joined), or the derived address was cached before the base itself was looked up.
Two addresses derived from the same variable before and after a reassignment were then treated as offsets from one value. The optimizer wrongly concluded they were distinct. A later load could return the outdated value, a needed store could be removed, and a keccak256 could be taken over outdated data.
The page's reproducer is a contract with S[20] public a whose g increments a local i in two loops. It writes a[k].v with k = i + 2, then a[i].v, then reads a[k].v. Under --via-ir with the optimizer, g(0, 2, 0xaaaa, 0xbbbb) returns 0xaaaa instead of 0xbbbb.
Preconditions / Affected
All four must hold:
- The Yul optimizer is enabled (
--optimizeor--optimize-yul;settings.optimizer.enabledorsettings.optimizer.details.yul). - The step sequence contains LoadResolver (L) or EqualStoreEliminator (E). The default sequence has both.
- In the Yul fed to those steps, a variable used for addresses is reassigned, with one address derived before and another after, using different constant offsets.
- The two addresses coincide at runtime.
Exposure depends on the pipeline. With --via-ir, any code may be affected. Yul compiled with --strict-assembly is also affected. With the default evmasm pipeline, only compiler-generated Yul utility functions and inline assembly blocks that touch no Solidity variables or functions may be affected. The target EVM version is irrelevant. Conditions 3 and 4 cannot be reliably checked from source alone. Known default-sequence reproducers modify an address-computing local in a loop or branch both before and after the first address is derived. Where addresses come from call arguments, a caller may be able to choose inputs that make them coincide.
What to do
Upgrade to Solidity 0.8.38 or later before deploying with the Yul optimizer enabled. The fix (argotorg/solidity#17116) gives each KnowledgeBase variable a version that increments on every recorded value change, and caches representations per variable and version. It touches only libyul/optimiser/KnowledgeBase.h, KnowledgeBase.cpp, DataFlowAnalyzer.h and DataFlowAnalyzer.cpp.
For already-deployed contracts built with 0.8.19 to 0.8.37 and the optimizer on, the page offers three checks:
- Build the compiler at commit
ce5702db(last before the fix) and00d5ee65(fix merged). Compile with your deployed settings and compare bytecode, ignoring the metadata hash. Identical output means the fix changed nothing. Emscripten-only nightlies exist:0.8.38-nightly.2026.10.13+commit.ce5702dband0.8.38-nightly.2026.10.14+commit.8bff1c3aon binaries.soliditylang.org (soljson.jsinbin/). - Compile with your deployed compiler version but remove every L and E from its default optimizer step sequence, then run your test suite against both builds. This is unlikely to hit "Stack too deep", unlike a fully unoptimized build. A difference reveals the bug.
- Apply the fix to the exact release you deployed with and compare bytecode, ignoring the metadata hash. Identical means not affected. A difference does not prove the contract is affected. A symbolic equivalence checker such as hevm can compare deployed bytecode against the build from check 2 and may rule the bug out for all inputs, but may not scale to large contracts.
Caveats
Solidity scanned Sourcify, rebuilding about 283,000 verified --via-ir compilations and 607,000 evmasm compilations with a development compiler, with and without the fix. None produced different bytecode. That scan used the current optimizer, not the original compiler versions, and earlier optimizer steps changed between releases, so it is not a guarantee. Check 1 shares this limit. Check 2 only exposes the bug for inputs where the locations coincide. The compiler emits no warning, and the affected code does not necessarily revert.
Organisations named here
- argotorg/solidity#17116
- Daniel Boye
- EqualStoreEliminator
- LoadResolver
- Surya Prakash Akula
Technologies named here
- Solidity compiler 0.8.19 through 0.8.37
- Solidity compiler with Yul optimizer
- Yul optimizer steps LoadResolver and EqualStoreEliminator
- Code compiled with the Yul optimizer enabled
- KnowledgeBase
- Code compiled with --via-ir
- Sourcify database
- Yul compiled directly with --strict-assembly
- Default evmasm pipeline
- hevm
- Ethereum Foundation bug bounty program
- Solidity
- Yul
- Sourcify
Also named here
- Ethereum Foundation bug bounty
Related in AppSec
Flowise MCP Security Bypass Enables RCE
Inside an Oracle Database SQL Injection Attack | Huntress
OASIS
