Ethereum Gas Optimization Techniques
Tags: ethereum, gas, solidity, blockchain
Published: 2024-02-07 05:09:29

## Gas Units:
- Making transactions on Ethereum or any other Ethereum backed blockchain will cost you a gas fee. The total gas fee consists of two parts: `Gas Units x Gas Price`

- Gas units are a number that depends on the amount of computation required for a transaction. E.g. If you send some Ether (Ethereum’s native cryptocurrency) to someone, it requires `21,000 gas units`. It’s the `minimum number of units required for any transaction`.

- The number of required gas units increases as the complexity of the transaction increases. E.g. Sending an `ERC-20 token` might cost around `60,000 gas units`. `Minting an NFT might cost around 120,000 gas units`, and so on.

- It all depends on how many computations need to be made while processing the transaction.

## Gas Price:
- Gas price is determined by the demand for making transactions. The more the traffic, the higher the price.

- It’s usually expressed in `Gwei` which is a billionth part of an Ether. If the gas price is 100 Gwei, it means 1 gas unit currently costs 100 Gwei as per the demand. The gas price will vary throughout the day depending on the total traffic.

---

- Let’s combine these two now. If you need to send some Ether to someone at a time when gas price for 1 gas unit is 100 Gwei, you’ll have to pay a total of: `21,000 gas units x 100 Gwei = 2,100,000 Gwei`.

- Since Gwei is `1/1bn of Ether, you’ll pay 0.0021 Ether`.

- To find the amount in dollars, multiply it with the current price of ether i.e. 1200 USD. Hence `0.0021 Ether x 1200 USD = 2.52 USD` total gas fee. To summarise, the gas fee depends on: 
  - Complexity of transaction (gas unit)
  - Demand for making transactions (gas price)
  - Price of Ether (to calculate dollar value)

---

## Where does my gas fee go?
- So, we know that we need to pay a gas fee when making a transaction. But where does it go now? Is it burnt? Or is it given to the validator?
- Actually, it’s both, after the `EIP-1559` (an upgrade to Ethereum that happened in September 2022).
- Total gas fee is made up of:
  - **_Base Fee_** (fixed for every transaction of a block) – which gets burnt entirely.
  - **_Max Priority Fee_** (decided by the user) – which goes to the validator partially or entirely depending on the Max Total Fee which is again decided by the user.
- Let’s run through an example:
  - Suppose a user wants to pay a Max Total Fee of `100 Gwei` per `gas unit consumed` for the transaction. It’s inclusive of the Base Fee (burnt) and Max Priority Fee (for validator).

  - While making the transaction, the user doesn’t know what the exact Base Fee will be for the transaction/block.

  - During peak time, a lot of people will be trying to make a transaction, thus congesting the network. This will cause your transaction to take a `long time before going through`. In order to prioritise his/her transaction, a user may want to `pay some (extra) fee to the block validator`. This fee is called the `Max Priority Fee`. Suppose the user wants it to be 30 Gwei per gas unit.

  - Hence, the user enters the values of `100 Gwei` and `30 Gwei` respectively.

  - When the block is confirmed, there are 2 possibilities depending on what is the final value of Base Fee:

```
Case 1: Base Fee + Max Priority Fee <= Max Total Fee

    E.g.

    Base Fee = 60, Max Priority Fee = 30, Max Total Fee = 100

    Hence,

        - 60 Gwei is burnt, (per gas unit)

        - 30 Gwei goes to the validator, (per gas unit)

        - The remaining 10 Gwei is refunded to the user (per gas unit)
```

```
Case 2: Base Fee + Max Priority Fee > Max Total Fee

    E.g.

    Base Fee = 80, Max Priority Fee = 30, Max Total Fee = 100

    Hence,

        - 80 Gwei is burnt, (per gas unit)

        - Only 20 Gwei goes to the validator (per gas unit), since Max Total Fee is 100 Gwei

        - 0 refund for the user since the max limit is consumed.
```

- Note: `Base Fee` can’t be >= `Max Total Fee`. Your Metamask wallet will let you know if the Max Total Fee entered by you is not sufficient as per the network conditions.

---

## How to save Gas Cost:
Important: This document contains useful information to optimize your Gas Cost in Ethereum Blockchain. The following document contains `90%` of the gas optimization techniques in Ethereum.
  - We will see which method is useful and which method is useless. (or event harmful)

  - We will see how much gas you can save by using each method.

  - We will rank all the methods by using a tier list. (`A = excellent, D= useless/harmful`)

