Basics of Gas Optimization in Solidity Smart Contract
Gas optimization is a matter of doing what is cheap and avoiding what is expensive in terms of gas costs on EVM blockchains.
What is cheap:
Reading constants and immutable variables.
Reading and writing local variables.
Reading and writing
memory
variables likememory
arrays and structs.Reading
calldata
variables likecalldata
arrays and structs.Internal function calls.
What is expensive:
Read and write state variables that are stored in contract storage.
External function calls.
Loops
If you can rewrite or organize your code so you are doing more of what is cheap and less of what is expensive then you are saving gas and that’s a gas optimization.
These days gas optimization is getting more and more important, even on sidechains and L2 solutions.
A common optimization is to replace state variable reads and writes within loops with local variable reads and writes. This is done by assigning state variable values to new local variables, reading and/or writing the local variables in a loop, then after the loop assigning any changed local variables to their equivalent state variables.
Here is a simple example of unoptimized code:
function doIt() external {
for(uint256 i; i < myArray.length; i++) { // state reads
myCounter++; // state reads and writes
}
}
Here is the optimized code:
function doIt() external {
uint256 length = myArray.length; // one state read
uint256 local_mycounter = myCounter; // one state read
for(uint256 i; i < length; i++) { // local reads
local_mycounter++; // local reads and writes
}
myCounter = local_mycounter; // one state write
}
The optimized code takes the state reads and writes out of the loop so that they are done one time instead of multiple times.
Packing Structs
A common gas optimization is “packing structs” or “packing storage slots”. This is the action of using smaller types like uint128 and uint96 next to each other in contract storage. When values are read or written in contract storage a full 256 bits are read or written. So if you can pack multiple variables within one 256-bits storage slot then you are cutting the cost to read or write those storage variables in half or more.
Packing structs is a way to reduce contract storage reads and writes. Because with it you can get the values of multiple state variables or write the values of multiple state values in a single state read or state write to contract storage.
Here is an example of an unoptimized struct:
struct MyStruct {
uint256 myTime;
address myAddress;
}
Example of an optimized struct:
struct MyStruct {
uint96 myTime;
address myAddress;
}
In the above the myTime and myAddress state variables take up 256 bits so both values can be read or written in a single state read or write.
External Function Calls
It is sometimes possible to convert external function calls between contracts into internal function calls.
Gas costs increase linearly as more external function calls are made to different smart contracts within a single transaction. External calls are expensive.
Internal calls are extremely cheap. They are a jump from one place in the code to another.
It is not possible in all cases to convert external calls to internal calls. You can only do it with smart contracts under your control. So this is useful if you are building a larger multi-contract system like an exchange, an NFT platform, or DeFi protocol, etc.
Let’s say that you have multiple contracts that make external function calls between each other. These contracts become facets of your diamond. A facet is a smart contract whose external functions get added to a diamond.
There are two ways to convert external functions to internal functions. Which way you choose is up to your coding style. But the basic principle is the same: change external functions to internal functions and import them into smart contracts that need them and use them.
Solidity Gas Optimizer
Make sure Solidity’s optimizer is enabled. It reduces gas costs. If you want to gas optimize for contract deployment (costs less to deploy a contract) then set the Solidity optimizer at a low number. If you want to optimize for run-time gas costs (when functions are called on a contract) then set the optimizer to a high number.
Subscribe to my newsletter
Read articles from Soft Skiller directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Soft Skiller
Soft Skiller
Senior Blockchain Full-stack engineer specializing in Solidity smart contracts, web3, and Golang. Work experience with IT startups for over a decade, helping to deliver an MVP, leading to successful funding.