By following these practices, developers can reduce Gas consumption in smart contracts, lower transaction costs, and create more efficient and user-friendly applications.
Gas fees on the Ethereum mainnet have always been a major issue, especially during periods of network congestion. During peak times, users often need to pay extremely high transaction fees. Therefore, optimizing Gas costs during the smart contract development phase is crucial. Gas optimization can not only effectively reduce transaction costs but also 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), core concepts related to Gas fee optimization, and best practices for optimizing Gas fees when developing smart contracts. It is hoped that this content will inspire and assist developers, while also helping ordinary users better understand how the EVM Gas fee system works, together addressing the challenges within the blockchain ecosystem.
In EVM-compatible networks, “Gas” refers to the unit used to measure the computational power required to execute specific operations.
The diagram below illustrates the structure 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 activation of EIP-1559 (London Hard Fork), Gas fees are 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 increases the likelihood of the transaction being included in the next block. This is similar to a “tip” paid by users to the validators.
When compiling a smart contract with Solidity, the contract is converted into a series of “operation codes,” or opcodes.
Each opcode (such as creating a contract, making message calls, accessing account storage, and executing operations on the virtual machine) has an associated Gas consumption cost, which is documented in the Ethereum Yellow Paper[2].
After multiple EIP modifications, the Gas costs of some opcodes have been adjusted, which may differ from the values in the Yellow Paper. For detailed information on the latest costs of opcodes, please refer to this source[3].
The core concept of Gas optimization is to prioritize cost-efficient operations on the EVM blockchain and avoid operations that incur high Gas costs.
In the EVM, the following operations are relatively low-cost:
High-cost operations include:
Based on the above basic concepts, we have compiled a list of Gas fee optimization best practices for the developer community. By following these practices, developers can reduce the Gas consumption of smart contracts, lower transaction costs, and create more efficient and user-friendly applications.
In Solidity, Storage is a limited resource, and its Gas consumption is significantly higher than Memory. Each time a smart contract reads from or writes to storage, it incurs a high Gas cost.
According to the definition in the Ethereum Yellow Paper, the cost of storage operations is more than 100 times higher than memory operations. For example, opcodes like sload and sstore cost at least 100 Gas units in the best-case scenario, whereas memory operations like mload and mstore only consume 3 Gas units.
Methods to Limit Storage Usage Include:
The number of Storage slots used in a smart contract and how developers represent data can significantly impact Gas consumption.
The Solidity compiler packs consecutive storage variables during the compilation process, using 32-byte storage slots as the basic unit for variable storage. Variable packing refers to the practice of arranging variables in a way that allows multiple variables to fit into a single storage slot.
On the left is a less efficient implementation that consumes 3 storage slots; on the right is a more efficient implementation.
By making this adjustment, developers can save 20,000 Gas units (as storing an unused storage slot costs 20,000 Gas), but now only two storage slots are required.
Since each storage slot consumes Gas, variable packing optimizes Gas usage by reducing the number of required storage slots.
A variable can be represented using different data types, but the operation costs vary depending on the type. 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, and this conversion incurs additional Gas costs.
We can compare the Gas costs of uint8 and uint256 using the code in the diagram. The UseUint() function consumes 120,382 Gas units, while the UseUInt8() function consumes 166,111 Gas units.
On its own, using uint256 is cheaper than uint8. However, if we apply the previously suggested variable packing optimization, it makes a difference. If developers can pack four uint8 variables into a single storage slot, the total cost of iterating over them will be lower than using four uint256 variables. In this case, the smart contract can read and write the storage slot once, and load all four uint8 variables into memory/storage in a single operation.
If the data can be constrained to 32 bytes, it is recommended to use the bytes32 data type instead of bytes or strings. Generally, fixed-size variables consume less Gas than dynamically sized variables. If the byte length can be limited, try to choose the smallest length from bytes1 to bytes32.
In Solidity, data lists can be represented using two data types: Arrays and Mappings, each with distinct syntax and structure.
Mappings are generally more efficient and cost-effective in most cases, while 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.
Variables declared in function parameters can be stored in either calldata or memory. The main difference is that memory can be modified by the function, while calldata is immutable.
Keep this principle in mind: if function parameters are read-only, prefer using calldata instead of memory. This avoids unnecessary copy operations from function calldata to memory.
Example 1: Using memory
When using the memory keyword, the values of the array are copied from the encoded calldata to memory during ABI decoding. The execution cost of this code block is 3,694 Gas units.
Example 2: Using calldata
When reading values directly from calldata, the intermediate memory operation is skipped. This optimization reduces the execution cost to only 2,413 Gas units, resulting in a 35% improvement in Gas efficiency.
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 compared to storage variables. It is recommended to use the Constant or Immutable keywords whenever possible.
When developers can be certain that arithmetic operations will not result in 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 diagram below, the conditionally constrained i
In addition, compiler versions 0.8.0 and above no longer require the use of the SafeMath library, as the compiler itself now includes built-in overflow and underflow protection.
The code of modifiers is embedded into the functions they modify. Each time a modifier is used, its code is duplicated, which increases the bytecode size and raises Gas consumption. Here’s one way to optimize the Gas cost of modifiers:
Before optimization:
After optimization:
In this example, by refactoring the logic into an internal function _checkOwner(), which can be reused in the modifier, the bytecode size is reduced and Gas costs are lowered.
For || (OR) and && (AND) operators, logical operations are evaluated with short-circuiting, meaning that if the first condition is enough to determine the result of the logical expression, the second condition will not be evaluated.
To optimize Gas consumption, conditions with lower computation costs should be placed first, so that potentially expensive calculations can be skipped.
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 the contract directly uses certain calculation results, redundant calculations should be removed. Essentially, any unused calculations 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 its default value.
Loop Optimization: Avoid high-cost loop operations, try to merge loops, and move repeated calculations out of the loop body.
Precompiled contracts provide complex library functions such as cryptography and hashing operations. Since the code is not executed on the EVM but runs locally on the client node, less Gas is required. Using precompiled contracts can save Gas by reducing the computational workload required to execute the smart contract.
Examples of precompiled contracts include the Elliptic Curve Digital Signature Algorithm (ECDSA) and the SHA2-256 hashing algorithm. By using these precompiled contracts in smart contracts, developers can reduce Gas costs and improve the efficiency of the application.
For a complete list of precompiled contracts supported by the Ethereum network, refer to this link [4].
Inline assembly allows developers to write low-level but efficient code that can be directly executed 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. Additionally, inline assembly can perform some complex operations that are difficult to implement with Solidity alone, offering more flexibility for optimizing Gas consumption.
Here is an example of using inline assembly to save Gas:
As seen in the above example, the second case, which uses inline assembly, has higher Gas efficiency compared to the standard case.
However, using inline assembly can also introduce risks and is prone to errors. Therefore, it should be used cautiously and is recommended only for experienced developers.
Layer 2 solutions can reduce the amount of data that needs to be stored and computed on the Ethereum mainnet.
Layer 2 solutions like rollups, sidechains, and state channels 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, which in turn lowers Gas fees. Using Layer 2 solutions also enhances Ethereum’s scalability, allowing more users and applications to participate in the network without causing congestion from overload.
There are several optimization tools available, such as the solc optimizer, Truffle’s build optimizer, and Remix’s Solidity compiler.
These tools can help minimize the bytecode size, remove unused code, and reduce the number of operations required to execute smart contracts. Combined with other Gas optimization libraries like “solmate,” developers can effectively lower Gas costs and improve the efficiency of smart contracts.
Optimizing Gas consumption is an important step for developers, as it not only minimizes transaction costs but also improves 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 Gas consumption of contracts.
However, it is important to note that during the optimization process, developers must exercise caution 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 compromised.
[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
Mời người khác bỏ phiếu
By following these practices, developers can reduce Gas consumption in smart contracts, lower transaction costs, and create more efficient and user-friendly applications.
Gas fees on the Ethereum mainnet have always been a major issue, especially during periods of network congestion. During peak times, users often need to pay extremely high transaction fees. Therefore, optimizing Gas costs during the smart contract development phase is crucial. Gas optimization can not only effectively reduce transaction costs but also 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), core concepts related to Gas fee optimization, and best practices for optimizing Gas fees when developing smart contracts. It is hoped that this content will inspire and assist developers, while also helping ordinary users better understand how the EVM Gas fee system works, together addressing the challenges within the blockchain ecosystem.
In EVM-compatible networks, “Gas” refers to the unit used to measure the computational power required to execute specific operations.
The diagram below illustrates the structure 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 activation of EIP-1559 (London Hard Fork), Gas fees are 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 increases the likelihood of the transaction being included in the next block. This is similar to a “tip” paid by users to the validators.
When compiling a smart contract with Solidity, the contract is converted into a series of “operation codes,” or opcodes.
Each opcode (such as creating a contract, making message calls, accessing account storage, and executing operations on the virtual machine) has an associated Gas consumption cost, which is documented in the Ethereum Yellow Paper[2].
After multiple EIP modifications, the Gas costs of some opcodes have been adjusted, which may differ from the values in the Yellow Paper. For detailed information on the latest costs of opcodes, please refer to this source[3].
The core concept of Gas optimization is to prioritize cost-efficient operations on the EVM blockchain and avoid operations that incur high Gas costs.
In the EVM, the following operations are relatively low-cost:
High-cost operations include:
Based on the above basic concepts, we have compiled a list of Gas fee optimization best practices for the developer community. By following these practices, developers can reduce the Gas consumption of smart contracts, lower transaction costs, and create more efficient and user-friendly applications.
In Solidity, Storage is a limited resource, and its Gas consumption is significantly higher than Memory. Each time a smart contract reads from or writes to storage, it incurs a high Gas cost.
According to the definition in the Ethereum Yellow Paper, the cost of storage operations is more than 100 times higher than memory operations. For example, opcodes like sload and sstore cost at least 100 Gas units in the best-case scenario, whereas memory operations like mload and mstore only consume 3 Gas units.
Methods to Limit Storage Usage Include:
The number of Storage slots used in a smart contract and how developers represent data can significantly impact Gas consumption.
The Solidity compiler packs consecutive storage variables during the compilation process, using 32-byte storage slots as the basic unit for variable storage. Variable packing refers to the practice of arranging variables in a way that allows multiple variables to fit into a single storage slot.
On the left is a less efficient implementation that consumes 3 storage slots; on the right is a more efficient implementation.
By making this adjustment, developers can save 20,000 Gas units (as storing an unused storage slot costs 20,000 Gas), but now only two storage slots are required.
Since each storage slot consumes Gas, variable packing optimizes Gas usage by reducing the number of required storage slots.
A variable can be represented using different data types, but the operation costs vary depending on the type. 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, and this conversion incurs additional Gas costs.
We can compare the Gas costs of uint8 and uint256 using the code in the diagram. The UseUint() function consumes 120,382 Gas units, while the UseUInt8() function consumes 166,111 Gas units.
On its own, using uint256 is cheaper than uint8. However, if we apply the previously suggested variable packing optimization, it makes a difference. If developers can pack four uint8 variables into a single storage slot, the total cost of iterating over them will be lower than using four uint256 variables. In this case, the smart contract can read and write the storage slot once, and load all four uint8 variables into memory/storage in a single operation.
If the data can be constrained to 32 bytes, it is recommended to use the bytes32 data type instead of bytes or strings. Generally, fixed-size variables consume less Gas than dynamically sized variables. If the byte length can be limited, try to choose the smallest length from bytes1 to bytes32.
In Solidity, data lists can be represented using two data types: Arrays and Mappings, each with distinct syntax and structure.
Mappings are generally more efficient and cost-effective in most cases, while 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.
Variables declared in function parameters can be stored in either calldata or memory. The main difference is that memory can be modified by the function, while calldata is immutable.
Keep this principle in mind: if function parameters are read-only, prefer using calldata instead of memory. This avoids unnecessary copy operations from function calldata to memory.
Example 1: Using memory
When using the memory keyword, the values of the array are copied from the encoded calldata to memory during ABI decoding. The execution cost of this code block is 3,694 Gas units.
Example 2: Using calldata
When reading values directly from calldata, the intermediate memory operation is skipped. This optimization reduces the execution cost to only 2,413 Gas units, resulting in a 35% improvement in Gas efficiency.
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 compared to storage variables. It is recommended to use the Constant or Immutable keywords whenever possible.
When developers can be certain that arithmetic operations will not result in 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 diagram below, the conditionally constrained i
In addition, compiler versions 0.8.0 and above no longer require the use of the SafeMath library, as the compiler itself now includes built-in overflow and underflow protection.
The code of modifiers is embedded into the functions they modify. Each time a modifier is used, its code is duplicated, which increases the bytecode size and raises Gas consumption. Here’s one way to optimize the Gas cost of modifiers:
Before optimization:
After optimization:
In this example, by refactoring the logic into an internal function _checkOwner(), which can be reused in the modifier, the bytecode size is reduced and Gas costs are lowered.
For || (OR) and && (AND) operators, logical operations are evaluated with short-circuiting, meaning that if the first condition is enough to determine the result of the logical expression, the second condition will not be evaluated.
To optimize Gas consumption, conditions with lower computation costs should be placed first, so that potentially expensive calculations can be skipped.
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 the contract directly uses certain calculation results, redundant calculations should be removed. Essentially, any unused calculations 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 its default value.
Loop Optimization: Avoid high-cost loop operations, try to merge loops, and move repeated calculations out of the loop body.
Precompiled contracts provide complex library functions such as cryptography and hashing operations. Since the code is not executed on the EVM but runs locally on the client node, less Gas is required. Using precompiled contracts can save Gas by reducing the computational workload required to execute the smart contract.
Examples of precompiled contracts include the Elliptic Curve Digital Signature Algorithm (ECDSA) and the SHA2-256 hashing algorithm. By using these precompiled contracts in smart contracts, developers can reduce Gas costs and improve the efficiency of the application.
For a complete list of precompiled contracts supported by the Ethereum network, refer to this link [4].
Inline assembly allows developers to write low-level but efficient code that can be directly executed 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. Additionally, inline assembly can perform some complex operations that are difficult to implement with Solidity alone, offering more flexibility for optimizing Gas consumption.
Here is an example of using inline assembly to save Gas:
As seen in the above example, the second case, which uses inline assembly, has higher Gas efficiency compared to the standard case.
However, using inline assembly can also introduce risks and is prone to errors. Therefore, it should be used cautiously and is recommended only for experienced developers.
Layer 2 solutions can reduce the amount of data that needs to be stored and computed on the Ethereum mainnet.
Layer 2 solutions like rollups, sidechains, and state channels 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, which in turn lowers Gas fees. Using Layer 2 solutions also enhances Ethereum’s scalability, allowing more users and applications to participate in the network without causing congestion from overload.
There are several optimization tools available, such as the solc optimizer, Truffle’s build optimizer, and Remix’s Solidity compiler.
These tools can help minimize the bytecode size, remove unused code, and reduce the number of operations required to execute smart contracts. Combined with other Gas optimization libraries like “solmate,” developers can effectively lower Gas costs and improve the efficiency of smart contracts.
Optimizing Gas consumption is an important step for developers, as it not only minimizes transaction costs but also improves 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 Gas consumption of contracts.
However, it is important to note that during the optimization process, developers must exercise caution 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 compromised.
[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