**1.** **Deployment optimization** vs **Call optimization**:

At first, you need to know that there are 2 ways to optimize gas on a Decentralized Application.

_**A).**_ **_Optimizing Deployment costs_**:
  - You can optimize the cost to deploy a smart contract.

  - The `cost = 21000 gas (for creating transaction) + 32000 gas (for creating a contract) + CONSTRUCTOR EXECUTION + BYTES DEPLOYED * 78.125`

  - So, for deploying a smart contract, you will spend `at least 53000 gas`.

  - To minimize gas costs you need to `minimize the size of a smart contract and the cost of the constructor`.

**_B). Optimize every function call:_**

  - You can also optimize the `cost of calling every function` in your smart contract.

  - The cost is equal to` 21000 gas` (for creating transaction) + the function execution cost).

  - So, the objective will be to reduce the function execution cost.

_Note that these 2 means of optimization are not the same!!_

- A lot of times, you may deploy a larger solidity code (so with higher deployment gas cost) but more optimized for every function call. (So with a smaller calling gas cost)

- If you need to choose which type of optimization, you need to use it wisely.

- I strongly advise you to `optimize every function call` as the `deployment is made only one time` and the `user experience will be enhanced`. (unless your smart contract `size exceeds the limit of 24Kb`)

---

**2. Ranking Gas optimization methods:**

- We will rank all the gas optimization methods by using a tier list. (`Because there is a difference between saving 2 gas on a transaction of 21000 gas and saving 10000 gas.`)

  - **Tier A:** If you don’t know these rules, you’re not a _TRUE_ solidity developer.

  - **Tier B:** Essential, should be known by all the developers.

  - **Tier C:** may be useful sometime, should be known by developers too.

  -  **Tier D:** Useless/harmful, don’t use them (unless you know what you are doing). You may lose, either your time or the security of the smart contract.

---

- **_Tier A:_** 

> **_A1 : Knowing how much each `EVM opcodes` cost:_**

- Here is the 6 of the `most costly` gas opcodes (among the most used is smart contracts):
  - **_CREATE/CREATE2_** -> Deploys a smart contract. (`32,000 gas`)

  - **_SSTORE_** -> Stores a value in storage. (`20,000 gas` if the slot hasn’t been accessed, `2900 `otherwise.)

  - **_EXTCODESIZE_** -> Get the size in bytes of a smart contract code. (`2600` if not accessed before, `100` gas otherwise)

  - **_BALANCE_** (`address(this).balance`), same as `EXTCODESIZE`.

  - **_SLOAD_** -> Access a value in storage. (`2100 gas` if not accessed before `100 gas` otherwise)

  - **_LOG4_** -> Create an event with `4 topics`. (`1875 gas`)

- (This is in fact an estimation. Some of them may vary depending on the situation.)
- The best website which describes how much each opcode is https://www.evm.codes/, it’s very well explained.
- If you know that, you’ll better understand solidity and you’ll get a “better” intuition of how much each line of code costs you.

> _**A2: Use `view` the modifier:**_
 

```
function getBalance() external view {
                return balance;
        }
```
- You can easily cut your gas costs to 0 `when you call the smart contract if you don’t write to the storage`. (moreover, you don’t need to wait `5–20 seconds the response`)
- (A3 Understand the solidity programming language & A4 Become good at computer science/algorithms)

---
* **_Tier B:_**

These tips and tricks can save you a good amount of gas and should be implemented as much as possible. _IMPORTANT NOTE BEFORE STARTING:_
- As the `not_optimized()` function is situated before `optimized()` function in the byte code, Calling `optimized()` costs `more than not_optimized()` with the same code (`22 more gas`), so I will subtract `22 gas` on each call to` optimized() function`.

> **_B1: Batching operations (Saved gas: `21000 gas * batched transactions`):_**


- Submitting a transaction alone on the blockchain costs a lot of gas (`21,000` gas to be precise), so if you can find a way to `batch transactions` for your users, it can save you a good amount of gas.

> **_B2: change `orders` of storage slot (`20,000` gas saved at deployment):_**

- Ethereum storage is composed of `slots of 32 bytes`, the problem is that writing is expensive. (Up to `20,000 gas` using “cold” writing)
- Let’s say you have 3 variables:
               

