The Optimizer’s Guide to Solidity pt. 3 — Hidden Gas Costs

Omniscia
7 min readOct 20, 2022

--

omniscia.io

​Today we are diving deeper into the internal working of the Ethereum Virtual Machine (EVM) to illustrate how the Omniscia team regularly “exploits” peculiar traits of the EVM to minimize the execution cost of solidity smart contracts for its clients. In case you are not up to date on ‘The Optimizer’s Guide to Solidity’ series, you can find part one here and two here. In order to stay up to date with all the security tips and insights we share, be sure to subscribe to our newsletter where we will be publishing many tips and insights solidity developers can leverage to design and develop more secure and gas-efficient smart contracts.

This week we look at two different types of optimizations that are relatively simple to apply to any codebase and that are relevant to a majority of smart contracts we audit for our clients. The first optimization is applicable to all versions of Solidity. The second optimization discussed in this article is only valid for pragma versions 0.8.0 onwards. However, lets first clarify at a high level how one can assess the cost of any EVM instructions.

EVM Instruction Cost Assessment

​At the very core, the reasoning behind introducing “gas costs” to transactions and “gas limits” to assembled blocks on EVM blockchains is to 1) introduce an additional revenue stream to incentivize stakers (previously miners) to secure and validate the network and 2) act as a protection measure against Denial-of-Service (DoS) attacks by prohibiting the execution of computationally expensive tasks via the upper gas limit (e.g. for loops with significant limits that would otherwise delay the median block creation rate).​

To strike a balance between block builder compensation and a competitively priced computational system, EVM blockchains dynamically adjust their block gas limit based on the demand for bandwidth amongst network actors. Transactional gas costs are generally slow to evolve and revolve around a simplistic basis; the computational cost of performing a transaction. This may seem simple to perform within centralized and / or traditional computing environments. However, it differs greatly in blockchain ecosystems due to the way state changes of the blockchain ledger are propagated across the network of nodes that validate the instructions performed on a decentralized network.

In this article we illustrate how the hidden cost of memory can inflate the cost of otherwise straightforward transaction types on EVM blockchains, and how developers can optimize their dapps to reduce their gas footprint.​

EVM: Local Variable Hidden Cost

​When declaring a local variable that is the result of a statement, we incur a hidden gas cost proportional to the amount of “memory” needed for the local variable we declared. This extra cost is usually offset when reading variables from storage (SLOAD), storing them to a local variable (MSTORE plus hidden cost [A0–1](https://github.com/wolflo/evm-opcodes/blob/main/gas.md#a0-1-memory-expansion) chapter of evm-opcodes), and reading them on each utilization (MLOAD).

This is not the case when dealing with primitive instructions of the EVM. Indeed, each transaction on a blockchain contains a set of unavoidable data. Consequently, sets of data are exposed to all smart contracts via primitive EVM instructions that consume very small amounts of gas. This is due to the fact that they do not require extra memory to read special data slots, as they are already loaded in-memory as part of the EVM’s block creation workflow.

The instruction set is significant and revolves around contextual data of a transaction, such as the msg.sender, block.timestamp, and more. As such, the following contract implementation is in fact inefficient:​

Snippet from “Context.sol” of Aave v3 @ f3e037b

The Context contract is an implementation that was introduced by OpenZeppelin with the intention to streamline the development process of a contract that can be easily upgraded to a meta-transaction compatible one. However, thus far it has been mostly misused and has led to non-negligible gas increase for various protocols including both Aave V2 and Aave V3. To illustrate a tangible example of the IncentivizedERC20 implementation of Aave V3, we have provided a caption below:

Snippet from “IncentivizedERC20.sol” of Aave v3 @ f3e037b

​In the above function, we incur the _msgSender implementation’s msg.sender gas cost (CALLER: 2), and the _msgSender() invocation itself (JUMP: 2 as well as the memory allocation of the returned variable) twice. By optimizing the above segment, we can reduce the instructions’ gas cost by half:​

Optimized Snippet from “IncentivizedERC20.sol” of Aave v3 @ f3e037b​

While the optimization by itself may be insignificant, it will lead to tangible savings when applied across the entire codebase.

​Solidity: Mathematical Hidden Cost​

Hidden gas costs are not only limited to the EVM. Developers need to be cognizant that the Solidity language itself has introduced some hidden costs in its latest minor semver 8, which enforces safe arithmetics by default. Given that a lot of applications already perform safety checks for unsafe arithmetic operations, as part of their error handling workflows, built-in safe arithmetic checks become superfluous and thus incur an increase in gas redundantly.

Thankfully, Solidity also introduced a new code-block declaration style that instructs the compiler to perform arithmetic operations unsafely: unchecked code blocks. These blocks can be cleverly utilized to significantly reduce the gas cost of a particular contract as long as the operation is guaranteed to be performed safely by surrounding statements and / or conditions. As an example, let’s take a look at this segment of the Compound CToken implementation’s _reduceReservesFresh function:

Snippet from “CToken.sol” of Compound @ a3214f6​

The totalReservesNew calculation and assignment are performed after the condition reduceAmount > totalReserves has been evaluated to false. This means that the trait totalReserves >= reduceAmount has been guaranteed by the execution context and as such the calculation of totalReservesNew can be performed in an unchecked code block as it is guaranteed to be performed properly. After optimization the above code block should resemble the caption below:​

Optimized Snippet from “CToken.sol” of Compound @ a3214f6

Another way to avoid built-in safe arithmetics from incurring extra gas is during incrementation operations (++ and --). These operations are usually performed in for loops and in any post-0.8.X version. Each incrementation is performed with bound-checks when they are entirely redundant. A very simple example illustrating this is provided below:

Example Snippet of Loops​

Due to the inherent constraints of Solidity, bar.length is guaranteed to fit within a uint256 variable, meaning that performing a safe incrementation on each loop for the i variable would be redundant. To optimize such code blocks, we relocate the incrementation to the end of the unchecked code block:​

Optimized Example Snippet of Loops

​Verdict​

The EVM is an intrinsically complex machine and as a consequence multiple tools have been developed to aid developers in creating solutions within its system using high-level languages (such as Solidity). The simplifications performed at the liberty of the Solidity compiler, however, are usually poorly relayed to the developer community and as such, programmers end up creating inefficient programs.

To counteract this, our Optimizer’s Guide to Solidity series outlines valuable tips and insights that address many of the challenges developers face when developing and deploying secure, scalable and optimized decentralized applications/smart contracts.

Omniscia.io is one of the fastest growing and most trusted blockchain security firms and has rapidly become a true market leader. To date, our team has collectively secured over $200+ billion worth of digital assets, worked with 220+ clients and detected over 1000+ high-severity issues in our clients’ smart contracts.

Founded at the start of 2021 by blockchain cybersecurity veterans, omniscia.io is a pioneer in Web3 security, utilizing years of experience, developing proprietary tooling and a tried-and-tested approach to securing smart contracts and complex decentralized protocols out there — including the likes Aave, YFI, lien, 1inch, fetch, compound, synthetix, and many others.

Our clients, partners and backers include leading ecosystem players such as Polygon, AvaLabs, CLabs, Olympus DAO, Fetch.ai, allianceBlock, Boson Protocol, and many more.

Be sure to follow our social medias and subscribe to our newsletter for more updates:

Twitter / LinkedIn / Newsletter

--

--

Omniscia

Team of experienced smart contract auditors & developers with deep expertise building & securing complex #decentralized networks & applications …