
Followin' the high Gas fees on the Ethereum mainnet has always been a thorny issue, especially during network congestion. During peak periods, users often need to pay extremely high transaction fees. Therefore, optimizing Gas consumption during the smart contract development stage is particularly important. Optimizing Gas consumption can not only effectively reduce transaction costs, but also improve transaction efficiency, providing users with a more economical and efficient blockchain usage experience.
This article will outline the Gas fee mechanism of the Ethereum Virtual Machine (EVM), the core concepts related to Gas fee optimization, and the best practices for Gas fee optimization when developing smart contracts. It is hoped that through these contents, developers can gain inspiration and practical help, and also help ordinary users better understand the operation of the EVM's Gas fee, so as to jointly address the challenges in the blockchain ecosystem.
Introduction to the EVM's Gas Fee Mechanism
In EVM-compatible networks, "Gas" is the unit used to measure the computing power required to execute a specific operation.
The diagram below illustrates the structure of the EVM. In the figure, Gas consumption is divided into three parts: execution of operations, external message calls, and reading and writing of memory and storage.

Source: Ethereum website[1]
Since the execution of each transaction requires computing resources, a certain fee is charged to prevent infinite loops and denial-of-service (DoS) attacks. The fee required to complete a transaction is called the "Gas fee".
Since the EIP-1559 (London hard fork) took effect, the Gas fee is calculated using the following formula:
Gas fee = units of gas used * (base fee + priority fee)
The base fee is burned, while the priority fee serves as an incentive to encourage 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" paid by the user to the validator.
1. Understanding Gas Optimization in the EVM
When compiling a smart contract with Solidity, the contract is converted into a series of "opcodes", i.e. opcodes.
Any sequence of opcodes (such as creating a contract, making a message call, 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 EIP modifications, the Gas cost of some opcodes has been adjusted and may deviate from the Yellow Paper. For detailed information on the latest opcode costs, please refer here[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 with high Gas costs.
In the EVM, the following operations have lower costs:
- Reading and writing memory variables
- Reading constants and immutable variables
- Reading and writing local variables
- Reading calldata variables, such as calldata arrays and structs
- Internal function calls
Operations with higher costs include:
- Reading and writing state variables stored in contract storage
- External function calls
- Loop operations
Best Practices for EVM Gas Fee Optimization
Based on the above basic concepts, we have compiled a list of best practices for Gas fee optimization for the developer community. By following these practices, developers can reduce the Gas consumption of their smart contracts, lower transaction costs, and create more efficient and user-friendly applications.
1. Minimize the use of storage
In Solidity, Storage is a limited resource, and its Gas consumption is much higher than Memory. Each time a smart contract reads from or writes to storage, it incurs a high Gas cost.
According to the Ethereum Yellow Paper, the cost of storage operations is over 100 times higher than memory operations. For example, the OPcodes mload and mstore instructions only consume 3 Gas units, while storage operations like sload and sstore cost at least 100 units, even in the most ideal case.

Methods to limit the use of storage include:
- Storing non-permanent data in memory
- Reducing the number of storage modifications: By saving intermediate results in memory and only assigning the final result to storage variables.
2. Variable Packing
The number of Storage slots used by a smart contract and the way developers represent data can greatly impact Gas fee consumption.
The Solidity compiler will pack consecutive storage variables during the compilation process, using 32-byte storage slots as the basic unit for variable storage. Variable packing refers to arranging variables in a reasonable way so that multiple variables can fit into a single storage slot.
The left side is a less efficient implementation, consuming 3 storage slots; the right side is a more efficient implementation.

With this adjustment, developers can save 20,000 Gas units (it costs 20,000 Gas to store an unused storage slot), but now only need two storage slots.
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 using multiple data types, but different data types have different operation costs. Choosing the appropriate data type can help optimize Gas usage.
For example, in Solidity, integers can be further divided 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 consumption.

We can compare the Gas cost of uint8 and uint256 through the code in the figure. The UseUint() function consumes 120,382 Gas units, while the UseUInt8() function consumes 166,111 Gas units.
Taken alone, using uint256 is cheaper than uint8. However, when combined with the variable packing optimization we mentioned earlier, the story is different. If developers can pack four uint8 variables into a single storage slot, the total cost of iterating through them will be lower than four uint256 variables. In this way, the smart contract can read and write a single storage slot and place the four uint8 variables in memory/storage in a single 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 ones. If the byte length can be limited, choose the smallest length from bytes1 to bytes32 as much as possible.


5. Mappings vs. Arrays
Solidity's data lists can be represented using two data types: Arrays and Mappings, but their syntax and structure are quite different.
Mappings are more efficient and cost-effective in most cases, but Arrays have iterability 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 the function parameters are read-only, you should use calldata instead of memory. This can avoid unnecessary copy operations from function calldata to memory.
Example 1: Using memory

When using the memory keyword, the array's values will be 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: Using calldata
Here is the English translation of the text, with the specified terms translated and the content within <> tags left unchanged:
When directly reading values from calldata, skip the intermediate memory operation. This optimization method reduces the execution cost to only 2,413 Gas units, improving Gas efficiency by 35%.
7. Use Constant/Immutable keywords as much as possible
Constant/Immutable variables are not stored in the contract's storage. These variables are calculated 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 Constant or Immutable keywords as much as possible.
8. Use Unchecked when you are sure no overflow/underflow will occur
When the developer can ensure that the arithmetic operation 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, thereby saving Gas cost.
In the figure below, the variable i will never overflow due to the constraint iunchecked block is considered safe and more Gas-efficient.

Additionally, the compiler version 0.8.0 and above no longer requires the use of the SafeMath library, as the compiler itself has built-in overflow and underflow protection.
9. Optimize modifiers
The code of a modifier is inlined into the modified function, and the modifier's code is copied each time it is used. This increases the bytecode size and raises Gas consumption. Here is a method to optimize the Gas cost of modifiers:
Before optimization:

After optimization:

In this example, by refactoring the logic into the internal function _checkOwner(), the modifier can reuse this internal function, reducing the bytecode size and lowering the Gas cost.
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, the conditions with lower computation cost should be placed first, so that the more expensive computations can potentially be skipped.
Additional General Recommendations
1. Remove unused code
If there are unused functions or variables in the contract, it is recommended to remove them. This is the most direct way to reduce contract deployment cost and keep the contract size small.
Here are some practical suggestions:
- Use the most efficient algorithms for calculations. If the contract directly uses the results of certain calculations, the redundant calculation processes should be removed. Essentially, any unused computations should be deleted.
- In Ethereum, developers can receive Gas rewards by releasing storage space. If a variable is no longer needed, it should be deleted using the delete keyword or set to the default value.
- Loop optimization: Avoid high-cost loop operations, try to merge loops, and move repeated calculations out of the loop body.
2. Use precompiled contracts
Precompiled contracts provide complex library functions, such as cryptographic and hashing operations. Since the code is not executed on the EVM, but rather on the client node locally, less Gas is required. Using precompiled contracts can reduce the computational workload required to execute smart contracts, thereby saving Gas.
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 efficiency of their applications.
For a complete list of precompiled contracts supported by the Ethereum network, please refer to [4].
3. Use inline assembly
Inline assembly allows developers to write low-level yet efficient code that can be directly executed by the EVM, without using expensive Solidity opcodes. Inline assembly also allows for more precise control of memory and storage usage, further reducing Gas fees. Additionally, inline assembly can perform some complex operations that are difficult to achieve using Solidity alone, providing more flexibility for Gas optimization.
Here is an example of using inline assembly to save Gas:

As shown in the image, the second use case, which employs inline assembly techniques, has higher Gas efficiency compared to the standard use case.
However, using inline assembly also carries risks and is prone to errors. Therefore, it should be used with caution and only by 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 main Ethereum chain.
Layer 2 solutions like rollups, sidechains, and state channels can offload transaction processing from the main Ethereum chain, enabling faster and cheaper transactions. By bundling many transactions together, these solutions reduce the number of on-chain transactions, thereby lowering Gas fees. Using Layer 2 solutions can also improve Ethereum's scalability, allowing more users and applications to participate in the network without causing network congestion.
5. Use optimization tools and libraries
There are several optimization tools available, such as the solc optimizer, Truffle's build optimizer, and the Solidity compiler in Remix.

These tools can help minimize the size of the bytecode, remove unused code, and reduce the number of operations required to execute smart contracts. Combined with other Gas optimization libraries, such as "solmate", developers can effectively reduce Gas costs and improve the efficiency of their 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, leveraging inline assembly, and following the other best practices discussed in this text, developers can effectively lower the Gas consumption of their contracts.
However, it is crucial to note that during the optimization process, developers must exercise caution to avoid introducing security vulnerabilities. The process of optimizing code and reducing Gas consumption should never compromise the inherent security of the smart contract.
[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



