Network
Launch Date
Consensus
Note
Sepolia
Oct 2021
PoW
Like-for-like representation of Ethereum
Görli
Jan 2019
PoA
Proof-of-Authority
Kiln
Mar 2022
PoS
Post-Merge (for ETH2), shadow fork of the mainnet
Kintsugi
Dec 2021
PoS
DEPRECATED, use Kiln; post-Merge (for ETH2)
Ropsten
Nov 2016
PoW
DEPRECATED, use Sepolia; the Merge to happen on Jun 8, 2022
Rinkeby
Apr 2017
PoA
DEPRECATED, use Görli and Görli Faucet
Kovan
Mar 2017
PoA
DEPRECATED, use Sepolia or Görli
List of active and deprecated Ethereum testnets, including Kintsugi.
Features
Optimistic rollup 
ZK-rollup 
Proof
Uses fraud proofs to prove transaction validity. 
Uses validity (zero-knowledge) proofs to prove transaction validity. 
Capital efficiency
Requires waiting through a 1-week delay (dispute period) before withdrawing funds. 
Users can withdraw funds immediately because validity proofs provide incontrovertible evidence of the authenticity of off-chain transactions. 
Data compression
Publishes full transaction data as calldata to Ethereum Mainnet, which increases rollup costs. 
Doesn't need to publish transaction data on Ethereum because ZK-SNARKs and ZK-STARKs already guarantee the accuracy of the rollup state. 
EVM compatibility
Uses a simulation of the Ethereum Virtual Machine (EVM), which allows it to run arbitrary logic and support smart contracts. 
Doesn't widely support EVM computation, although a few EVM-compatible ZK-rollups have appeared. 
Rollup costs
Reduces costs since it publishes minimal data on Ethereum and doesn't have to post proofs for transactions, except in special circumstances. 
Faces higher overhead from costs involved in generating and verifying proofs for every transaction block. ZK proofs require specialized, expensive hardware to create and have high on-chain verification costs. 
Trust assumptions
Doesn't require a trusted setup. 
Requires a trusted setup to work. 
Liveness requirements
Verifiers are needed to keep tabs on the actual rollup state and the one referenced in the state root to detect fraud. 
Users don't need someone to watch the L2 chain to detect fraud. 
Security properties 
Relies on cryptoeconomic incentives to assure users of rollup security. 
Relies on cryptographic guarantees for security. 
Start building
on Alchemy.
Sign up for free
Start building on Optimism.
Sign up for free
Start building on Arbitrum.
Sign up for free
Start building on Ethereum.
Sign up for free
Start building on Polygon.
Sign up for free
Start building on Starknet.
Sign up for free
Start building on Flow.
Sign up for free
kiln faucet
Get free Kiln ETH.
Start building today
Goerli faucet
Get free Goerli ETH.
Start building today
mumbai faucet
Get free Mumbai Matic.
Start building today
rinkeby faucet
Get free Rinkeby
ETH.
Start building today
Start building on Ethereum.
Get started for free
Start building on Ethereum.
Get started for free
Start building on Flow.
Get started for free
Start building on Polygon.
Get started for free
Start building on Starknet.
Get started for free
Start building on Optimism.
Get started for free
Start building on Solana.
Get started for free
Start building on Solana.
Sign up for beta access
Start building on Solana.
Join the waitlist
Arbitrum logo
Start building on Arbitrum.
Get started for free
Learn
Solidity at
Alchemy
University
Get started today
curl 
https://release.solana.com/v1.10.32/solana-install-init-x86_64-pc-windows-msvc.exe 
--output 
C:\solana-install-tmp\solana-install-init.exe 
--create-dirs
Learn Solidity
SOLIDITY GAS OPTIMIZATION OVERVIEW

10 Expert Solidity Gas Optimization Techniques

Why Your Gas Fees Are High and 10 Ways to Lower Them
Last Updated:
October 4, 2022
Table of Contents
Table of Contents
Table of Contents

{{learn-solidity}}

Ethereum gas fees have long been a source of concern for users, and while the recent Ethereum Proof-of-Stake merge introduced a more energy-efficient system, there was little effect on gas fees. To maintain high standards, minimize risk, write clean code, and create secure, cost-effective smart contracts, it is critical to know the techniques for optimizing gas with Solidity. 

This article will give you a near-complete understanding of the key concepts underlying gas optimization with Solidity, examples of optimized (and sub-optimal) smart contract code, and tips on how to integrate these Solidity gas optimization concepts into your web3 project today. 

