
Introduction
To overcome these limitations, several Layer 2 solutions such as Base have been developed and are controlled by scalability and decreased gas expenses. Although L2s have significant fee cuts over Ethereum mainnet, developers of smart contracts cannot ignore gas optimization in their development.
This tutorial will endeavor to consolidate user experience and build more competitive, decentralized applications through advanced strategies that lower the cost of gas on smart contracts that will be tested. The examples they provide are simulated with simple contracts and mainly take into account runtime gas prices, since deployment costs differ greatly depending on the size of a contract.
Gas optimization is significant to developers, users, and long run project success. The economic utilization of smart contract gas can ensure that protocols are more cost-effective and scalable and less prone to security threats like denial of service attacks.
The practical application of smart contracts needs comprehensive and systematic auditing of every smart contract.
The Significance of Solidity Gas Optimization
Gas optimization allows smart contracts, protocols, and projects to work under congested networks conditions and make them both cheap and efficient. By enhancing contract code, there is a possibility of revealing possible vulnerabilities and protocols and users are more assured.
Gas optimization is also an important focus of the development. It is not merely a pleasant-to-have attribute but a key of smart contract long term success and security. The fact that it is possible to build on L2s with comparably low fees does not imply that gas fees will not be significantly higher than competitors.
All tests are making use of Foundry and Solidity version 0.8.13, local blockchain node Anvil, forge test commands, and 100 optimization runs.
Professional auditing firms should be involved in full security checks, this guide should be an auxiliary one.
Reduce On-Chain Data
Non-Fungible Tokens became popular in 2021 and attracted the interest in fully on-chain NFTs. On-chain NFTs, as compared to traditional NFTs that use off-chain data, including metadata and image references, place all the information directly on the blockchain.
These tokens are infamously costly to communicate with and hybrid solutions are the default when users were affected by fees. Regardless of whether you are building NFTs, games, or DeFi protocols, you should always think about what data is actually required to be stored on blockchain and trade-offs to both alternatives.
Dramatically decrease the use of gas by storing information off-chain, requiring less storage of variables. One specific method is to use events to store data off-chain rather than on the actual on-chain storage.
Transaction gas costs are raised by additional emit functions incurred by events; but information not stored on-chain, savings usually exceed costs.
Consider a smart contract which lets users vote true or false. The former one is one that stores the user votes in on-chain structs. A demonstration of the vote function test with Foundry with over 100 iterations displays certain gas costs.
Compare with a smart contract that would not store information on-chain but would emit an event when the vote function is invoked. A test involving Foundry more than 100 times in the new vote functionality provides results.
Reduced on-chain data reduced the average gas by 90.34%.
In order to access off-chain data on-chain, Chainlink functions, support solutions, are available to integrate with most popular L2 networks, such as Base.
Use Mappings Over Arrays
There are two main data structures in Solidity: arrays and mappings.
- Arrays are used to store sets of items that are attributed to certain indices
- Key-value Mappings are data structures that support direct access to data using unique keys
Although arrays could be useful in the storage of vectors and other similar data, mappings are usually preferred due to their gas efficiency. They are especially optimally adapted to situations in which on-demand data is needed, e.g., by name, wallet address or account balance.
To comprehend gas usage when utilizing arrays or mappings, one may have to consult gas used by associated EVM opcodes. Opcodes are low-level instructions the Ethereum Virtual Machine executes in the execution of smart contracts, with each opcode having a gas cost.
To access the values of the array, it must pay per gas unit used by the EVM opcodes. Storing the user addresses and the corresponding balances in the array form necessitates looping all the items in the array, checking whether the userAddress and the argument which is given match and returning the balance in case of a match.
Moving directly to the specific balance of the user would avoid the necessity of going through all the items in the array. Once the arrays were replaced with mappings, 89 percent less gas was used to access data.
Gas Usage Comparison: Arrays vs Mappings
| Method | Before Optimization | After Optimization | Gas Savings |
|---|---|---|---|
| Data Access | 30,586 | 3,081 | 89% |
| Data Addition | - | - | 93% |
Use Constant and Immutable to Reduce Smart Contract Gas Costs
Another optimization tip would be to use constants and immutable variables. In stating variables as immutable or constant, their values are provided only at the time of contract creation and no longer.
They do not occupy storage space in the EVM as compared to other variables. It is possible to directly encode values in smart contract bytecode, which means the cost of storing data is reduced by the fact that variables like maxSupply and owner do not have the constant or immutable keywords.
When tests are run 100 times, average cost of gas is 112,222 units. Because maxSupply and owner are known values that are not intended to be changed, declaring the maximum supply and owner that do not use storage space is the best choice.
The constant or immutable variable tests result in 35.89% average savings (from 112,222 to 71,940 gas units).
Optimize Unused Variables
It is an obvious piece of gas optimization advice to optimize variables in smart contracts. Non-useful variables are however in most cases retained in the execution of the contract leading to unnecessary use of gas.
Removal of single unused variables can be used to save gas costs, on average, 18% when considering contracts with unused variables defined and manipulated in functions but never called elsewhere.
Unused Variables Optimization Results
| Optimization Stage | Gas Usage | Savings |
|---|---|---|
| Before Optimization | 32,513 | - |
| After Optimization | 27,429 | 18% |
Solidity Gas Refund: Deleting Unused Variables
Eliminating unused variables is an attempt to assign default values to variables after they have been fully computed without data being stored in memory. As an illustration, the default of the variables of the type of uint is 0.
By not destroying the variables of the function when the functions terminate, average gas costs are 100,300 units to assign variables to data. On average 19% gas savings using the delete key to remove data variables (to set variables to 0).
Use Fixed-Sized Arrays Over Dynamic Arrays to Minimise Smart Contract Gas Costs
Use mappings wherever possible in order to reduce smart contract gas costs. Nevertheless, where arrays are required, fixed sized arrays are more economical in terms of gas than dynamically sized arrays since dynamic sized arrays can be expanded indefinitely leading to higher gas costs.
Dynamic arrays can be expanded, and thus the EVM needs to monitor lengths and will need to refresh them when new items are added.
Consider code that defines dynamically sized arrays and updates them with updateArray functions. Require statements will make sure that indices supplied are within an array range of fixed size. When 100 repetitions of a test are run, the average amount of gas that was used is 12,541.
Changing arrays to fixed size 5 creates fixed-size arrays of length 5, type uint256. The EVM is aware that fixedArray with state variable size 5 has 5 slots, and the length is not stored in the storage. Replacing dynamic with fixed arrays saves 17.99% gas.
Optimize Your Smart Contracts Today
Start implementing these gas optimization techniques to reduce costs by up to 90% on Base and other L2 chains.
Using uint8 as a Replacement of uint256
This is less efficient and can be more expensive to use than uint256 because of the operation of the EVM. The EVM has 256-bit word sizes. Operations with 256-bit integers (uint256) tend to be the most efficient, because they fit the EVM word size, whereas smaller types (such as uint8) require the Solidity compiler to create additional operations to fit smaller types into a single 256-bit storage slot.
In general, while small types such as uint8 may be beneficial when it comes to storage (a number of small types can be packed into one operation with a 256-bit storage slot), it has been seen that smaller types only help with storage. Storage savings can be nullified by converting to and from uint256 to do computations.
Combining Smaller Than 256-Bit Variables
Combining variables that are less than 256-bit in size is usually not as efficient as 256-bit variables. Non-interoperable situations however compel less powerful types to be used in smaller sizes, e.g. booleans which consume 1 byte or 8-bit to be stored.
When using less powerful types, it is possible to declare state variables with storage space in mind, and Solidity can pack them together and store them using the same storage slot. The advantage of variable packing is usually considered in the storage operations, rather than in the memory operations or stack operations.
Since two-way combination sizes are 16 bits, or 240 bits smaller than the capacity to store a single storage slot, Solidity can be used to pack variables into the same slot to minimize deployment gas utilization by not requiring many slots to store state variables.
Rearranging variable declarations enables optimization of 13% gas on average.
Variable Packing Results
| Stage | Gas Usage | Savings |
|---|---|---|
| Pre-optimization | 1,678 | - |
| Post-optimization | 1,447 | 13% |
Select External Visibility Modifier
To maximize smart contract gas, it is reasonable to select the right visibility of functions. External visibility modifiers may be more efficient in gas usage than public because of the way that a public function manages its arguments, and of the manner in which data is transferred to the functions.
External functions can access calldata, which is a read-only temporary space in the EVM that contains the parameters of a function call. Public functions may be called:
- Externally (when they are called externally by a transaction using calldata)
- Internally (when they are called within a contract using calldata)
Since functions are public, they need to receive arrays in memory which can be expensive in gas cost in case it is large. Switching functions to external enables the reception of arrays in calldata, which is more cost-effective in situations involving large arrays, saving an average of 0.3 percent of gas units per call.
Storage to Re-read the same value
A good technique to save gas is not to read the same value in storage many times. Reading out of storage is higher in cost than reading in memory. Re-reading the same variables in storage many times, and writing to storage when it is not necessary can be a waste of gas in smart contracts.
This can be prevented by defining variables that are in memory at the start of functions, and giving them values of numbers variables.
This saves 17% of the gas consumed by sumNumbers functions, but the larger the array, the larger will be the iteration numbers and the gas used.
Caching Variables Results
| Stage | Gas Usage | Savings |
|---|---|---|
| Before Optimization | 3,527 | - |
| After Optimization | 2,905 | 17% |
Do not Initialize Variables to Default Values
When declaring variables of state without initializing them (that is, not assigning them an initial value), these variables are automatically initialized to default values:
- uint: 0
- bool: false
- address: address(0)
The latter is cheaper than declaring values to be default and simply updating them when the user interacts with the system, which is not gas efficient. With no variable initialisation, optimization indicates a 4% average gas savings.
Support Solidity Compiler Optimization
Solidity includes compilers with settings that are easy to alter in order to optimize compiled code. The optimize build runs a number of hundred times, optimizing code, and converting it to cheaper forms that require fewer gas to run.
Compilers can be adjusted to achieve the appropriate tradeoffs between deployment and runtime costs. Defining the estimated number of contracts to run with the use of the run commands:
- Greater levels are optimal in terms of low gas prices when enforcing the contract
- Fewer numbers are optimal in terms of reduced gas costs in contract deployment
Optimize flags plus the value of run=200 tells compilers to make code optimal to save gas when running incrementCount functions 200 times. Categorize such settings according to application unique requirements.
Bonus Solidity Gas Optimization: Assembly
When writing smart contracts in Solidity, it is compiled into bytecodes, a sequence of EVM opcodes. Through assembly, you can simply write code running at levels more compatible with opcodes, and in some cases, the ability to manually optimize opcodes gives it an advantage over Solidity bytecode.
Although it is not the simplest task to write code at such low level, it has the benefit of being able to optimize opcodes manually, and is therefore generally better compared to Solidity bytecode. This level of optimization gives it more efficiency and effectiveness in the execution of the contract.
Even in simple cases where the two functions are supposed to add two numbers, plain solidity and assembly versions will have some differences, but assembly versions are cheaper.
The implementation of projects on L2s like Base will mean that the users will have lesser cost to use protocols but it is the duty of the developers to ensure that they implement the gas optimization techniques. These hints can save costs of transactions significantly, add scalability, and enhance efficiency of the contract in general.
Assembly may be used to optimize the execution of smart contracts using gas, there may be cases where the Assembly versions will result in the creation of insecure code. There are solid suggestions to engage the smart contract security experts in the review of the contracts before their deployment.