```

    // These vars use 3 different storage slots
         unit128 a; // slot 0(16 bytes)
         unit256 b; // slot 1(32 bytes)
         unit128 c; // slot 2(16 bytes)

    // These var use 2 different storage slots
         unit256 b; // slot 0(32 bytes)
         unit128 a; // slot 1(16 bytes)
         unit128 c; // slot 1(16 bytes)
```
- But if we align the `3 slots well`, we can economize `1 slot`. (`a` and `c` variable will be in the `same slot`)

- Therefore, there are `1` less used slot at the deployment. (So `20,000 gas` saved)

- Moreover, if you want to access the `c` variable, but the `b` variable is already accessed. It will count as a warm access (`accessing a storage slot already accessed`) so it will cost you `1000 `instead of `2900 gas` which is pretty significant.

- If you don’t understand the storage, the documentation may help you: https://docs.soliditylang.org/en/v0.8.13/internals/layout_in_storage.html

> **_B3: Use the `optimizer` (Saved gas: `10–50%` on deployment & call):_**

- You can use the solidity `built-in optimizer`.
- This is a very simple way to save a lot of gas without learning any new concepts. You need to check the “`enable optimization`” checkbox under Advance configuration section.
- A value near to `1` will optimize gas cost, but a value near to the `max (2³²-1)` will optimize calls to the function. In most of the cases you can use the `default value (200)`

> **_B4: use `mapping` instead of `array` (`5000 gas` saved per value):_**

- Mappings are usually less expensive than arrays, but you can’t iterate over them.
                

```
      mapping(unit => unit) map;
      unit[] map2;

      function optimized() external{
          map[1] = 10;
      }

       function not_optimized() external{
          map2.push(10);
      }
```
- `not_optimised()` function gas usage: `48,409 gas`
- `optimised()` function gas usage: `43,400 gas`
       
> **_B5: Use `require` instead of `assert`:_**

- `Assert` should NOT be used by other means than testing purposes. (because when the `assertion fails, gas is NOT refunded` contrary to require)

> **_B6: Use `self-destruct` (save up to `24,000` gas):_**

- `selfdestruct()` function destroys the smart contract and refunds `24,000 gas`.
- In a more complex transaction like deploying another smart contract, you can call `selfdestruct()` on the smart contract to economize some gas.

> **_B7: Make fewer `external` calls (amount saved: variable):_**

- Make a call to another contract only when you’re obligated to do so.

> **_B8: Look for `dead code` (saves a variable amount of gas on deployment):_**

- Sometimes, developers forget to remove useless code like:

```
      require(a == 0)
      if (a == 0) {
         return true;
      } else {
          return false
      }
```
- This is a dump example because it shows you that some devs can forget to remove `useless parts`.

> **_B9: use `immutable` variables (saves `15,000` gas on deployment)":_**

- Use immutable storage variables whenever it’s possible.

```
       contract not_optimised{
       unit256 public a;
       constructor(unit256 _a) public{
             a = _a;
         } 
      }
```

```
       contract optimised{
       unit256 public immutable a;
       constructor(unit256 _a) public{
            a = _a;
         } 
       }
```
- `not_optimised `contract gas usage: `11,6800` gas
- `optimised` contract gas usage: `10,1013` gas

> **_B10: Storing data in `events` (up to `20,000` gas saved on function call):_**

- If you don’t need to access the data on-chain in solidity, you can store it using events.

```
                address store;
                event Store(uint256 indexed key, 
                            address indexed data);

                function optimized() external{
                    emit Store(1, 0xaaC532..);
                }

                function not_optimized() external{
                    store = 0xaaC532..;
                }
```
- `not_optimised()` function gas usage: `43,353` gas
- `optimised()` function gas usage: `22,747` gas

---

- **_Tier C:_**

- These tips can save you a fair amount of gas and cost you nothing.

> **_C1: Use `static size` type in solidity (about `50` gas saved):_**

- Static size types (like `bool`, `uint256`, `bytes5`) are cheaper than dynamic size type (like `string` or `bytes`).

```
                function optimized() external{
                    bytes4 str = 0x61626364;
                }

                function not_optimized() external{
                     bytes4 memory str = "abcd";
                }
```
-  `not_optimised()` function gas usage: `21,255` gas
-  `optimised()` function gas usage: `21,227` gas

> **_C2: `Cold access` vs `warm access` (`70` saved gas):_**

- Don’t access the same storage variable twice.

