Storage Write Removal Before Conditional Termination
Overview
The Yul unused-store eliminator in Solidity 0.8.13 through 0.8.16 dropped sstore instructions that preceded a conditional return or stop reached through inline assembly or an inlined function. The minimal reproduction is x = 1; g(a); x = 2; where g may terminate execution: the optimizer concluded the first write to x was unused and erased it, even though g could end the call before the second write ran. The pass was found through differential fuzzing and the bug was fixed in 0.8.17.
The result is that a storage variable keeps a stale value. A write the source clearly performs simply does not appear in the deployed bytecode, so the on-chain state diverges from what the code says it should be.
Sigvex reports this from recovered compiler metadata combined with a runtime shape signal. The contract must actually contain at least one storage write and at least one stop or return terminator; code with no storage writes, or no short-circuit exits, cannot exhibit the bug. When metadata records that the triggering optimizer pass ran, the finding is reported at high confidence; when the optimizer step sequence is unknown, it is reported at reduced confidence to flag the affected version.
Why This Is an Issue
A silently dropped storage write corrupts contract state. A balance update, a re-entrancy lock, an ownership transfer, or a paused flag that the source sets may never be persisted, leaving the contract in a state its own logic believes is impossible. Because the write is removed during optimization, neither source review nor a quick read of the code reveals the gap; only the bytecode shows it. The consequences range from accounting drift to bypassed access guards, which is why this is treated as a high-severity miscompilation.
How to Resolve
Recompile with a fixed compiler and redeploy.
// Affected: compiled with 0.8.13 - 0.8.16
pragma solidity 0.8.15;
contract Vault {
bool private locked;
function withdraw() external {
locked = true; // may be dropped by the optimizer
_payout(msg.sender); // can terminate the call
locked = false;
}
function _payout(address to) private {
(bool ok, ) = to.call{value: address(this).balance}("");
require(ok);
}
}
// Fixed: compile with 0.8.17 or later
pragma solidity 0.8.17;
If an upgrade is not feasible, manually audit every storage write that precedes a potentially terminating external call and confirm the write survived code generation.
Detection Methodology
The detector checks the recovered compiler version against the affected range and inspects optimizer metadata for the Yul pass that triggers the bug. It then scans the recovered instruction stream for two independent signals: at least one persistent storage write, and at least one stop or return terminator. Both must be present, since a contract that never writes storage has nothing to lose and a contract that never short-circuits has no terminator for the write to precede. When the triggering pass is confirmed in metadata the finding carries high confidence; when the optimizer step sequence cannot be recovered it is demoted, and it is further reduced when the metadata source itself is unreliable.
Sample Sigvex Output
{
"detector_id": "solc-2022-09-storage-write-removal",
"severity": "high",
"confidence": 0.9,
"description": "solc-2022-09-storage-write-removal: Contract was compiled with Solidity 0.8.15, which is in the affected range [0.8.13, 0.8.17). Runtime corroboration: contract uses SSTORE and has at least one STOP/RETURN terminator.",
"location": {
"function": "withdraw",
"offset": 196
}
}
Limitations
False positives are possible: the presence of a storage write and a terminator does not prove the specific drop occurred, and a contract in range may simply never hit the pattern. Confidence is reduced when the optimizer step sequence is unknown or when metadata is unreliable. False negatives occur when compiler metadata cannot be recovered, since the version cannot be placed in range without it.
Related Detectors
- Outdated Compiler — general detection of compiler versions with known bugs.
- Transient Storage Helper Collision — a later compiler bug that silently misroutes storage zeroing.
- Atomic State Update — contract-level state-consistency issues around partial updates.