
Critical Vulnerability in Solidity Compiler: Incorrect Deletion of State Variable Assignment
TechFlow Selected TechFlow Selected

Critical Vulnerability in Solidity Compiler: Incorrect Deletion of State Variable Assignment
This article provides an in-depth explanation, at the source code level, of a medium/high-severity vulnerability in the Solidity compiler (0.8.13 <= solidity < 0.8.17) that occurs during compilation. Due to flaws in the Yul optimization mechanism, state variable assignment operations are incorrectly removed. The article also covers the underlying principles of this issue and corresponding preventive measures.
This article provides an in-depth analysis from the source code level of a medium-to-high severity vulnerability in the Solidity (0.8.13 <= solidity < 0.8.17) compiler, caused by flaws in the Yul optimization mechanism that incorrectly remove state variable assignments during compilation, along with corresponding preventive measures.
It aims to help smart contract developers enhance security awareness during development and effectively avoid or mitigate the impact of the SOL-2022-7 vulnerability on contract code security.
Vulnerability Details
Yul optimization is an optional feature in Solidity used to compile contract code, reducing redundant instructions and thereby lowering gas costs during deployment and execution. For more details about Yul optimization, refer to the official documentation.
During the UnusedStoreEliminator optimization step, the compiler removes seemingly "redundant" Storage write operations. However, due to a flaw in identifying redundancy, when a Yul function block calls a specific user-defined function (whose internal branch does not affect the control flow of the calling block), and there are consecutive writes to the same state variable before and after the call within that Yul block, the Yul optimizer may permanently eliminate all Storage write operations preceding the function call at the compilation level.
Consider the following code:
contract Eocene {
uint public x;
function attack() public {
x = 1;
x = 2;
}
}
During UnusedStoreEliminator optimization, x = 1 is clearly redundant for the entire execution of function attack(). Naturally, the optimized Yul code will remove x = 1 to reduce gas consumption.
Now consider inserting a call to a custom function in between:
contract Eocene {
uint public x;
function attack(uint i) public {
x = 1;
y(i);
x = 2;
}
function y(uint i) internal{
if (i > 0){
return;
}
assembly { return( 0, 0) }
}
}
Clearly, because of the call to y(), we must determine whether y() affects the execution flow of attack(). If y() could terminate the entire function execution (note: not revert—this can be achieved via the return() instruction in Yul assembly), then x = 1 cannot be removed. In this example, since y() contains assembly { return(0, 0) }, which terminates the message call, x = 1 should not be deleted.
However, due to a logic issue in the Solidity compiler, x = 1 is incorrectly removed during compilation, permanently altering the intended code logic.
Actual compilation test results are as follows:
Shockingly! The Yul code for x = 1, which should not have been optimized away, is gone! Read on to learn why.

In the UnusedStoreEliminator component of the Solidity compiler, SSA variable tracking and control-flow analysis are used to determine whether a Storage write operation is redundant. When entering a user-defined function, UnusedStoreEliminator performs the following:
-
On encountering a memory or storage write: store the operation in the m_store variable and set its initial status to Undecided;
-
On encountering a function call: retrieve the memory/storage read/write locations of the called function and compare them with all operations marked as Undecided in m_store;
-
If the function call overwrites a stored write operation, mark the corresponding operation in m_store as Unused;
-
If the function call reads from a previously written location, mark the corresponding operation in m_store as Used;
-
If the function has no branches that allow continued execution, mark all memory write operations in m_store as Unused;
-
Under these conditions, if the function can terminate execution flow, mark all Undecided storage write operations in m_store as Used; otherwise, mark them as Unused;
-
At function end: remove all write operations marked as Unused.
Initialization code for memory or storage write operations:
As shown, encountered memory and storage write operations are stored into m_store.

Handling logic upon encountering a function call:
Here, operationFromFunctionCall() and applyOperation() implement steps 2.1 and 2.2 above. The conditional statement below based on the function's canContinue and canTerminate implements step 2.3.
Note: It is precisely the flaw in this lower conditional check that leads to the vulnerability!

operationFromFunctionCall() retrieves all memory/storage read/write operations of the function. Note that Yul includes many built-in functions such as sstore() and return(). Different handling logic applies to built-in versus user-defined functions.

The applyOperation() function compares all read/write operations obtained from operationFromFunctionCall() against those stored in m_store to determine whether they were accessed during the call, updating their status accordingly.

Consider how the UnusedStoreEliminator optimization logic processes the attack() function in the Eocene contract:
Store x = 1 operation into m_store with status set to Undecided
1. Encounter call to y(), retrieve all memory/storage read/write operations caused by y()
2. Traverse m_store; find that none of the operations caused by y() relate to x = 1; x = 1 remains Undecided
- Obtain control flow information of y(); since y() has a branch that allows normal return, canContinue is True, so it does not enter the conditional check. Thus, x = 1 remains Undecided!!!
3. Encounter x = 2 storage operation:
-
Traverse m_store, find x = 1 in Undecided state; since x = 2 overwrites x = 1, mark x = 1 as Unused.
-
Store x = 2 into m_store with initial status Undecided.
4. Function ends:
-
Change all operations in m_store with status Undecided to Used
-
Remove all operations in m_store marked as Unused
Clearly, when calling a function, if the called function can terminate message execution, all preceding Undecided write operations should be marked as Used—not left undecided—otherwise, writes before the function call may be incorrectly removed.
Moreover, note that control flow flags for each user-defined function are inherited through recursive calls. Therefore, even if the deepest-level function satisfies the correct logic, x = 1 might still be deleted in complex call chains.
In Solidity, an example shows similar logic where the contract is unaffected. However, this immunity is not due to any correction in UnusedStoreEliminator’s logic, but rather because an earlier Yul optimization step—FullInliner—automatically inlines small or single-use functions into their callers, thus avoiding the vulnerable condition involving user-defined functions.
contract Normal {
uint public x;
function f(bool a) public {
x = 1;
g(a);
x = 2;
}
function g(bool a) internal {
if (!a)
assembly { return( 0, 0) }
}
}
Compilation result:
Function g(bool a) is inlined into function f(), eliminating the user-defined function call and thus preventing the vulnerability from being triggered.

Solutions
The most fundamental solution is to avoid using Solidity compilers within the affected version range (0.8.13 <= solidity < 0.8.17). If using a vulnerable compiler version is necessary, consider disabling the UnusedStoreEliminator optimization step during compilation.
For mitigation at the contract code level, given the complexity of multiple optimization steps and actual function call flows, it is strongly recommended to engage professional security experts to perform code audits to identify and address potential security issues arising from this vulnerability.
Join TechFlow official community to stay tuned
Telegram:https://t.me/TechFlowDaily
X (Twitter):https://x.com/TechFlowPost
X (Twitter) EN:https://x.com/BlockFlow_News