```
                uint256 a = 100;
                function optimized() external{
                    uint tmp =  a;
                    uint b   =  tmp;
                    uint c   =  tmp;
                }

                function not_optimized() external{
                     uint b   =  a;
                     uint c   =  a;
                }
```
- `not_optimised()` function gas usage: `23,412` gas
- `optimised()` function gas usage: `23,347` gas

> **_C3: Using `calldata` instead of `memory` (`450` saved gas per call):_**

```
        string str;
        function optimized(string calldata _str) external{

                }

        function not_optimized(string memory _str) external{

                }
```
- `not_optimised()` function gas usage: `22,442` gas
- `optimised()` function gas usage: `21,994` gas

> **_C4: Use `Indexed events` (`62` gas saved on function call per topic):_**

- You can mark each event as `indexed` as bellow:
- Indexed events allow events to be `searched` more easily.

```
   event Test1(address a, address b);
   event Test2(address indexed a, address indexed b);

   function optimized(string calldata _str) external{

        }

    function not_optimized() external {
            emit Test1(0x..,0x..)
        }
    function optimized() external {
            emit Test2(0x..,0x..)
        }
```
- Here the function `optimised()` uses `135` less gas than `not_optimised()`.

> **_C5: Changing the `order of calls` (`20–2000` saved gas per call):_**

- We can call every smart contract, you supply in `msg.data` the signature of the function. (the first `4 bytes` of the function `keccak256` hash).

- Firstly, the EVM will do a “`switch`” of this signature, to see which function to execute.

```
      switch(msg.data[0:4]) { // compare to signature
        case 0x01234567: go to functionA;

        // cost 22 more gas..
        case 0x11111111: go to functionB;

        // cost 22+22 more gas... 
        case 0x4913aaaa: go to functionC; 
              ...
      }
```
- As `comparing to signature computation` use a bit of gas (`22` gas), you need to place the most called function in the first place of the `switch()` by assuring that the signature is in the first place (by changing its name)

- You can learn more about this par is my reversing EVM articles: https://trustchain.medium.com/reversing-and-debugging-evm-smart-contracts-392fdadef32d

> **_C6: Use `i++` instead of `i = i+1` (`62` gas saved on each call):_**

- This sounds like a joke, but it seems to work.:

```
               function optimized() external{
                    uint i = 10;
                    i++;
                }
                function not_optimized() external{
                    uint i = 10;
                    i = i + 1;
                }
```
- `not_optimised()` function gas usage: `21,401` gas
- `optimised()` function gas usage: `21,339` gas

> **_C7: Use `uint256` instead of `uintXX` on storage (`55` gas saved per call):_**

- To `write/access` data on storage, the EVM uses `32 bytes (= 256 bits slots)`, by using smaller types than uint256, the EVM needs to perform additional operations to assure the conversion. So better to use `uint256` instead of `uint8` for storing data.

```
                uint256 balance;
                uint8 balance2;
                function not_optimized() external{
                    balance2 = 10;
                }
                function optimized() external{
                    balance = 10;
                }
```
- `not_optimised()` function gas usage: `43,353` gas
- `optimised()` function gas usage: `43,298` gas
    
> **_C8: Create a `custom error` (`24` gas saved per call):_**

- You can create a custom error on solidity and revert with it like below:

```
                error err(string test);
                function not_optimized() external{
                   revert("test");
                }
                function optimized() external{
                    revert err("test");
                }
```
- `not_optimised()` function gas usage: `21,476` gas
- `optimised()` function gas usage: `21,474` gas
    
> **_C9: Exchanging 2 variables by using a `tuple (a, b) = (b, a)` (saves `5` gas on every call):_**

```
               function not_optimized() external{
                   uint i = 10 ;
                   uint j = 20 ;
                   uint temp = i ;
                   i = j ;
                   j = temp;
                }
                function optimized() external{
                    uint i = 10;
                    uint j = 20 ;
                    (i,j) = (j,i);
                }
```
- `not_optimised()` function gas usage: `21,241` gas
- `optimised()` function gas usage: `21,236` gas
- We put it on the C tier because the notation is way more readable.

> **_C10: Use Calldata instead of Memory in function arguments in case of not changing the passed data:_**

- Using calldata in a summing function that loops over an input array can save about 1829 gas (or 3.5%) on average.


```
   function not_optimised(uint[] memory nums) external {
     for (uint i = 0; i < nums.length; ++i){
        ...
    }
}

   function optimised(uint[] calldata nums) external {
     for (uint i = 0; i < nums.length; ++i){
        ...
   }
}


```

