Home

Awesome

Solidity Gas Optimization Tips

Solidity Gas Optimization Tips taken from various sources (sources listed in the #credits section below). Make pull requests to contribute gas alpha.

Gas Optimizations in Solidity

1- Use of Bit shift operators

Check if arithmethic operations can be achieved using bitwise operators, if yes, implement same operations using bitwise operators and compare the gas consumed. Usually bitwise logic will be cheaper (ofc we cannot use bitwise everywhere, there some limitations)

Note: Bit shift << and >> operators are not among the arithmetic ones, and thus don’t revert on overflow.

Code example:

-    uint256 alpha = someVar / 256;
-    uint256 beta = someVar % 256;
+    uint256 alpha = someVar >> 8;
+    uint256 beta = someVar & 0xff;


-    uint 256 alpha = delta / 2;
-    uint 256 beta = delta / 4; 
-    uint 256 gamma = delta * 8;
+    uint 256 alpha = delta >> 1;
+    uint 256 beta = delta >> 2; 
+    uint 256 gamma = delta << 3;

2- Public vs External (External is cheaper)

The public visiblilty modifier is equivalent to external plus internal. In other words, both public and external can be called from outside your contract (like MetaMask), but of these two, only public can be called from other functions inside your contract.

Because public grants more access than external (and is costlier than the latter), the general best practice is to prefer external. Then you can consider switching to public if you fully understand the security and design implications.

Code Example:

pragma solidity 0.8.10;

contract Test {
 
    string message = "Hello World";
    
    // Execution cost: 24527
    function test() public view returns (string memory){
         return message;
    }

    //Execution cost: 24505
    function test2() external view returns  (string memory){
         return message;
    }
}

3- There is no need to initialize variables with default values

If the variable is not set/initialized, it is assumed to have a default value (0, false, 0x0, etc., depending on the data type). If you explicitly initialize it with its default value, you are just wasting gas.

uint256 foo = 0;//bad, expensive
uint256 bar;//good, cheap

4- Use short strings in revert, require checks

It is considered a best practice to append the error reason string with require statement. But these strings take up space in the deployed bytecode. Each reason string needs at least 32 bytes, so make sure your string complies with 32 bytes, otherwise it will become more expensive.

Code Example:

require(balance >= amount, "Insufficient balance");//good
require(balance >= amount, "Too bad, it appears, ser you are broke, bye bye"; //bad

5- Avoid redundant checks

We should avoid unnecessary/redundant checks to save some extra gas.

Code Example:

Bad Code:
require(balance>0, "Insufficient balance");
if (balance>0){ // this check is redundant
    . . . //some action
}

Good Code: 
require(balance>0, "Insufficient balance");
. . . //some action

6- Use nested if and, avoid multiple check combinations

Using nested is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.

Code Example:

pragma solidity 0.8.10;

contract NestedIfTest {

    //Execution cost: 22334 gas
  function funcBad(uint256 input) public pure returns (string memory) { 
       if (input<10 && input>0 && input!=6){ 
           return "If condition passed";
       } 

   }

    //Execution cost: 22294 gas
    function funcGood(uint256 input) public pure returns (string memory) { 
    if (input<10) { 
        if (input>0){
            if (input!=6){
                return "If condition passed";
            }
        }
    }
}
}

7- Use of Multiple require(s)

Using mutiple require statements is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.

pragma solidity 0.8.10;

contract MultipleRequire {

    // Execution cost: 21723 gas
  function bad(uint a) public pure returns(uint256) {
        require(a>5 && a>10 && a>15 && a>20);
        return a;
    }

    // Execution cost: 21677 gas
    function good(uint a) public pure returns(uint256) {
        require(a>5);
        require(a>10);
        require(a>15);
        require(a>20);
        return a;
    }
}

8- Internal functions are cheaper to call

Set approriate function visibillities, as internal functions are cheaper to call than public functions.There is no need to mark a function as public if it is only meant to be called internally.

//Following function will only be called interally
- function _func() public {...}
+ function _func() internal {...}

9- Use libraries to save some bytecode

When you call a public function of the library, the bytecode of the function will not become part of your contract, so you can put complex logic in the library while keeping the contract scale small.

The call to the library is made through a delegate call, which means that the library can access the same data and the same permissions as the contract. This means that it is not worth doing for simple tasks.

10- Replace state variable reads and writes within loops with local variable reads and writes.

Reading and writing local variables is cheap, whereas reading and writing state variables that are stored in contract storage is expensive.

function badCode() external {                
  for(uint256 i; i < myArray.length; i++) { // state reads
    myCounter++; // state reads and writes
  }        
}


function goodCode() external {
    uint256 length = myArray.length; // one state read
    uint256 local_mycounter = myCounter; // one state read
    for(uint256 i; i < length; i++) { // local reads
        local_mycounter++; // local reads and writes  
    }
    myCounter = local_mycounter; // one state write
}

11- Packing Structs

A common gas optimization is “packing structs” or “packing storage slots”. This is the action of using smaller types like uint128 and uint96 next to each other in contract storage. When values are read or written in contract storage a full 256 bits are read or written. So if you can pack multiple variables within one 256 bit storage slot then you are cutting the cost to read or write those storage variables in half or more.

// Unoptimized
struct MyStruct {
  uint256 myTime;
  address myAddress;
}

//Optimized
struct MyStruct {
  uint96 myTime;
  address myAddress;
}

In the above a myTime and myAddress state variables take up 256 bits so both values can be read or written in a single state read or write.

12- Solidity Gas Optimizer

Make sure Solidity’s optimizer is enabled. It reduces gas costs. If you want to gas optimize for contract deployment (costs less to deploy a contract) then set the Solidity optimizer at a low number. If you want to optimize for run-time gas costs (when functions are called on a contract) then set the optimizer to a high number.

13- Save on data types

It is better to use uint256 and bytes32 than using uint8 for example. While it seems like uint8 will consume less gas than uint256 it is not true, since the Ethereum virtual Machine(EVM) will still occupy 256 bits, fill 8 bits with the uint variable and fill the extra bites with zeros.

Code Example:

pragma solidity ^0.8.1;

contract SaveGas {
    
    uint8 resulta = 0;
    uint resultb = 0;
    
    // Execution cost: 73446 gas
    function UseUint() external returns (uint) {
        uint selectedRange = 50;
        for (uint i=0; i < selectedRange; i++) {
            resultb += 1;
        }
        return resultb;
    }
    
    // Execution cost: 84175 gas 
    function UseUInt8() external returns (uint8){
        uint8 selectedRange = 50;
        for (uint8 i=0; i < selectedRange; i++) {
            resulta += 1;
        }
        return resulta;
    }
}

14- Short circuiting

Short-circuiting is a strategy we can make use of when an operation makes use of either || or &&. This pattern works by ordering the lower-cost operation first so that the higher-cost operation may be skipped (short-circuited) if the first operation evaluates to true.

// f(x) is low cost
// g(y) is expensive

// Ordering should go as follows
f(x) || g(y)
f(x) && g(y)

15- Avoiding/Removing unnecessary libraries

Libraries are often only imported for a small number of uses, meaning that they can contain a significant amount of code that is redundant to your contract. If you can safely and effectively implement the functionality imported from a library within your contract, it is optimal to do so.

16- Make fewer external calls

External calls are expensive, therefore, fewer external gas == fewer gas cost.

17- unchecked { ++i;} is cheaper than i++; & i=i+1;

Code Example:


// Unoptimized Code

for(uint i=0; i<n; i++){
    . . . //some stuff here
}

//Optimized Code

for(uint i; i<n;){
    . . . //some stuff here
    unchecked { ++i; }
}

18- Avoid repeated computations

Arithmetic computations cost gas, it is recommended to avoid repeated computations.

Code Example:


// Unoptimized code: 
for (uint i=0;i<length;i++) {
tokens[i] += limit * price;
}


// Optimized code:
uint local = limit * price;
for (uint i=0;i<length;i++) {
tokens[i] += local;
}

19- Avoid dead code

There is no point of leaving dead lines in code. Those lines are never going to execute, and but they will take place in bytecode, it is better to get rid of em.

Code Example:

function deadCode(uint x) public pure {
  if(x <1) {
    if(x> 2) {
      return x;
    }
  }
}

20- Delete your variables to get a gas refund.

Since there's no garbage collection, you have to throw away unused data yourself.

Code Example:


//Using delete keyword
delete myVariable;

//Or assigning the value 0 if integer
myInt = 0;

21- Single line swaps

One line to swap two variables without writing a function or temporary variable that needs more gas.

Code Example:

(hello, world) = (world, hello);

22- Store data on memory not storage.

Choosing the perfect Data location is essential. You must know these:

Also, it is important to note that, If not specified data location, then it storage by default.

23- Use calldata instead of memory for function parameters

contract C {
    function add(uint[] memory arr) external returns (uint sum) {
        uint length = arr.length;
        for (uint i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
    }
}

In the above example, the dynamic array arr has the storage location memory. When the function gets called externally, the array values are kept in calldata and copied to memory during ABI decoding (using the opcode calldataload and mstore). And during the for loop, arr[i] accesses the value in memory using a mload. However, for the above example this is inefficient. Consider the following snippet instead:

contract C {
    function add(uint[] calldata arr) external returns (uint sum) {
        uint length = arr.length;
        for (uint i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
    }
}

24- Some state variables can be set to immutable


// Unoptimized code: 
contract C {
    /// The owner is set during contruction time, and never changed afterwards.
    address public owner = msg.sender;
}

// Optimized code: 


contract C {
    /// The owner is set during contruction time, and never changed afterwards.
    address public immutable owner = msg.sender;
}

25- Declaring constructor as payable

You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. The following opcodes are cut out:

In Solidity, this chunk of assembly would mean the following:

if(msg.value != 0) revert();

26- Upgrade to at least 0.8.4

Using newer compiler versions and the optimizer gives gas optimizations and additional safety checks for free.

27- Use != 0 instead of > 0 for unsigned integer comparison

When dealing with unsigned integer types, comparisons with != 0 are cheaper then with > 0.

28- Limit Modifiers

The code of modifiers is inlined inside the modified function, thus adding up size and costing gas. Limit the modifiers. Internal functions are not inlined, but called as separate functions. They are slightly more expensive at run time, but save a lot of redundant bytecode in deployment, if used more than once.

29- Packing booleans

In Solidity, Boolean variables are stored as uint8 (unsigned integer of 8 bits). However, only 1 bit would be enough to store them. If you need up to 32 Booleans together, you can just follow the Packing Variables pattern. If you need more, you will use more slots than actually needed.

Pack Booleans in a single uint256 variable. To this purpose, create functions that pack and unpack the Booleans into and from a single variable. The cost of running these functions is cheaper than the cost of extra Storage.

30- Mappings are cheaper than arrays

Solidity provides only two data types to represents list of data: arrays and maps. Mappings are cheaper, while arrays are packable and iterable.

In order to save gas, it is recommended to use mappings to manage lists of data, unless there is a need to iterate or it is possible to pack data types. This is useful both for Storage and Memory. You can manage an ordered list with a mapping using an integer index as a key.

31- Fixed size variables are cheaper than variable size.

Whenever it is possible to set an upper bound on the size of an array, use a fixed size array instead of a dynamic one.

32- Use Custom errors instead of revert strings

Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)

Code Example:


// Revert Strings 
contract C {
    address payable owner;

    function withdraw() public {
        require(msg.sender == owner, "Unauthorized");

        //..
    }
}


//Custom errors
error Unauthorized();

contract C {
    address payable owner;

    function withdraw() public {
        if (msg.sender != owner)
            revert Unauthorized();

        //..
    }
}

Credits