Eocene | Security
6 min readMay 6, 2023

--

Solidity compiler medium-high risk vulnerability:Analysis of storage Write removal on conditional early termination [SOL-2022–7]

## Overview

This article introduces the storage write removal of solidity compiler (0.8.13<=solidity<0.8.17) which caused by Yul optimization mechanism from the source code level and corresponding prevention measures in detail.

Help contract developers to raise awareness of security in contract development, effectively avoid or alleviate the impact of SOL-2022–7 vulnerabilities on the security of contract code.

## Vulnerability details

The Yul optimization mechanism is the optional option of the Solidity compiler. It can reduce some redundant instructions in the contract, thereby reducing the cost of the contract deployment and execution. The specific Yul optimization mechanism can refer to official document

In the UnustedStoreliminator optimization steps of the compilation process, the compiler will remove the redundant storage writing operation.

But due to the recognition defect of “redundant”, when a yul function block calls a specific user definition function (There is an internal existence of a certain branch that does not affect the execution flow of the calling function ), and the writing operation of the same state variables before and after the called function is used in the calling function , which will cause all the `storage` writing operations before the called function is permanently deleted at the compilation level.

Consider the following code:

contract Eocene {
uint public x;
function attack() public {
x = 1;
x = 2;
}
}

In this case, the entire execution of the entire execution of the function `attack ()` is obviously redundant. Naturally, the optimized yul code will delete the `x = 1;` to reduce the GAS consumption of the contract.

Next, consider inserting the call of the custom function:

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) }
}
}

Obviously, due to the call of the `y()` function, we need to judge whether the `y()` function will affect the execution of the function `attack()`, if the `y()` function can cause the entire function to execute the termination (note , Not revert, the `return()` function in the yul code can terminate the entire message call), then`x = 1` cannot be deleted. For the above contract, because the `y ()` function exists `Assembly {return(0,0)}` can cause the entire message to be terminated, `x = 1` should not be deleted.

However, in the solidity compiler, due to the logical problem of the optimizer’s code, the `x = 1` is deleted by error during compilation, which permanently changes the logic of the contract code.

Let’s take a look at the compilation results of the above contract code:

Oops!`x = 1` lost!.Let’s enter the compiler code to find the cause of this wrong behavior.

In the UnustedStoreliminator of the solidity compiler code, it is determined whether a Storage writing operation is redundant through the SSA variable tracking and control flow tracking.

When entering a custom function, the behavior of UnusedStoreEliminator is as follows:

  1. Encountered the writing operation of `Memory` or` Storage`: Store them in the `m_store` variable, and set the initial state of the operation to`Undecided`;
  2. Encountered a function call: Get the operation to the `Memory` or` Storage` of the function, and compare it with the operation of all `Undecided` stored in the`m_store` variable:

2.1 If the writing operation in the `m_store` is covered, the state of the corresponding operation is changed to`Unused`.

2.2 If the operation is read on the value written in `m_store`, change the corresponding operation state to`Used`.

2.3 If this called function has no branches that can continue to execute the entire message call, change all the memory writing operations state in `m_store` from `Undecided` to `Unused`

2.3.1 Under the condition of appeal, if the called function can be terminated, the state of the storage writing opration in the `m_store` is changed from the` Undecided` to the `Used`;

3. Encountered the end of the function: Delete all the writing operations marked as `unused`

The code for initialize the state of writing operation as follows.

As we see, all `Memory` and` Storage` Writing operations are stored in the variables of `m_store`, and the initialization status is` Undecided`:

The processing logic code when encountering a function call is as follows:

Among them, `OperationFromFunctionCall()` and` Applyoperation()`realize the 2.1,2.2 logic of appeal. The `If` statement based on the value of `CanContinue` and` CanTerminate` is the realization of logic 2.3.

It should be noted that the defect of `If` judgment has led to the existence of this vulnerability!

`OperationFromFunctionCall ()` Get all memory and storage read and write operations of the called function function. It should be noted here that there are many built-in functions in Yul, such as `sstore (), return ()`. Here you can see different processing logic for built-in functions and user definition functions.

The `applyoperation()` function will compare all the read and write operations obtained from `OperationFromfunCitonCall()` with the operations stored in `m_store` , and modify the corresponding status based on the judgment results.

Think of the above UnustedStoreliminator optimization logic of the `attack()` function of our Eocene contract:

1. Put the `x = 1` store in the` m_store` variable, and set the status to `Undecided`

2. Get all read and write operations of the `y ()` function call:

2.1 Traversing the variable `m_store`, found that all read and write operations caused by the call of`y()`, and have nothing to do with `x = 1`. The state of `x = 1` in `m_store` is still `Undecided`.

2.2 Get the control flow logic of the `y()` function, because the `y()` function exists branches that can be returned normally, so `cancontinue` is `true`, does not enter if judgment. `x = 1` State is still `Undecided` !!!

3. Encounter `x = 2` storage operation:

3.1 Traversing the `m_store` variable, found that `x = 1` in the state of `Undecided`, `x = 2 ` operate covering` x = 1`, then set the `x = 1` state to `unused`.

3.2 Save the `x = 2` in the` m_store`, the initial state is `Undecided`.

4. Function end:

4.1 Change the operation status of all `m_store` from `Undecided` to `Unused`.

4.2 Delete the writing operation in the state of the `unused`.

Obviously, the problem that causes vulnerabilities is that when calling the function, if the called function can be terminated, the optimizer should but not deal with the writing operation of the `Undecided` state before the called function. As a result, the status variable writing operation before the call function is deleted.

In addition, it should be noted that each user’s custom function control flow state will be passed, so in the scenario recursive call, even if the bottom layer function meets the appeal logic, x = 1 may be deleted.

In Solidity Blog, the contract code that will not be affected under the same logic. That is because the Yul optimization steps before UnusedStoreliminator, the Fullinlineer optimization process, that will embed a small or only one-called called function into the calling function, and the “user defined function” in the vulnerability trigger condition is avoided.

Example code:

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 results are as follows:

The function `g(bool a)` is embedded into the function `f()` to avoid the vulnerability conditions of the “user defined function” and avoid the occurrence of vulnerabilities.

## Prevention measures

1. The most fundamental solution is that does not use the influential Solidity compiler to compile your contract code.

2. If the compiler of the vulnerability version needs to be used, you can consider removing the UnusedStoreEliminator optimization step during compilation.

3. If you want to relieve vulnerabilities from the contract code level, considering the complexity of multiple optimization steps and the complexity of the actual function call flow, please find professional security personnel for code audit security question.

--

--

Eocene | Security

Smart contract audit, attack analysis, web3 security research, on-chain monitor and alert. Powered by Eceone Research.