In this article, we are going to discuss some tips and practices to save gas while writing solidity code.
What Exactly Is Gas In The Ethereum Blockchain?
Each transaction in the ethereum blockchain requires computational resources to execute. Therefore, each transaction requires a fee, which is called gas (or gas fee).
A gas fee is required while executing the solidity code. It is measured in units of "Wei", similar to how we require gas to travel from one place to another.
If you want to add two numbers on the blockchain you need to pay three units of gas and to get the balance of the account it will cost 400 units of gas. More about the costs for various transactions can be found in the ethereum yellowpaper
To calculate the transaction fee, we must multiply the gas cost by the gas price.
Total cost = gasUsed * gasPrice
gasUsed is the total gas that is consumed by the transaction i.e combining amount of executed opcodes.
gasPrice specified in the transaction
What Is The Gas Price?
Gas price is the amount of ether you pay for every unit of gas. Gas price is not fixed, similar to fuel price. It is measured in units of Wei or gwei.
The exact price of the gas is determined by the supply and demand between the network's miners.
1 gwei = 1000000000 wei
1 ether = 1000000000 gwei
The Ethereum Average Gas Price Chart:
The following chart signifies the average gas price for transactions in gwei across time.
Source -etherscan.io
Hence it becomes necessary to make sure the gas consumed by the smart contract is as less as possible.
Tips To Save Gas When Writing A Smart Contract In Solidity.
1. Minimize Reading And Writing On-Chain Data
Reading and writing in memory is cheaper than saving in the storage.
Let's take the following example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract SaveGas {
uint public var1 = 70;
function testFunction1() external view returns (uint) {
uint sum = 0;
for (uint i = 0; i < var1; i++) {
sum += i;
}
return sum;
}
function testFunction2() external view returns (uint) {
uint sum = 0;
uint _var1 = 70;
for (uint i = 0; i < _var1; i++) {
sum += i;
}
return sum;
}
}
In the function test1, we have a loop that reads the value of var1 directly from storage. While in the function test2, we have a loop that reads the value of _var1 from memory which is cheaper than reading it from storage, hence saving gas.
Values of var1 | gas consumed by testFunction2() | gas consumed by testFunction1() | Gas saved (in %) |
5 | 24348 | 24837 | 1.97% |
10 | 25308 | 26297 | 3.76% |
15 | 26268 | 27757 | 5.36% |
20 | 27228 | 29217 | 6.81% |
50 | 32988 | 37977 | 13.14% |
70 | 36828 | 43817 | 15.95% |
Similarly, we can take the below example where arrays are used to read and write from the blockchain:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.7;
contract Gas_Test{
uint[] public arrayOfNumbers;
uint public total;
constructor() {
arrayOfNumbers = [1,2,3,4,5,6,7,8,9,10,11,12,13];
}
function optionA() external {
for (uint i =0; i < arrayOfNumbers.length; i++){
total = total + arrayOfNumbers[i];
}
}
function optionC() external {
uint _total;
uint[] memory _arrayOfNumbers = arrayOfNumbers;
for (uint i =0; i < _arrayOfNumbers.length; i++){
_total = _total + _arrayOfNumbers[i];
}
total = _total;
}
}
In the first function, we're reading and writing directly onto the storage, while in the other function, we're caching the variables. This saves us a significant amount of gas.
2. Turn On Solidity Optimiser
The Solidity compiler is to produce highly optimized bytecode.
Enable the optimizer by including the following config in the truffle.config.js
file for a truffle configured project or in the hardhat.config.js` for a hardhat configured project.
{ "optimizer": { "enabled": true, "runs": 200 } }
Side note: Make sure to disable this in a development environment, as it will take comparatively more time to execute.
Truffle Config Example - view more in the Truffle Docs
module.exports = {
compilers: {
solc: {
version: <string>, // A version or constraint - Ex. "^0.5.0"
// Can be set to "native" to use a native solc or
// "pragma" which attempts to autodetect compiler versions
docker: <boolean>, // Use a version obtained through docker
parser: "solcjs", // Leverages solc-js purely for speedy parsing
settings: {
optimizer: {
enabled: <boolean>,
runs: <number> // Optimize for how many times you intend to run the code
},
evmVersion: <string> // Default: "istanbul"
},
modelCheckerSettings: {
// contains options for SMTChecker
}
}
}
}
Hardhat Config Example - view more in the Hardhat Docs
module.exports = {
solidity: {
version: "0.8.9",
settings: {
optimizer: {
enabled: true,
runs: 1000,
},
},
},
}
3. Use Events
Events/logs are the results of LOG opcodes being executed in the EVM. Events are a part of "internal transactions" which are derived by executing transaction data through the EVM.
More about how the events are stored and how it's cheaper as compared to storage can be found here
4. Avoid Loops For Dynamic Values
As dynamic values aren't something predictable and not completely in our control, it's always a good idea to avoid performing loops with them. Using these values can create unpredictable loops, costing more gas.
Using static values is always a better option, as it's easier to predict and can be used in a more efficient way.
5. Combine Variables Also Known As Variable Packing
Solidity variables store data in 256-bit memory slots, which means the data that does not fit in a single slot, occupies the next slot.
Lets take an example of integers:
Instead of
uint128 x;
uint256 y;
uint128 z;
If we use the following approach, variables x
and y
will be packed into one 256byte slot.
uint128 x;
uint128 y;
uint256 z;
Conclusion
This article covered how saving gas while writing smart contracts becomes essential in the development process as it helps for cheaper transactions saving ๐ธ on transactions. Use the above tips and optimize your next solidity smart contract.
Hope you enjoyed reading this article and found it helpful. Cheers! ๐ฅ