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:
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:
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:
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:
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:
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:
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:
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: