Optimizing Gas in Solidity Smart Contracts: Choosing the Right Storage
In recent years, Ethereum Virtual Machine (EVM) networks have gained significant traction. Every day, a growing number of new users join these networks, engaging in numerous transactions. However, this increased activity leads to rising transaction fees, sparking interest in reducing these fees to make Web3 apps more affordable and user-friendly.
One promising solution is optimizing the gas execution of smart contracts. By using the right implementation approach, developers can create more efficient smart contracts, thereby reducing gas fees. This optimization not only makes transactions cheaper but also enhances the overall user experience on EVM networks. As these improvements continue, the future of Web3 applications looks increasingly promising.
Solidity Development
Solidity is the most widely used programming language for developing smart contracts on Ethereum Virtual Machine (EVM) chains. Smart contracts are executed on-chain, and each action in a contract transaction incurs a gas cost. Naturally, complex or resource-intensive operations consume more gas.
The most gas-intensive operations are those related to storage. Adding and reading data from storage can become prohibitively expensive if not handled properly, utilizing all available storage areas. When examining EVM Codes, it is evident that STORE opcodes for storage are significantly more expensive than opcodes for memory usage. Specifically, they are 33 times more costly.
Opcode |
Gas |
Description |
SLOAD |
100 |
Load word from storage |
SSTORE |
100 |
Save word to storage |
MSTORE |
3 |
Load word from memory |
MLOAD |
3 |
Save word to memory |
Storage Areas
The EVM offers five storage areas: storage, memory, calldata, stack, and logs. In Solidity, code primarily interacts with the first three because it doesn’t have direct access to the stack. The stack is where EVM processing takes place, and accessing it requires low-level programming techniques. Logs are used by Solidity for events, but contracts cannot access log data once it is created.
Storage- A key-value store that maps 256-bit words to 256-bit words;
- Stores all smart contract’s state variables which are mutable (constants are part of the contract bytecode);
- Is defined per contract at deployment time.
- Created for function calls;
- Linear and addressable at the byte level;
- Reads limited to 256 bits width, writes can be 8 or 256 bits wide;
- Stores all function variables and objects specified with the memory keyword;
-
Recommended for storing mutable data for a short period.
Calldata
- A temporary location which stores function arguments;
- It can’t be written and is used only for readings.
Gas Optimization Approaches
To lower gas costs related to storage, prioritize using memory over storage. Consider the following smart contract which uses the storage area exclusively:
contract GasCostComparison {
uint256[] private s_numbers;
uint256 private s_sum;
function numberSum()public returns(uint256) {
for(uint i=0; i< s_numbers.length; i++){
s_sum+=s_numbers[i];
}
return s_sum;
}
function initNumbers(uint256 n)public {
for(uint i=0; i < n; i++){
s_numbers.push(i);
}
}
}
If s_numbers is initialized by calling initNumbers with n=10, the gas usage for numberSum would be 53,010 gas.
Avoid Reading Too Often from Storage
In the `for` statement, we compare the index i with s_numbers.length. Even though we might think the array length is read from storage only once, it is read every time the comparison takes place. To optimize, read the length only once from storage:
function numberSum()public returns(uint256) {
uint256 l = s_numbers.length;
for(uint i=0; i< l; i++){
s_sum+=s_numbers[i];
}
return s_sum;
}
We store the length read from the storage in the l variable which is stored in the memory area of the new numberSum() function.
This reduces gas usage to 51,945 gas, saving 1,065 gas.
Avoid Writing Too Often in Storage
Similarly, storing the final sum only at the end of the for statement in the s_sum state variable (which is in storage) is more efficient. Create a temporary variable sum in memory:
function numberSum()public view returns(uint256) {
uint256 l = s_numbers.length;
uint256 sum = 0;
for(uint i=0; i< l; i++){
sum+=s_numbers[i];
}
return sum;
}
Gas execution this time is 27,770 gas, almost half of the previous cases.
Choosing the right storage type can significantly reduce blockchain gas fees, as shown in the examples above. Optimizing how data is stored and accessed is crucial for minimizing costs and improving the efficiency of smart contracts on Ethereum Virtual Machine (EVM) chains.
By prioritizing memory over storage for mutable data and understanding the nuances of gas costs associated with different operations, developers can significantly enhance the performance and cost-effectiveness of their applications in the Web3 ecosystem.