What is gas and gas optimization in Solidity?

Gas is the unit of measurement for the amount of computational effort required to carry out specific operations on the Ethereum network, and Solidity gas optimization is the process of making your Solidity smart code less expensive to execute. 

Because each Ethereum transaction necessitates the use of computational resources, each transaction necessitates a fee. The fee required to complete an Ethereum transaction is referred to as gas.

When a smart contract is compiled in Solidity, it is converted into a series of "operation codes," also referred to as opcodes. Each opcode is given a predefined amount of gas, which represents the computing work necessary to carry out that specific operation. 

Opcodes and bytecodes are similar, however, bytecodes use hexadecimal integers to represent them. Bytecodes are executed by the Ethereum Virtual Machine, also known as the EVM, which is a piece of software that executes smart contracts and sits atop the Ethereum node and network layers.

The goal of optimization is to reduce the overall number of operations needed to run a smart contract, and optimized smart contracts not only reduce the gas needed to process transactions, they are also a protection against malicious misuse. 

Top 10 Solidity Gas Optimization Techniques

1. Use Mappings Instead of Arrays

There are two data types to describe lists of data in Solidity, arrays and maps, and their syntax and structure are quite different, allowing each to serve a distinct purpose. While arrays are packable and iterable, mappings are less expensive.

For example, creating an array of cars in Solidity might look like this:



string cars[];
cars = ["ford", "audi", "chevrolet"];

Let’s see how to create a mapping for cars:



mapping(uint => string) public cars

When using the mapping keyword, you will specify the data type for the key (uint) and the value (string). Then you can add some data using the constructor function.



 constructor() public {
        cars[101] = "Ford";
        cars[102] = "Audi";
        cars[103] = "Chevrolet";
    }
}

Except where iteration is required or data types can be packed, it is advised to use mappings to manage lists of data in order to conserve gas. This is beneficial for both memory and storage.

An integer index can be used as a key in a mapping to control an ordered list. Another advantage of mappings is that you can access any value without having to iterate through an array as would otherwise be necessary. 

2. Enable the Solidity Compiler Optimizer

The Solidity compiler optimizer works to make complex expressions simpler, which minimizes the size of the code and the cost of execution via inline operations, deployments costs, and function call costs.

The Solidity optimizer specializes in inline operations. Even though an action like inlining functions can result in significantly larger code, it is frequently used because it creates the potential for additional simplifications.

Deployment costs and function call costs are two more areas where the compiler optimizer impacts your smart contracts’ gas. 

For example, deployment costs decrease with the decrease in "runs"—which specifies how often each opcode will be executed over the life of a contract. The impact on function call costs, however, increases with the number of runs. That’s because code optimized for more runs costs more to deploy and less after deployment.

In the examples below, runs are set at 200 and 10,000: 



module.exports = {
  solidity: {
    version: "0.8.9",
    settings: {
      optimizer: {
        enabled: false,
        runs: 200,
      },
    },
  },
};

Increasing runs to 10,000 and setting the default value to true:



module.exports = {
  solidity: {
    version: "0.8.9",
    settings: {
      optimizer: {
        enabled: true,
        runs: 10000,
      },
    },
  },
};

3. Minimize On-Chain Data

Because on-chain data is limited to what can be created natively inside a blockchain network (e.g. state, account addresses, balances, etc.), you can reduce unnecessary operations and complex computations by saving less data in storage variables, batching operations, and avoiding looping.

The less data you save in storage variables, the less gas you'll need. Keep all data off-chain and only save the smart contract’s critical info on-chain. Developers can create more complex applications, including prediction markets, stablecoins, and parametric insurance, by integrating off-chain data into a blockchain network. 

Using events to store data is a popular, but ill-advised method for gas optimization because, while it is less expensive to store data in events relative to variables, the data in events cannot be accessed by other smart contracts on-chain. 

Batching Operations

Batching operations enables developers to batch actions by passing dynamically sized arrays that can execute the same functionality in a single transaction, rather than requiring the same method several times with different values. 

Consider the following scenario: a user wants to call getData() with five different inputs. In the streamlined form, the user would only need to pay the transaction's fixed gas cost and the gas for the msg.sender check once.



function batchSend(Call[] memory _calls) public payable {
       for(uint256 i = 0; i < _calls.length; i++) {
           (bool _success, bytes memory _data) = _calls[i].recipient.call{gas: _calls[i].gas, value: _calls[i].value}(_calls[i].data);
           if (!_success) {
              
               assembly { revert(add(0x20, _data), mload(_data)) }
           }
       }
   }