- `not_optimised()` function gas usage: `50,992` gas
- `optimised()` function gas usage: `49,163` gas

---

- **_Tier D: (Useless/harmful)_**

These optimizing gas tips are useless, don’t lose your time for saving a low amount of gas it will be shadowed anyway by the `21,000 `gas transaction.

> **_D1: using `unchecked` (gas saved: `150`/operation):_**

- Since solidity 0.8.0, operations like + * / — are checked every time if there is an `overflow/underflow`.

- But it cost a little bit of gas to verify if there is an overflow/underflow.

- Unless you are doing a lot of operations in a single function call (like in for loops), you should NOT use `unchecked{}`.

```
               function not_optimized() external{
                   uint i = 10 ;
                   uint j = 20 ;
                   uint add;
                   add = a + b;
                }
                function optimized() external{
                   uint i = 10 ;
                   uint j = 20 ;
                   uint add;
                   unchecked {c = a + b};
                }
```
- `not_optimised()` function gas usage: `21,419` gas
- `optimised()` function gas usage: `21253` gas

> **_D2: changing and optimizing the `bytecode` yourself:_**

- Don’t do this unless you have a good reason to do so, some of the functions may end up with undefined behavior and security issues.
- Use the optimizer instead or implement a very strict testing policy.

> **_D3: Delete `Errors string` messages (about `78.125` gas saved on deployment cost per characters):_**

- Don’t do it, the code may be harder to debug for you and for users. 
- For example, someone can replace:

```
         require(account != address(0), 
           "ERC20: mint to the zero address");
    To:-
         require(account != address(0))
```

> **_D4: using `low level assembly` (gas saved: variable):_**

- If you don't know what you’re doing, `DON’T do solidity assembly`. You will end up having a lot more chances to have a security hole on your contract for a marginal saved gas amount.

> **_D5: Use `external` instead of `public`:_**

- Surprisingly they are not any difference between `external` and `public` functions,

```
       function not_optimized() public returns (uint){
                 return 777;
             }
       function optimized() external returns(uint){
                return 777;
           }
```
- `not_optimised()` function gas usage: `21,401` gas
- `optimised()` function gas usage: `21,379` gas (22)

> **_D6: Use `left shift` instead of `multiplication` >> (150 saved gas):_**

- `Shifting binary data n times to the left results in multiplying the data by 2^n`. Here is an example:
           

```
      00000001 = 1 dec
      00000010 = 2 dec
      00000100 = 4 dec
      00001000 = 8 dec

     function not_optimized()  external returns (uint){
                   uint a = 5;
                   uint b = a * 1024;
                   return b;
              }

     function optimized() external returns(uint){
                   uint a = 5;
                   uint b = a >> 10;
                   return b;
              }

```
- `not_optimised()` function gas usage: `21,615` gas
- `optimised()` function gas usage: `21,436` gas
- Gains are not bad, but the `>>` operator doesn't check for an overflow.

> **_D7: Delete `metadata hash` (`3000–5000` gas on deployment):_**

- On every smart contract byte code a “`hash`” is appended in the end.
- This is the `hash of all the metadata of the smart contract`. (Including `abi`, `comments`, `code`…)
- As the metadata hash is `32 bytes` in length, it can save you a bunch of gas.
- But this is quite hard to do, because you have to do it by hand.
- So it may be dangerous if executed badly.

> **_D8: Add `payable` to every function (`24` gas saved per call):_**

- Adding `payable` to a function, removes the verification on `msg.value`. Therefore, some gas is saved.

```
              function optimized() external payable{
                  ...
                }

              function not_optimized() external{
                  ...
                }
```
- `not_optimised()` function gas usage: `21,186` gas
- `optimised()` function gas usage: `21,184` gas

> **_D9: Doing the important work `offchain`:_**

- Be careful about the work you’re doing off-chain (Like in JS).
- For example, you `can’t do security verification off-chain` in the website front end, this is very dangerous as anyone may be able to change the JS code in the browser and send bad input to the smart contract.
- **`ALWAYS DO THE SECURITY VERIFICATION ON-CHAIN`**.!!!

### Some other gas saving techniques: -
    - Loading state variable to memory
    - Replace for loop i++ with ++i
    - Caching array elements
    - Short circuit
### Useful links:  https://www.cyfrin.io/blog/solidity-gas-optimization-tips