Table Of Contents
- Introduction
- On Chain or Off Chain
- Solidity Optimiser
- EVM Slots
- Loop through an array
- Libraries
- Events
- Computed Values
- Constants
- Error handling
Introduction
When writing smart contract in Solidity, you may not be really concerned about the gas consumption in Remix environment or in a testnet. You only realise how much cost every OPCODES when you deploy in the mainnet on Ethereum (or in L2 but it's quite cheap).
Today, we will cover some optimisation we can do in Solidity. This list is not exhaustive and feel free to test every option you can.
The very first rule we keep in mind is : Every state variable we update cost you some fees. For more information, you can see the following documentation : Opcode. Many EIP try to optimise or adjust the gas cost of operations in the EVM. (EIP 2929, EIP 1559 for example)
On Chain or Off Chain Data
As mentioned above, writing state variable cost you some gas. That's why you should be aware that every user's call can cost a lot. You should be asking : "Do I really need to store this information on chain ?"
Every data related to the "ecosystem" or that doesn't need to be censored should be inside your smart contract. When it comes to metadata, files you can handle them off chain.
Solidity Optimiser
It's a mode that enable to produce a more efficient bytecode by reducing, the code size or the gas needed for the deployment. Keep in mind that the optimiser is not turned on by default because it takes more time to compile the actual solidity code which is not efficient in development mode. Turn on the optimiser when you want to deploy your smart contract
EVM Slots
You have probably seen or heard someone telling you to order your state variables by type ?
This is not without any reason. The EVM stores the data of a smart contract into a memory slots of 256 bits. A uint256 will fill one slot. But a uint128 will fill 128 bits over 256. if the next state variable is a uint256 then, the variable will be stored in the next slot. The EVM will perform a computation to force the previous slots to fit the remaining space.
Take this code :
uint128 variable_1
uint128 variable_2
uint256 variable_3
variable_1
will be stored in slot 0. Then variable_2
can fit inside the slot 0 because it has a remaining space of 128 bits.
After that, the variable_3
will be stored in slot 1.
In a not optimised version we can have this code
address address_1
uint128 variable_1
uint256 variable_2
address address_2
uint128 variable_3
address_1
is 160 bits long (address is a 20 bytes type). It will be stored in slot 0. variable_1
is 128 bits. But 128 + 160 = 288 bits. It can't fit in the 96 bits remaining space. (256-160). Then, variable_1
will be stored in slot 1. variable_2
is a 256 bits length. It will be stored in slot 2 because it can't fit in the slot 1 (128 bits remaining space). address_2
will be in slot 3 because the slot 2 is full. The variable_3
will be in slot 4 because the slot 3 has already 160 bits reserved (96 remaining).
To optimise this version, we can adjust the variable as follow :
uint128 variable_1
uint128 variable_3
uint256 variable_2
address address_1
address address_2
Here, there is 4 slots taken while the non-optimised version has 5 slots taken. We have saved 32 bytes and prevent unnecessary computation.
Loop through an array
Arrays are quite common nowadays and it's pretty straightforward to read values from them with a for-loop.
If you have guessed where I'm going by saying that, it means you read carefully the introduction.
If you read an array which is a state variable, looping inside the array without caching it into a memory variable cost you more gas.
In the following example, you can see that for a 9-length array, we spent 83037 gas in the un-optimised version and only 56475 in the most optimised
My advice: Avoid looping through an array that has a dynamic length. The gas consumption increases overtime and it will become a very expensive process.
(Try it on Remix)
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract GasTest {
uint[] public arrayTest;
uint public sumArray;
constructor(){
arrayTest = [1,2,3,4,5,6,7,8,9];
}
/**
* Keep reading the value of the array from the state variable
* Keep writing the result in the state variable sumArray
* Bad idea
* Gas cost = 83037
*/
function calculateSum() external {
for(uint i = 0; i<arrayTest.length; i++){
sumArray += arrayTest[i];
}
}
/**
* Keep reading the value of the array from the state variable
* Caching the sum in the memory. Write only once in the state variable
* Not an excellent idea
* Gas cost = 58087
*/
function calculateSum2() external {
uint _sumArray = 0;
for(uint i = 0; i<arrayTest.length; i++){
_sumArray += arrayTest[i];
}
sumArray = _sumArray;
}
/**
* Caching the array in the memory
* Caching the sum in the memory. Write only once in the state variable
* Excellent idea
* Gas cost = 56475
*/
function calculateSum3() external {
uint _sumArray = 0;
uint[] memory _arrayTest = arrayTest;
for(uint i = 0; i<_arrayTest.length; i++){
_sumArray += _arrayTest[i];
}
sumArray = _sumArray;
}
}
Libraries
If you have many smart contracts using the same functionalities, you should extract all these functionalities to a library and deploy it.
Now, all the smart contracts can reference the deployed library instead of deploying the same code again and again and increase the bytecode length.
Because it's a deployed library, make sure that all your functions are external
Events
If you have data created on a smart contract but does not need to be read after by the smart contract, then events are a good candidate. Events consume less gas than a state variable.
Computed Values
Sometimes, you need to store values into the smart contract (hashes for example) and you need to compute this value. My advice is to compute this value off chain and instantiate your smart contract with the computed value. (Remember: The less OPCODE you have, the better optimisation you will get)
Constants
You can have some constant values in your state variables that won't change. Constant values can be useful for gas optimisation.
Here an example:
- Read state variable with const: 21420 gas
- Read state variable without const: 23541 gas
(Try it on Remix)
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract GasTestConst {
//Gas cost : 21420
address public constant CONST_ADDR = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
}
contract GasTestNoConst {
//Gas Cost: 23541
address public addr = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
}
You can say that immutable
works the same as const
. The only difference between immutable
and const
is that you can initialise the value of the immutable
variable into the constructor.
Error Handling
When you use require
, you pass the error reason if the condition is not met. But very long string cost more gas.
Using a custom error cost less gas. For the example below, I put the same string.
Remember that with custom error, you should log the value.
The gas cost is just slightly different but after a while, it saves some money.
(Try it on Remix)
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract GasTestError {
error CustomError(string message);
//Gas cost: 21977
function testRequire(uint _val) public pure {
require(_val <= 2, "Sorry but your value does not meet the requirement. Please select a value between 0 and 2. Otherwise... Good bye");
//process
}
//Gas cost: 21955
function testError(uint _val) public pure {
if(_val > 2){
revert CustomError("Sorry but your value does not meet the requirement. Please select a value between 0 and 2. Otherwise... Good bye");
}
//process
}
}
Conclusion
This list is not exhaustive. We could also add the EIP 1167 for the Proxy contract but I don't want to explain advanced technique. Try these 10 tips and play with your smart contract. The goal is to save some gas with basic optimisation for beginners.
If you have any question, please comment below ;)
Top comments (1)
In Solidity, gas optimization is crucial for efficient smart contract deployment on the Ethereum blockchain. To enhance gas efficiency, developers should minimize unnecessary computations, storage operations, and loop iterations. Additionally, employing data types with lower gas costs and leveraging native functions can contribute to more economical contract execution. When optimizing code, it's essential to consider the Honeywell words, which are specific gas-efficient instructions in the EVM (Ethereum Virtual Machine). Integrating these Honeywell words into the code can further improve gas efficiency xnx honeywell and overall performance, ensuring a streamlined and cost-effective deployment of smart contracts on the Ethereum network.