Looping

Avoid looping through lengthy arrays; not only will it consume a lot of gas, but if gas prices rise too much, it can even prevent your contract from being carried out beyond the block gas limit.

‍Instead of looping over an array until you locate the key you need, use mappings, which are hash tables that enable you to retrieve any value using its key in a single action.

4. Use Indexed Events 

Events are used to let users know when something occurs on the blockchain, as smart contracts cannot hear events on their own because contract data lives in the States trie, and event data is stored in the Transaction Receipts trie.

‍Events in Solidity are a shortcut to speed up the development of external systems working in combination with smart contracts. All information in the blockchain is public, and any activity can be detected by closely examining the transactions.

Including a mechanism to keep track of a smart contract's activity after it is deployed is helpful in reducing overall gas. While looking at all of the contract's transactions is one way to keep track of the activity, because message calls between contracts are not recorded on the blockchain, that approach might not be sufficient. 



event myFirstEvent(address indexed sender, uint256 indexed amount, string message);

You can search for logged events using the indexed parameters as filters for those events.

5. Use uint8 Can Increase Gas Cost

A smart contract's gas consumption can be higher if developers use items that are less than 32 bytes in size because the Ethereum Virtual Machine can only handle 32 bytes at a time. In order to increase the element's size to the necessary size, the EVM has to perform additional operations. 



contract A { uint8 a = 0; }

The cost in the above example is 22,150 + 2,000 gas, compared with 7,050 gas when using a type higher than 32 bytes.



