It is well known that most transactions on Ethereum require a fee to ensure that the network is spam-resistant and doesn’t get stuck in an endless loop of calculations.
The gas fee is the amount of gas required to complete the transaction multiplied by the cost per unit of gas. The fee is paid whether the transaction succeeds or fails. As of November 2023, gas costs about 10 gwei. Although 1 gwei is now equivalent to about US$0.000002, we need to understand that this is just one unit, and you have to pay tens and sometimes hundreds of thousands of gas to complete transactions, which will be a more tangible amount. That is why we need to keep optimization in mind.
As developers, we cannot influence the price of gas, so we need to focus on the units of gas consumed by the code to reduce it as much as possible.
Due to the optimizations, developing on Ethereum is different from developing Web 2.0 applications because we have to give up familiar patterns, such as splitting into small functions, for ease of testing. In this article, I will provide several examples, for each of which I will give an optimized and unoptimized smart contract with the amount of gas consumed.
There are two main approaches to optimization:
Optimizing the cost of deployment (minimizing the size of the smart contract and the cost of
executing the constructor)
Optimizing each function call
The first thing to check is that the smart contract does not contain unnecessary and unused code, as deployment is compiled to bytecode, and the more of it there is, the more expensive deployment will be.
Here are some expensive gas operation codes in EVM:
CREATE/CREATE2 – Deploys a smart contract (32,000 gas)
SSTORE – Stores the value in memory (20,000 gas if the slot has not been accessed, 2,900
otherwise)
BALANCE (address(this).balance) – (2,600 gas if not previously accessed)
A complete list of all the codes for the different forks can be found here: https://www.evm.codes.
Optimizing each function call is a better strategy because the smart contract is deployed once.
Do not initialize variables with default values
If a variable is not initialized, the compiler assumes that it has a default value: 0
for uint
, “”
for string
, false
for bool
, 0x(0)
for address
. So, initializing with default values is a waste of gas.
Organize variables
Solidity stores variables in cells of 32 bytes. If a variable is smaller than a cell, it is packed with another variable, but such variables must be declared side by side.
Do not truncate variables
When declaring a single variable, it makes sense to use uint256
, because otherwise, the variable has to be truncated, which takes a bit more time.
Use static values
In cases where there is an option to pre-calculate static values, it is better to use it, even if it is hardcoded. For instance, in the example below, you need to call two functions that increase the gas consumption.
Use constant variables
For variables that will be constants, just one immutable
keyword will save a lot of gas.
Do not declare temporary variables
It may seem harmless to introduce an extra variable that is stored in memory rather than on the blockchain, but adding an intermediate variable requires more gas.
Use mappings instead of arrays
This is not always possible, but where possible, arrays should be avoided as they are more expensive to work with.
Prefer a fixed-length array to a dynamic one
In some cases, you cannot avoid using arrays. Dynamic arrays are more expensive than fixed-length arrays because you have to change the array length when using the push and pop functions.
Use the data type you need in the array
What applies to variables does not apply to arrays. If you specify the required type, the engine will pack the data in the correct way.
Do not create many functions that call each other
In any other language, the smaller the function, the better it is for debugging and testing, but things are not the same in Solidity. It is also desirable to minimize function calls to third-party smart contracts.
Avoid multiple changes to state variables
If there is a variable in the blockchain whose value needs to be changed multiple times, it is better to create a temporary variable and write the final value to the blockchain after the loop is executed.
Optimize gas by removing unnecessary variables
Ethereum offers gas refunds for deleting variables. Using the delete
keyword with a variable name will return 15,000 gas but no more than half of the transaction value.
Use selfdestruct() to compensate for gas
Deleting a contract without the option to restore will return 24,000 gas but no more than half of the transaction value.
Use Solidity Gas Optimizer
The built-in optimizer allows you to adjust the value of iterations depending on your optimization goals. A lower value should be set to minimize deployment costs, and a higher value should be set to optimize runtime costs. Below is the configuration file for the Solidity Optimizer with Hardhat.
At compile time, contract code is converted to byte code for EVM, and transaction codes have a cost depending on the Ethereum fork. Despite initiatives to reduce the cost of gas (in particular, the introduction of variable size blocks in the London update, which changed the way fees are calculated), as developers, we need to optimize both the deployment and function calls in smart contracts to save money on deployment and execution.