DEV Community

Jamiebones
Jamiebones

Posted on

Gas Saving Techniques in Solidity

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

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);
   }
}

Enter fullscreen mode Exit fullscreen mode

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;
   }
}
Enter fullscreen mode Exit fullscreen mode

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;
   }
}

Enter fullscreen mode Exit fullscreen mode

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){}
}
Enter fullscreen mode Exit fullscreen mode

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 as calldata 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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
       }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Avoid loading too many data in memory

Loading data in memory in a single transaction has the effect of increasing gasused. 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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 the require statement fails, the remaining gas is refunded to the user on revert.

  • When comparing operation using && or || with a require 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 on gas 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.

Oldest comments (0)