contract A { uint a = 0; // or uint256 }

Only when you’re working with storage values is it advantageous to utilize reduced-size parameters because the compiler will compress several elements into one storage slot, combining numerous reads or writes into a single operation.

Smaller-size unsigned integers, such as uint8, are only more effective when multiple variables can be stored in the same storage space, like in structs. Uint256 uses less gas than uint8 in loops and other situations.

6. Pack Your Variables 

When processing data, the EVM adopts a novel approach: each contract has a storage location where data is kept permanently, as well as a persistent storage space where data can be read, written, and updated.

There are 2,256 slots in the storage, each of which holds 32 bytes. Depending on their particular nature, the "state variables," or variables declared in a smart contract that are not within any function, will be stored in these slots. 

Smaller-sized state variables (i.e. variables with less than 32 bytes in size), are saved as index values in the sequence in which they were defined, with 0 for position 1, 1 for position 2, and so on. If small values are stated sequentially, they will be stored in the same slot, including very small values like uint64.

Consider the following example:

Before

Small values are not stored sequentially and use unnecessary storage space.



contract MyContract {
  uint128 c; 
  uint256 b; 
  uint128 a;
}

After

Small values are stored sequentially and use less storage space because they are packed together.



contract Leggo {
  uint128 a;  
  uint128 c;  
  uint256 b; 
}

7. Free Up Unused Storage

Deleting your unused variables helps free up space and earns a gas refund. Deleting unused variables has the same effect as reassigning the value type with its default value, such as the integer's default value of 0, or the address zero for addresses.



//Using delete keyword
delete myVariable;

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

Mappings, however, are unaffected by deletion, as the keys of mappings may be arbitrary and are generally unknown. Therefore, if you delete a struct, all of its members that are not mappings will reset and also recurse into its members. However, individual keys and the values they relate to can be removed.

8. Store Data in calldata Instead of Memory for Certain Function Parameters 

Instead of copying variables to memory, it is typically more cost-effective to load them immediately from calldata. If all you need to do is read data, you can conserve gas by saving the data in calldata.



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

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

Because the values in calldata cannot be changed while the function is being executed, if the variable needs to be updated when calling a function, use memory instead.

9. Use immutable and constant

Immutable and constant are keywords that can be used on state variables to limit changes to their state. Constant variables cannot be changed after being compiled, whereas immutable variables can be set within the constructor. Constant variables can also be declared at the file level, such as in the example below:



contract MyContract {
    uint256 constant b = 10;
    uint256 immutable a;

    constructor() {
        a = 5;
    } 
 }

10. Use the external Visibility Modifier

Use the external function visibility for gas optimization because the public visibility modifier is equivalent to using the external and internal visibility modifier, meaning both public and external can be called from outside of your contract, which requires more gas.

Remember that of these two visibility modifiers, only the public modifier can be called from other functions inside of your contract.



   function one() public view returns (string memory){
         return message;
    }

 
    function two() external view returns  (string memory){
         return message;
    }

Test Your Smart Contract Before Deployment  

This code will always produce the same gas used in Hardhat, Rinkeby, and Mainnet, regardless of the environment in which it is run. When testing your functionalities, pay special attention to the ones that are most similar to the mint function because those are the ones that your users will access most frequently.

In this guide, we covered the importance of gas optimization, the value it gives developers, and ten techniques for writing gas-optimized smart contracts with Solidity.   

As a web3 and blockchain developer, optimizing gas cost in Solidity smart contracts is one of the most challenging and important aspects of creating a high-quality, efficient project. It requires practice and a thorough understanding of both the concepts and practicalities of Ethereum and Solidity. Gas optimization is a benefit not just to your project, but the blockchain ecosystem at large. 

ALCHEMY SUPERNODE - ETHEREUM NODE API

Scale to any size, without any errors

Alchemy Supernode finally makes it possible to scale blockchain applications without all the headaches. Plus, our legendary support will guide you every step of the way.

Get started for free
Supernode footer
Learn Solidity
SOLIDITY GAS OPTIMIZATION OVERVIEW

Learn 10 Expert Solidity Gas Optimization Techniques

Why Your Gas Fees Are High and 10 Ways to Lower Them
Last Updated:
October 4, 2022
Don't miss an update
Sign up for our newsletter to get alpha, key insights, and killer resources.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Table of Contents
Table of Contents

{{learn-solidity}}

Ethereum gas fees have long been a source of concern for users, and while the recent Ethereum Proof-of-Stake merge introduced a more energy-efficient system, there was little effect on gas fees. To maintain high standards, minimize risk, write clean code, and create secure, cost-effective smart contracts, it is critical to know the techniques for optimizing gas with Solidity. 

This article will give you a near-complete understanding of the key concepts underlying gas optimization with Solidity, examples of optimized (and sub-optimal) smart contract code, and tips on how to integrate these Solidity gas optimization concepts into your web3 project today. 

What is gas and gas optimization in Solidity?

Gas is the unit of measurement for the amount of computational effort required to carry out specific operations on the Ethereum network, and Solidity gas optimization is the process of making your Solidity smart code less expensive to execute. 

Because each Ethereum transaction necessitates the use of computational resources, each transaction necessitates a fee. The fee required to complete an Ethereum transaction is referred to as gas.

When a smart contract is compiled in Solidity, it is converted into a series of "operation codes," also referred to as opcodes. Each opcode is given a predefined amount of gas, which represents the computing work necessary to carry out that specific operation. 

Opcodes and bytecodes are similar, however, bytecodes use hexadecimal integers to represent them. Bytecodes are executed by the Ethereum Virtual Machine, also known as the EVM, which is a piece of software that executes smart contracts and sits atop the Ethereum node and network layers.

The goal of optimization is to reduce the overall number of operations needed to run a smart contract, and optimized smart contracts not only reduce the gas needed to process transactions, they are also a protection against malicious misuse. 

Top 10 Solidity Gas Optimization Techniques

1. Use Mappings Instead of Arrays

There are two data types to describe lists of data in Solidity, arrays and maps, and their syntax and structure are quite different, allowing each to serve a distinct purpose. While arrays are packable and iterable, mappings are less expensive.

For example, creating an array of cars in Solidity might look like this:



string cars[];
cars = ["ford", "audi", "chevrolet"];

Let’s see how to create a mapping for cars:



mapping(uint => string) public cars

When using the mapping keyword, you will specify the data type for the key (uint) and the value (string). Then you can add some data using the constructor function.



 constructor() public {
        cars[101] = "Ford";
        cars[102] = "Audi";
        cars[103] = "Chevrolet";
    }
}

Except where iteration is required or data types can be packed, it is advised to use mappings to manage lists of data in order to conserve gas. This is beneficial for both memory and storage.

An integer index can be used as a key in a mapping to control an ordered list. Another advantage of mappings is that you can access any value without having to iterate through an array as would otherwise be necessary. 

2. Enable the Solidity Compiler Optimizer

The Solidity compiler optimizer works to make complex expressions simpler, which minimizes the size of the code and the cost of execution via inline operations, deployments costs, and function call costs.

The Solidity optimizer specializes in inline operations. Even though an action like inlining functions can result in significantly larger code, it is frequently used because it creates the potential for additional simplifications.

