Alpha: Gas optimization Hacks

0xAdnan
CoinsBench
Published in
6 min readAug 27, 2023

--

This comprehensive guide will point you to strategies for minimizing gas usage for your Ethereum smart contracts.

Mastery of this topic is critical because high deployment costs can affect blockchain adoption and execution costs can deter users from interacting with your decentralized application (Dapp).

We’ve consolidated insights from various reliable sources, credited towards the end, to ensure you have the most effective tactics at your disposal.

These tips will be classified under categories D and E, with D representing strategies to save on deployment costs, and E focusing on methods to minimize execution costs in your smart contracts.

Point #1: Use Solidity Optimization (D || E)

In blockchain development environments like Hardhat, Truffle, or Foundry, there is the ability to activate the optimizer and specify the number of its runs.

// In hardhat.config.js
module.exports = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 1000, <------------------
},
},
},
};

As the Solidity official documentation explains, there exists a trade-off between the number of runs and the associated costs of deployment and execution. Fewer runs can result in lower deployment costs at the expense of higher execution costs. Conversely, a larger number of runs may increase the deployment costs but considerably reduce the cost of execution.

The number of runs (--optimize-runs) specifies roughly how often each opcode of the deployed code will be executed across the life-time of the contract. This means it is a trade-off parameter between code size (deploy cost) and code execution cost (cost after deployment). A “runs” parameter of “1” will produce short but expensive code. In contrast, a larger “runs” parameter will produce longer but more gas efficient code. — Solidity Documentation — The Optimizer

Point #2: Use Immutable and Constant Keywords (D && E)

When a project uses variables to store data, it’s advisable to use the constant keyword if the value is identifiable during code development and not applied to changes within the life cycle of the contract, or the immutable keyword if the value will be known during contract deployment (To be set in the constructor). In this way, the content of the variable is stored in the bytecode of the contract, not in the storage.

The deployment cost can be slightly decreased through this method, but more importantly, it significantly reduces execution costs. This is because the value of the variable is retrieved directly from the bytecode, not from the storage, thereby saving the gas associated with the SLOAD operation, as it avoids the costly process of reading from storage.

Point #3: Delete unused variables (E)

Within EVM blockchains, a gas refund is granted when storage space is cleared. The act of deleting a variable gives a big amount of gas refund, capped at half the gas expense of the entire transaction.

Using the delete keyword to remove a variable is equivalent to setting the variable back to its initial value specific to its data type, like zero for integers or bytes32(0) for bytes32.

I’ll have another article soon specifically related to Gas Refund, current and historical values.

Point #4: Pack storage variables (D)

Data in the EVM is stored within 256-bit memory slots. Data smaller than 256 bits are contained within a single slot, while larger data is distributed across multiple slots. Since updating every storage slot consumes gas, variable packing can optimize gas usage by reducing the number of slots to update within a contract.

For instance, the Solidity compiler can only store four uint64 variables within the same 256-bit memory slots if they are packed. If not, each uint64 variable will occupy an individual uint256 slot, thereby wasting the rest of the bits per slot.

Point #5: Avoid storing unnecessary data onchain (E)

For cases where data isn’t crucial or doesn’t require accessibility by other smart contracts, it’s acceptable and often beneficial to emit this data in the form of an event. Emitting data in events allows it to be documented in the blockchain’s historical blocks without making it accessible to other smart contracts.

Events may be not reliable in the future to access historical values but rather accessing the data instantly as Ethereum may introduce chain-pruning. Pruning is a process that involves erasing older data to optimize disk space utilization. By implementing incremental pruning, Ethereum clients can efficiently manage their storage needs while still maintaining the necessary historical data required for network operations.

Point #6: Use Assembly (D && E)

When you compile a Solidity smart contract, it is transformed into a series of EVM (Ethereum virtual machine) opcodes.
With assembly, you write code very close to the opcode level. It’s not very easy to write code at such a low level, but the benefit is that you can manually optimize the opcode and outperform Solidity bytecode in some cases.

Point #7: Use Calldata instead of Memory (E)

For function parameters, use calldata instead of memory, as when memory is used the variables are copied to the memory, making it more runtime expensive. However, if calldata is used, the content of the variable will not be copied and just be read from the calldata.

Point #8: Use Mapping instead of Arrays (E)

Solidity offers two distinct data types, arrays, and mappings, for representing lists of data. Nonetheless, their syntax and structure diverge significantly.

While mappings tend to be more efficient and economical in the majority of scenarios, arrays boast iterability and packability. Consequently, the recommendation is to employ mappings to handle data lists, unless situations demand iteration or the feasibility of data type packing.

Point #9: Cache Storage Variables Before Using Them (E)

It is essential to cache storage variables in the memory before using them (use them several times or iterate through them), as each time a storage variable is read or written to, it uses SLOAD/SSTORE respectively.

However, when the value is cached, it uses MLOAD/MSTORE which is way cheaper than the one mentioned above.

Point #9: Use EIP-1167 Minimal Proxies (D)

When a contract is expected to be deployed a lot of times, it is expected that the contract should be deployed using the Proxies pattern, one of the cheapest proxy patterns in terms of deployment saving is EIP-1167 Minimal Proxy, where a contract will have a very minimal bytecode that delegatecalls any calldata it receives to the hardcoded implementation contract.

Note: The use of EIP-1167 proxies will be slightly more expensive in terms of runtime cost, as in each time a call is made, the cost of delegatecall to the implementation contract is added.

Point #10: Use Custom Errors (D)

Custom Errors were introduced in solidity 0.8.4, as a replacement for string errors, which are more expensive.

Point #11: Limit Error String to less than 32 characters (D)

If string errors are used, and not custom errors, one way to optimize the use of the string errors is to make the error string less than 32 characters.

Point #12: Use unchecked (E)

After solidity 0.8.0, built-in checks are introduced in the compiler to avoid overflow and underflow. These checks cost gas, when the developer is sure that the variable will not reach the value that will make it overflow or underflow, it is possible to wrap that code in an unchecked block.

For example, for loops that use uint256 for incrementing, it is not possible to reach the max uint256. In this way, you can wrap the iterator within an unchecked block

Point #13: Use ++i instead of i++ (E)

++i uses less gas than i++. However, both return different values in execution.

Point #15: Use Vyper (E)

It’s worth noting that the use of Vyper can often generate highly optimized execution code, sometimes even surpassing the optimization level of code written in Yul, the assembly language of Solidity. Vyper is a contract-oriented, pythonic programming language that targets the Ethereum Virtual Machine (EVM).

Piece of advice:

  • Use a gas reporter to track the gas cost of the code you write
  • Watch out for the code size limit defined in the EIP-170
  • Not all optimization that is written online is true, the compiler sometimes optimizes for some edge cases.

Connect With me on Github, Twitter, and LinkedIn.

--

--