rounded

Written by: Certik

Gas fees on the Ethereum mainnet have always been a significant issue, especially during network congestion. During peak times, users often have to pay extremely high transaction fees. Therefore, optimizing gas fees during the smart contract development phase is particularly important. Optimizing gas consumption can effectively reduce transaction costs and improve transaction efficiency, providing users with a more economical and efficient blockchain experience.

This article will outline the gas fee mechanism of the Ethereum Virtual Machine (EVM), the core concepts of gas fee optimization, and best practices for optimizing gas fees when developing smart contracts. Through this content, we hope to provide inspiration and practical help to developers while also assisting ordinary users in better understanding how gas fees operate in the EVM, together addressing the challenges in the blockchain ecosystem.

Introduction to the EVM gas fee mechanism

In EVM-compatible networks, 'Gas' refers to the unit used to measure the computational power required to execute specific operations.

The following diagram illustrates the structural layout of the EVM. In the diagram, gas consumption is divided into three parts: operation execution, external message calls, and memory and storage read/write.

Source: Ethereum Official Website [1]

Since the execution of each transaction requires computational resources, a fee is charged to prevent infinite loops and denial-of-service (DoS) attacks. The fee required to complete a transaction is called 'Gas fee.'

Since the implementation of EIP-1559 (London Hard Fork), gas fees have been calculated using the following formula:

Gas fee = units of gas used * (base fee + priority fee)

The base fee will be burned, while the priority fee serves as an incentive, encouraging validators to include the transaction in the blockchain. Setting a higher priority fee when sending a transaction can increase the likelihood of the transaction being included in the next block. This is similar to a 'tip' that users pay to validators.

1. Understand gas optimization in EVM

When compiling smart contracts with Solidity, contracts are converted into a series of 'opcodes'.

Each opcode (such as creating contracts, making message calls, accessing account storage, and executing operations on the virtual machine) has a recognized gas consumption cost, which is recorded in the Ethereum yellow paper [2].

After multiple modifications of the EIP, the gas costs of some opcodes have been adjusted and may differ from those in the yellow paper. For detailed information on the latest opcode costs, please refer to this [3].

2. Basic Concepts of Gas Optimization

The core idea of gas optimization is to prioritize cost-efficient operations on the EVM blockchain and avoid operations that are expensive in terms of gas costs.

In the EVM, the following operations have lower costs:

  • Read and write memory variables

  • Read constant and immutable variables

  • Read and write local variables

  • Read calldata variables, such as calldata arrays and structs

  • Internal function calls

High-cost operations include:

  • Read and write state variables stored in the contract storage

  • External function calls

  • Loop operations

Best Practices for EVM Gas Fee Optimization

Based on the above basic concepts, we have compiled a checklist of best practices for gas fee optimization for the developer community. By following these practices, developers can lower the gas fee consumption of smart contracts, reduce transaction costs, and create more efficient and user-friendly applications.

1. Minimize storage usage

In Solidity, Storage is a limited resource, and its gas consumption is much higher than that of Memory. Each time a smart contract reads or writes data from storage, it incurs high gas costs.

According to the Ethereum yellow paper, the cost of storage operations is more than 100 times higher than that of memory operations. For example, the OPcodes mload and mstore instructions only consume 3 gas units, while storage operations such as sload and sstore cost at least 100 units even under the most ideal circumstances.

Methods to limit storage usage include:

  • Store non-permanent data in memory

  • Reduce the number of storage modifications: by keeping intermediate results in memory, the final result can be assigned to storage variables after all calculations are completed.

2. Variable packing

The number of storage slots used in smart contracts and the way developers represent data greatly affects gas fee consumption.

The Solidity compiler packs contiguous storage variables during the compilation process, using a 32-byte storage slot as the basic unit for variable storage. Variable packing refers to arranging variables in such a way that multiple variables can fit into a single storage slot.

On the left is a less efficient implementation that will consume 3 storage slots; on the right is a more efficient implementation.

Through this detail adjustment, developers can save 20,000 gas units (storing an unused storage slot consumes 20,000 gas), but now only two storage slots are needed.

Since each storage slot consumes gas, variable packing optimizes gas usage by reducing the number of required storage slots.

3. Optimize data types

A variable can be represented by multiple data types, but the operational costs associated with different data types also vary. Choosing the appropriate data type helps optimize gas usage.

For example, in Solidity, integers can be subdivided into different sizes: uint8, uint16, uint32, etc. Since the EVM operates in 256-bit units, using uint8 means the EVM must first convert it to uint256, which incurs additional gas costs.

We can compare the gas costs of uint8 and uint256 in the code shown in the diagram. The UseUint() function consumes 120,382 gas units, while the UseUInt8() function consumes 166,111 gas units.

Looking at it individually, using uint256 here is cheaper than uint8. However, it is different if we use the variable packing optimization we suggested earlier. If developers can pack four uint8 variables into one storage slot, the total cost of iterating over them will be lower than that of four uint256 variables. This way, the smart contract can read and write one storage slot and load four uint8 variables into memory/storage in one operation.

4. Use fixed-size variables instead of dynamic variables

If the data can be controlled within 32 bytes, it is recommended to use the bytes32 data type instead of bytes or strings. Generally, fixed-size variables consume less gas than variable-size variables. If the byte length can be limited, choose the minimum length from bytes1 to bytes32 as much as possible.

5. Mappings vs Arrays

Solidity's data lists can be represented in two data types: arrays and mappings, but their syntax and structure are completely different.

Mappings are generally more efficient and cost-effective in most cases, but arrays are iterable and support data type packing. Therefore, it is recommended to prioritize using mappings when managing data lists unless iteration is required or gas consumption can be optimized through data type packing.