Deployment costs and function call costs are two more areas where the compiler optimizer impacts your smart contracts’ gas. 

For example, deployment costs decrease with the decrease in "runs"—which specifies how often each opcode will be executed over the life of a contract. The impact on function call costs, however, increases with the number of runs. That’s because code optimized for more runs costs more to deploy and less after deployment.

In the examples below, runs are set at 200 and 10,000: 



module.exports = {
  solidity: {
    version: "0.8.9",
    settings: {
      optimizer: {
        enabled: false,
        runs: 200,
      },
    },
  },
};

Increasing runs to 10,000 and setting the default value to true:



module.exports = {
  solidity: {
    version: "0.8.9",
    settings: {
      optimizer: {
        enabled: true,
        runs: 10000,
      },
    },
  },
};

3. Minimize On-Chain Data

Because on-chain data is limited to what can be created natively inside a blockchain network (e.g. state, account addresses, balances, etc.), you can reduce unnecessary operations and complex computations by saving less data in storage variables, batching operations, and avoiding looping.

The less data you save in storage variables, the less gas you'll need. Keep all data off-chain and only save the smart contract’s critical info on-chain. Developers can create more complex applications, including prediction markets, stablecoins, and parametric insurance, by integrating off-chain data into a blockchain network. 

Using events to store data is a popular, but ill-advised method for gas optimization because, while it is less expensive to store data in events relative to variables, the data in events cannot be accessed by other smart contracts on-chain. 

Batching Operations

Batching operations enables developers to batch actions by passing dynamically sized arrays that can execute the same functionality in a single transaction, rather than requiring the same method several times with different values. 

Consider the following scenario: a user wants to call getData() with five different inputs. In the streamlined form, the user would only need to pay the transaction's fixed gas cost and the gas for the msg.sender check once.



function batchSend(Call[] memory _calls) public payable {
       for(uint256 i = 0; i < _calls.length; i++) {
           (bool _success, bytes memory _data) = _calls[i].recipient.call{gas: _calls[i].gas, value: _calls[i].value}(_calls[i].data);
           if (!_success) {
              
               assembly { revert(add(0x20, _data), mload(_data)) }
           }
       }
   }

Looping

Avoid looping through lengthy arrays; not only will it consume a lot of gas, but if gas prices rise too much, it can even prevent your contract from being carried out beyond the block gas limit.

‍Instead of looping over an array until you locate the key you need, use mappings, which are hash tables that enable you to retrieve any value using its key in a single action.

4. Use Indexed Events 

Events are used to let users know when something occurs on the blockchain, as smart contracts cannot hear events on their own because contract data lives in the States trie, and event data is stored in the Transaction Receipts trie.

‍Events in Solidity are a shortcut to speed up the development of external systems working in combination with smart contracts. All information in the blockchain is public, and any activity can be detected by closely examining the transactions.

Including a mechanism to keep track of a smart contract's activity after it is deployed is helpful in reducing overall gas. While looking at all of the contract's transactions is one way to keep track of the activity, because message calls between contracts are not recorded on the blockchain, that approach might not be sufficient. 



event myFirstEvent(address indexed sender, uint256 indexed amount, string message);

You can search for logged events using the indexed parameters as filters for those events.

5. Use uint8 Can Increase Gas Cost

A smart contract's gas consumption can be higher if developers use items that are less than 32 bytes in size because the Ethereum Virtual Machine can only handle 32 bytes at a time. In order to increase the element's size to the necessary size, the EVM has to perform additional operations. 



contract A { uint8 a = 0; }

The cost in the above example is 22,150 + 2,000 gas, compared with 7,050 gas when using a type higher than 32 bytes.



