Gas
is a unit of computational measure in Solidity. This is the amount that we pay to interact and transact with a smart contracts. The gas
is calculated via a simple formulae which is:
Gas = Gas Price * Gas used
.
The gas
price is not constant and it is dependent on the congestion of the network. The more users pushing transaction through the network the higher the gas
price. The amount of gas
used is determined by the operation performed by the smart contract.
This blog post will discuss some techniques we might employ to reduce the gas
spent when sending transaction to a smart contract. We will consider the following listed techniques to reduce the gas
foot print of a smart contract.
- Use constant and immutable variables for variable that don't change
- Cache read variables in memory
- Incrementing and decrementing by 1
- calldata and memory
- Calling view function
- Integer overflow/underflow
- Use revert instead of require
- Avoid loading too many data in memory
- Other tips
Use constant and immutable variables for variable that don't change
Using the constant
and the immutable
keywords for variables that do not change helps to save on gas
used. The reason been that constant
and immutable
variables do not occupy a storage slot when compiled. They are saved inside the contract byte code. Lets see an example.
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.3;
interface IERC20 {
function transfer(address to, uint256 value) external returns (bool);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
//rest of the interface code
}
//Gas used: 187302
contract Expensive {
IERC20 public token;
uint256 public timeStamp = 566777;
constructor(address token_address) {
token = IERC20();
}
}
//Gas used: 142890
contract GasSaver {
IERC20 public immutable i_token;
uint256 public constant TIMESTAMP = 566777;
constructor(address token_address) {
i_token = IERC20(token_address);
}
}
The first contract Expensive
has a public variable of type IERC20
and a uint256
also defined as public. When we deployed this contract, the contract deployment used up a total of 187302 gas
when deployed to Remix. The second contract named GasSaver
also have the same variable structure as the Expensive
contract but we made use of constant
keyword for the TIMESTAMP
because we will not be changing the value. For the interface
we used the immutable
keyword which means we can no longer modify the value of the i_token
variable after setting it inside the constructor
. This action alone reduced the gas consumption from 187302 to 142890.
Cache read variables in memory
Reading from a variable in storage cost gas
. When we access a variable for the first time it cost 2100 gas
and subsequent access will cost 100 gas
. If we have an array and want to perform some computation on the array elements. It could be more gas effective to read the length
of the array and store it in a variable. If the array is not too large we could also read the whole array
into memory and access it from memory instead of continuously reading from storage.
//SPDX-License-Identifier:MIT;
pragma solidity ^0.8.3;
contract Expensive {
uint256[] public numbers;
constructor(uint256[] memory _numbers) {
numbers = _numbers;
}
//Gas used: 40146
function sum() public view returns (uint256 ){
uint256 total = 0;
for (uint256 i=0; i < numbers.length; i++) {
total += numbers[i];
}
return total;
}
}
contract GasSaver {
uint256[] public numbers;
constructor(uint256[] memory _numbers) {
numbers = _numbers;
}
//39434
function sum() public view returns (uint256 ){
uint256 total = 0;
uint256 arrLength = numbers.length;
uint256[] memory _numbersInMemory = numbers;
for (uint256 i=0; i < arrLength; i++) {
total += _numbersInMemory[i];
}
return total;
}
}
The contract GasSaver
above saved the length of the array in a variable arrLength
and also loaded the array into memory. This helps to reduce the gas
spent inside the function.
Incrementing and Decrementing by 1
There are four ways to increment and decrement a variable by 1 in Solidity. Let us see an example.
//SPDX-License-Identifier:MIT;
pragma solidity ^0.8.3;
contract One{
uint256 public number;
//Gas used : 43800
function incrementByOne() public returns (uint256){
number += 1;
return number;
}
}
contract Two{
uint256 public number;
//Gas used : 43787
function incrementByOne() public returns (uint256){
number = number + 1;
return number;
}
}
contract Three{
uint256 public number;
//Gas used : 43634
function incrementByOne() public returns (uint256){
return number++;
}
}
contract Four{
uint256 public number;
//Gas used : 43628
function incrementByOne() public returns (uint256){
return ++number;
}
}
The example above list four contracts which shows the way we could increment a number variable by a value of 1. The contract Four
is more gas effective out of the lot when ran. To save more on gas
when incrementing a variable use the method of increment in contract Four
(++number
) which is more gas
efficient.
calldata and memory
When running a function we could pass the function parameters as calldata
or memory
for variables such as strings
, structs
,arrays
etc. If we are not modifying the passed parameter we should pass it as calldata
because calldata
is more gas
efficient than memory
. Let's see an example:
//SPDX-License-Identifier:MIT;
pragma solidity ^0.8.3;
contract GasSaver {
//Gas used: 22471
function passParameterAsCallData(string calldata _name) public returns (string memory){}
//Gas used: 22913
function passParameterAsMemory(string memory _name) public returns (string memory){}
}
Calling these two functions in Remix and passing the same parameter to the functions, you will noticed that setting a function _name
parameter as calldata
reduced the gas
used.
You can only used
calldata
when you are not going to modify the value of the variable passed ascalldata
inside the function.
Calling a view
function
A view
function does not use gas
when called, but if we decide to call a view function inside of another function which is a transaction, it then uses gas
.
//SPDX-License-Identifier:MIT;
pragma solidity ^0.8.3;
contract GasSaver {
uint256[] private numbers = [2,3,5,67,34];
function getNumberAt( uint256 _index ) public view returns (uint256){
return numbers[_index];
}
//Gas used when getNumber was called: 44778
//Gas used without calling getNumber() 44450
function sumAndMultiply() public {
uint256[] memory _numbers = numbers;
uint256 arrlength = _numbers.length;
for(uint256 i=0; i < arrlength; ++i){
numbers[i] = _numbers[i] * i;
}
//getNumberAt(2);
}
}
The above is a simple contract to illustrate the point about calling view
functions inside of a transaction. The function getNumberAt
is a view
function that returns the value of the array at the passed index. Calling this function uses no gas
because it is a view function, but if we decide to call this function inside of the sumAndMultiply
function, we can see that the gas
usage of sumAndMultiply
has increased because the view
function is called within it.
Gas
used when sumAndMutiply
functions is called: 44778
.
Gas
used when getNumberAt
view
function is also called inside the sumAndMultiply
function: 44450
.
So, we can see that calling a view
function inside a transaction increased the amount of gas
that should have been spent, if the view
function was not included in the call.
Integer overflow/underflow
Prior to Solidity 0.8.0
version, interger overflow and underflow checks were performed by using the SafeMath
library. From Solidity 0.8.0
upward, the compiler does that check for us.
This extra check cost gas
. If we know that the mathematical operations we will perform inside the contract will not overflow or underflow, we can tell the compiler not to check for overflow or underflow in the operation.
//SPDX-License-Identifier:MIT;
pragma solidity ^0.8.3;
contract OverFlow {
uint256 public numberOne;
uint256 public numberTwo;
//Gas used: 43440
function setNumberOne() public {
++numberOne;
}
//Gas used: 43339
function setNumberTwo() public {
unchecked {
++numberTwo;
}
}
}
The above contract OverFlow
has two functions used to set two variables inside the contract. The compiler in setNumberOne
checks and prevents the number from overflow or underflow. In the second function setNumberTwo
we are telling the compiler to mind its business and not check for overflow or underflow. Using unchecked
means that the code block will not be checked for overflow or underflow.
If we know that our mathematics will be safe we could safe a little gas by using unchecked
.
Use revert instead of require
We use require
to check if a statement is true
. If the statement evaluates to false the transaction is reverted and the remaining gas returned to the user. Since the advert of custom error in Solidity we could use the revert
statement to throw a custom error. Using revert
instead of require
is more gas efficient. You can run the code below to see the the amount of gas
saved when revert
is used instead of require
.
//SPDX-License-Identifier:MIT;
pragma solidity ^0.8.3;
error NotEnough();
contract GasSaver {
uint256 public number;
//Gas Used: 21898
function setNumber(uint256 _number) public {
require(_number > 10, "number too small please");
number = _number;
}
//Gas Used: 21669
function setNumberAndRevert(uint256 _number) public {
if ( _number < 10 ){
revert NotEnough();
}
number = _number;
}
}
Avoid loading too many data in memory
Loading data in memory in a single transaction has the effect of increasing gas
used. The gas
used increase in a quadratic factor when more than 32kb
of memory is used in a single transaction. Let's see an example.
//SPDX-License-Identifier:MIT;
pragma solidity ^0.8.3;
contract One {
//Gas used: 29261
function setArrayInMemory() public pure {
uint256[1000] memory _array;
}
}
contract Two {
//Gas used: 276761
function setArrayInMemory() public pure {
uint256[10000] memory _array;
}
}
//Gas used: 922855
contract Three {
function setArrayInMemory() public pure {
uint256[20000] memory _array;
}
}
contract Four {
//Gas used: 3386918
function setArrayInMemory() public pure {
uint256[40000] memory _array;
}
}
The above example defined four contracts and each contract has a function named setArrayInMemory
where memory space is reserved for an array of uint256
.
Contract One
loads reserves space in memory for 1000 uint256
numbers and when the function is executed it uses up 29261 gas
. Contract Two
creates memory space for 10000 uint256
numbers and we can see that the gas
used is 276761 which is roughly about 10 times of the gas
used by Contract One
.
Things gets interesting in Contract Three
which reserves space for 20000 uint256
numbers. From the gas
used which is 922855, we can see that once the memory used in a single transaction exceeds 32 kilobyte, the quadratic factor of the memory expansion cost kicks in thereby increasing the gas
used in a quadratic way.
Do not populate arrays with enormous amounts of items in a single transaction to prevent
gas
used increase in a quadratic way.
Other tips
Always put your
require
statement on top in the function before making any state change so that if therequire
statement fails, the remaininggas
is refunded to the user on revert.When comparing operation using
&&
or||
with arequire
statement; you should put the cheaper part of the operation first so that if it fails, the compiler will not compare the second part thereby saving ongas
used.
In conclusion We can use a combination of these techniques highlighted here to improve the gas
consumption of our smart contracts thereby saving our users from spending too much on gas
when interacting with our smart contracts.
Thanks for reading.
Top comments (0)