6. Use calldata instead of memory

Variables declared in function parameters can be stored in calldata or memory. The main difference is that memory can be modified by the function, while calldata is immutable.

Remember this principle: if function parameters are read-only, prefer using calldata instead of memory. This avoids unnecessary copying operations from function calldata to memory.

Example 1: Use memory

When using the memory keyword, the values of the array are copied from the encoded calldata to memory during the ABI decoding process. The execution cost of this code block is 3,694 gas units.

Example 2: Use calldata

When reading values directly from calldata, intermediate memory operations are skipped. This optimization reduces execution costs to only 2,413 gas units, improving gas efficiency by 35%.

7. Use Constant/Immutable keywords whenever possible

Constant/Immutable variables are not stored in the contract's storage. These variables are computed at compile time and stored in the contract's bytecode. Therefore, their access cost is much lower than storage, and it is recommended to use the Constant or Immutable keywords whenever possible.

8. Use Unchecked when ensuring that overflow/underflow will not occur

When developers can ensure that arithmetic operations will not cause overflow or underflow, they can use the unchecked keyword introduced in Solidity v0.8.0 to avoid unnecessary overflow or underflow checks, thus saving gas costs.

In the following diagram, constrained by the condition i<length, the variable i can never overflow. Here, length is defined as uint256, meaning the maximum value of i is max(uint)-1. Therefore, incrementing i in an unchecked code block is considered safe and saves gas.

Additionally, compilers version 0.8.0 and above no longer require the use of the SafeMath library, as the compiler itself has built-in overflow and underflow protection.

9. Optimize modifiers

The code of modifiers is embedded in the modified functions, and each time a modifier is used, its code is copied. This increases bytecode size and raises gas consumption. Here is one way to optimize the gas cost of modifiers:

Optimized before:

Optimized:

In this example, by restructuring the logic into an internal function _checkOwner(), which allows for reuse of the internal function within the modifier, the bytecode size can be reduced, thereby lowering gas costs.

10. Short-circuit optimization

For the || and && operators, logical operations will undergo short-circuit evaluation, meaning if the first condition can already determine the result of the logical expression, the second condition will not be evaluated.

To optimize gas consumption, conditions with low computational costs should be placed first so that high-cost calculations can potentially be skipped.

Additional general suggestions

1. Remove useless code

If there are unused functions or variables in the contract, it is recommended to delete them. This is the most direct way to reduce contract deployment costs and keep the contract size small.

Here are some practical suggestions:

Use the most efficient algorithms for calculations. If some calculations are used directly in the contract, then redundant calculations should be removed. Essentially, any unused calculations should be deleted.

In Ethereum, developers can receive gas rewards by freeing up storage space. When a variable is no longer needed, it should be deleted using the delete keyword or set to a default value.

Loop optimization: avoid high-cost loop operations, merge loops whenever possible, and move repeated calculations out of the loop body.

2. Use precompiled contracts

Precompiled contracts provide complex library functions such as encryption and hashing operations. Since the code does not run on the EVM but runs locally on client nodes, less gas is required. Using precompiled contracts can save gas by reducing the computational workload required to execute smart contracts.

Examples of precompiled contracts include the Elliptic Curve Digital Signature Algorithm (ECDSA) and the SHA2-256 hash algorithm. By using these precompiled contracts in smart contracts, developers can reduce gas costs and improve the operational efficiency of applications.

For a complete list of precompiled contracts supported by the Ethereum network, please refer to this [4].

3. Use inline assembly code

Inline assembly allows developers to write low-level but efficient code that can be executed directly by the EVM without using expensive Solidity opcodes. Inline assembly also allows for more precise control over memory and storage usage, further reducing gas costs. Moreover, inline assembly can perform complex operations that are difficult to achieve using Solidity alone, providing more flexibility for optimizing gas consumption.

The following is an example of code that saves gas using inline assembly:

From the above diagram, it can be seen that compared to the standard use case, the second use case utilizing inline assembly technology has higher gas efficiency.

However, using inline assembly may also come with risks and is prone to errors. Therefore, it should be used cautiously and limited to experienced developers.

4. Use Layer 2 solutions

Using Layer 2 solutions can reduce the amount of data that needs to be stored and computed on the Ethereum mainnet.

Layer 2 solutions such as rollups, sidechains, and state channels can offload transaction processing from the main Ethereum chain, enabling faster and cheaper transactions.

By bundling a large number of transactions together, these solutions reduce the number of on-chain transactions, thereby lowering gas fees. Using Layer 2 solutions can also enhance Ethereum's scalability, allowing more users and applications to participate in the network without causing congestion due to overload.

5. Use optimization tools and libraries

There are multiple optimization tools available, such as the solc optimizer, Truffle's build optimizer, and Remix's Solidity compiler.

These tools can help minimize the size of bytecode, remove useless code, and reduce the number of operations required to execute smart contracts. In combination with other gas optimization libraries, such as 'solmate', developers can effectively lower gas costs and improve the efficiency of smart contracts.

Conclusion

Optimizing gas consumption is an important step for developers, as it can minimize transaction costs and improve the efficiency of smart contracts on EVM-compatible networks. By prioritizing cost-saving operations, reducing storage usage, utilizing inline assembly, and following other best practices discussed in this article, developers can effectively lower the gas consumption of contracts.

However, it must be noted that during optimization, developers must operate cautiously to avoid introducing security vulnerabilities. In the process of optimizing code and reducing gas consumption, the inherent security of the smart contract should never be sacrificed.

[1] : https://ethereum.org/en/developers/docs/gas/

[2] : https://ethereum.github.io/yellowpaper/paper.pdf

[3] : https://www.evm.codes/

[4] : https://www.evm.codes/precompiled