contract A { uint a = 0; // or uint256 }

Only when you’re working with storage values is it advantageous to utilize reduced-size parameters because the compiler will compress several elements into one storage slot, combining numerous reads or writes into a single operation.

Smaller-size unsigned integers, such as uint8, are only more effective when multiple variables can be stored in the same storage space, like in structs. Uint256 uses less gas than uint8 in loops and other situations.

6. Pack Your Variables 

When processing data, the EVM adopts a novel approach: each contract has a storage location where data is kept permanently, as well as a persistent storage space where data can be read, written, and updated.

There are 2,256 slots in the storage, each of which holds 32 bytes. Depending on their particular nature, the "state variables," or variables declared in a smart contract that are not within any function, will be stored in these slots. 

Smaller-sized state variables (i.e. variables with less than 32 bytes in size), are saved as index values in the sequence in which they were defined, with 0 for position 1, 1 for position 2, and so on. If small values are stated sequentially, they will be stored in the same slot, including very small values like uint64.

Consider the following example:

Before

Small values are not stored sequentially and use unnecessary storage space.



contract MyContract {
  uint128 c; 
  uint256 b; 
  uint128 a;
}

After

Small values are stored sequentially and use less storage space because they are packed together.



contract Leggo {
  uint128 a;  
  uint128 c;  
  uint256 b; 
}

7. Free Up Unused Storage

Deleting your unused variables helps free up space and earns a gas refund. Deleting unused variables has the same effect as reassigning the value type with its default value, such as the integer's default value of 0, or the address zero for addresses.



//Using delete keyword
delete myVariable;

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

Mappings, however, are unaffected by deletion, as the keys of mappings may be arbitrary and are generally unknown. Therefore, if you delete a struct, all of its members that are not mappings will reset and also recurse into its members. However, individual keys and the values they relate to can be removed.

8. Store Data in calldata Instead of Memory for Certain Function Parameters 

Instead of copying variables to memory, it is typically more cost-effective to load them immediately from calldata. If all you need to do is read data, you can conserve gas by saving the data in calldata.



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

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

Because the values in calldata cannot be changed while the function is being executed, if the variable needs to be updated when calling a function, use memory instead.

9. Use immutable and constant

Immutable and constant are keywords that can be used on state variables to limit changes to their state. Constant variables cannot be changed after being compiled, whereas immutable variables can be set within the constructor. Constant variables can also be declared at the file level, such as in the example below:



contract MyContract {
    uint256 constant b = 10;
    uint256 immutable a;

    constructor() {
        a = 5;
    } 
 }

10. Use the external Visibility Modifier

Use the external function visibility for gas optimization because the public visibility modifier is equivalent to using the external and internal visibility modifier, meaning both public and external can be called from outside of your contract, which requires more gas.

Remember that of these two visibility modifiers, only the public modifier can be called from other functions inside of your contract.



   function one() public view returns (string memory){
         return message;
    }

 
    function two() external view returns  (string memory){
         return message;
    }

Test Your Smart Contract Before Deployment  

This code will always produce the same gas used in Hardhat, Rinkeby, and Mainnet, regardless of the environment in which it is run. When testing your functionalities, pay special attention to the ones that are most similar to the mint function because those are the ones that your users will access most frequently.

In this guide, we covered the importance of gas optimization, the value it gives developers, and ten techniques for writing gas-optimized smart contracts with Solidity.   

As a web3 and blockchain developer, optimizing gas cost in Solidity smart contracts is one of the most challenging and important aspects of creating a high-quality, efficient project. It requires practice and a thorough understanding of both the concepts and practicalities of Ethereum and Solidity. Gas optimization is a benefit not just to your project, but the blockchain ecosystem at large. 

Ethereum gas fees have long been a source of concern for users, and while the recent Ethereum Proof-of-Stake merge introduced a more energy-efficient system, there was little effect on gas fees. To maintain high standards, minimize risk, write clean code, and create secure, cost-effective smart contracts, it is critical to know the techniques for optimizing gas with Solidity. 

This article will give you a near-complete understanding of the key concepts underlying gas optimization with Solidity, examples of optimized (and sub-optimal) smart contract code, and tips on how to integrate these Solidity gas optimization concepts into your web3 project today. 

What is gas and gas optimization in Solidity?

Gas is the unit of measurement for the amount of computational effort required to carry out specific operations on the Ethereum network, and Solidity gas optimization is the process of making your Solidity smart code less expensive to execute. 

Because each Ethereum transaction necessitates the use of computational resources, each transaction necessitates a fee. The fee required to complete an Ethereum transaction is referred to as gas.

When a smart contract is compiled in Solidity, it is converted into a series of "operation codes," also referred to as opcodes. Each opcode is given a predefined amount of gas, which represents the computing work necessary to carry out that specific operation. 

Opcodes and bytecodes are similar, however, bytecodes use hexadecimal integers to represent them. Bytecodes are executed by the Ethereum Virtual Machine, also known as the EVM, which is a piece of software that executes smart contracts and sits atop the Ethereum node and network layers.

The goal of optimization is to reduce the overall number of operations needed to run a smart contract, and optimized smart contracts not only reduce the gas needed to process transactions, they are also a protection against malicious misuse. 

Top 10 Solidity Gas Optimization Techniques

1. Use Mappings Instead of Arrays

There are two data types to describe lists of data in Solidity, arrays and maps, and their syntax and structure are quite different, allowing each to serve a distinct purpose. While arrays are packable and iterable, mappings are less expensive.

For example, creating an array of cars in Solidity might look like this:



string cars[];
cars = ["ford", "audi", "chevrolet"];

Let’s see how to create a mapping for cars:



mapping(uint => string) public cars

When using the mapping keyword, you will specify the data type for the key (uint) and the value (string). Then you can add some data using the constructor function.



 constructor() public {
        cars[101] = "Ford";
        cars[102] = "Audi";
        cars[103] = "Chevrolet";
    }
}

Except where iteration is required or data types can be packed, it is advised to use mappings to manage lists of data in order to conserve gas. This is beneficial for both memory and storage.

An integer index can be used as a key in a mapping to control an ordered list. Another advantage of mappings is that you can access any value without having to iterate through an array as would otherwise be necessary. 

2. Enable the Solidity Compiler Optimizer

The Solidity compiler optimizer works to make complex expressions simpler, which minimizes the size of the code and the cost of execution via inline operations, deployments costs, and function call costs.

The Solidity optimizer specializes in inline operations. Even though an action like inlining functions can result in significantly larger code, it is frequently used because it creates the potential for additional simplifications.

Deployment costs and function call costs are two more areas where the compiler optimizer impacts your smart contracts’ gas. 

For example, deployment costs decrease with the decrease in "runs"—which specifies how often each opcode will be executed over the life of a contract. The impact on function call costs, however, increases with the number of runs. That’s because code optimized for more runs costs more to deploy and less after deployment.

In the examples below, runs are set at 200 and 10,000: 



module.exports = {
  solidity: {
    version: "0.8.9",
    settings: {
      optimizer: {
        enabled: false,
        runs: 200,
      },
    },
  },
};

Increasing runs to 10,000 and setting the default value to true:



module.exports = {
  solidity: {
    version: "0.8.9",
    settings: {
      optimizer: {
        enabled: true,
        runs: 10000,
      },
    },
  },
};

3. Minimize On-Chain Data

Because on-chain data is limited to what can be created natively inside a blockchain network (e.g. state, account addresses, balances, etc.), you can reduce unnecessary operations and complex computations by saving less data in storage variables, batching operations, and avoiding looping.

The less data you save in storage variables, the less gas you'll need. Keep all data off-chain and only save the smart contract’s critical info on-chain. Developers can create more complex applications, including prediction markets, stablecoins, and parametric insurance, by integrating off-chain data into a blockchain network. 

Using events to store data is a popular, but ill-advised method for gas optimization because, while it is less expensive to store data in events relative to variables, the data in events cannot be accessed by other smart contracts on-chain. 

Batching Operations

Batching operations enables developers to batch actions by passing dynamically sized arrays that can execute the same functionality in a single transaction, rather than requiring the same method several times with different values. 

Consider the following scenario: a user wants to call getData() with five different inputs. In the streamlined form, the user would only need to pay the transaction's fixed gas cost and the gas for the msg.sender check once.



function batchSend(Call[] memory _calls) public payable {
       for(uint256 i = 0; i < _calls.length; i++) {
           (bool _success, bytes memory _data) = _calls[i].recipient.call{gas: _calls[i].gas, value: _calls[i].value}(_calls[i].data);
           if (!_success) {
              
               assembly { revert(add(0x20, _data), mload(_data)) }
           }
       }
   }

Looping

Avoid looping through lengthy arrays; not only will it consume a lot of gas, but if gas prices rise too much, it can even prevent your contract from being carried out beyond the block gas limit.

‍Instead of looping over an array until you locate the key you need, use mappings, which are hash tables that enable you to retrieve any value using its key in a single action.

4. Use Indexed Events 

Events are used to let users know when something occurs on the blockchain, as smart contracts cannot hear events on their own because contract data lives in the States trie, and event data is stored in the Transaction Receipts trie.

‍Events in Solidity are a shortcut to speed up the development of external systems working in combination with smart contracts. All information in the blockchain is public, and any activity can be detected by closely examining the transactions.

Including a mechanism to keep track of a smart contract's activity after it is deployed is helpful in reducing overall gas. While looking at all of the contract's transactions is one way to keep track of the activity, because message calls between contracts are not recorded on the blockchain, that approach might not be sufficient. 



event myFirstEvent(address indexed sender, uint256 indexed amount, string message);

You can search for logged events using the indexed parameters as filters for those events.

5. Use uint8 Can Increase Gas Cost

A smart contract's gas consumption can be higher if developers use items that are less than 32 bytes in size because the Ethereum Virtual Machine can only handle 32 bytes at a time. In order to increase the element's size to the necessary size, the EVM has to perform additional operations. 



contract A { uint8 a = 0; }

The cost in the above example is 22,150 + 2,000 gas, compared with 7,050 gas when using a type higher than 32 bytes.



contract A { uint a = 0; // or uint256 }

Only when you’re working with storage values is it advantageous to utilize reduced-size parameters because the compiler will compress several elements into one storage slot, combining numerous reads or writes into a single operation.

Smaller-size unsigned integers, such as uint8, are only more effective when multiple variables can be stored in the same storage space, like in structs. Uint256 uses less gas than uint8 in loops and other situations.

6. Pack Your Variables 

When processing data, the EVM adopts a novel approach: each contract has a storage location where data is kept permanently, as well as a persistent storage space where data can be read, written, and updated.

There are 2,256 slots in the storage, each of which holds 32 bytes. Depending on their particular nature, the "state variables," or variables declared in a smart contract that are not within any function, will be stored in these slots. 

Smaller-sized state variables (i.e. variables with less than 32 bytes in size), are saved as index values in the sequence in which they were defined, with 0 for position 1, 1 for position 2, and so on. If small values are stated sequentially, they will be stored in the same slot, including very small values like uint64.

Consider the following example:

Before

Small values are not stored sequentially and use unnecessary storage space.



contract MyContract {
  uint128 c; 
  uint256 b; 
  uint128 a;
}

After

Small values are stored sequentially and use less storage space because they are packed together.



contract Leggo {
  uint128 a;  
  uint128 c;  
  uint256 b; 
}

7. Free Up Unused Storage

Deleting your unused variables helps free up space and earns a gas refund. Deleting unused variables has the same effect as reassigning the value type with its default value, such as the integer's default value of 0, or the address zero for addresses.



//Using delete keyword
delete myVariable;

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

Mappings, however, are unaffected by deletion, as the keys of mappings may be arbitrary and are generally unknown. Therefore, if you delete a struct, all of its members that are not mappings will reset and also recurse into its members. However, individual keys and the values they relate to can be removed.

8. Store Data in calldata Instead of Memory for Certain Function Parameters 

Instead of copying variables to memory, it is typically more cost-effective to load them immediately from calldata. If all you need to do is read data, you can conserve gas by saving the data in calldata.



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

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

Because the values in calldata cannot be changed while the function is being executed, if the variable needs to be updated when calling a function, use memory instead.

9. Use immutable and constant

Immutable and constant are keywords that can be used on state variables to limit changes to their state. Constant variables cannot be changed after being compiled, whereas immutable variables can be set within the constructor. Constant variables can also be declared at the file level, such as in the example below:



contract MyContract {
    uint256 constant b = 10;
    uint256 immutable a;

    constructor() {
        a = 5;
    } 
 }

10. Use the external Visibility Modifier

Use the external function visibility for gas optimization because the public visibility modifier is equivalent to using the external and internal visibility modifier, meaning both public and external can be called from outside of your contract, which requires more gas.

Remember that of these two visibility modifiers, only the public modifier can be called from other functions inside of your contract.



   function one() public view returns (string memory){
         return message;
    }

 
    function two() external view returns  (string memory){
         return message;
    }

Test Your Smart Contract Before Deployment  

This code will always produce the same gas used in Hardhat, Rinkeby, and Mainnet, regardless of the environment in which it is run. When testing your functionalities, pay special attention to the ones that are most similar to the mint function because those are the ones that your users will access most frequently.

In this guide, we covered the importance of gas optimization, the value it gives developers, and ten techniques for writing gas-optimized smart contracts with Solidity.   

As a web3 and blockchain developer, optimizing gas cost in Solidity smart contracts is one of the most challenging and important aspects of creating a high-quality, efficient project. It requires practice and a thorough understanding of both the concepts and practicalities of Ethereum and Solidity. Gas optimization is a benefit not just to your project, but the blockchain ecosystem at large. 

Build web3 with Alchemy

Alchemy combines the most powerful web3 developer products and tools with resources, community and legendary support.

Get started for free