Recommended resources
The freely available resources this lesson will direct you to.
Hello! In our last lesson, we mastered the art of measuring costs by estimating deployment and transaction fees using tools like testnet deployments, gas trackers, and hardhat-gas-reporter. This gave you a clear picture of the economic footprint of your smart contracts.
Today, we'll build directly on that knowledge. Now that you know how to measure cost, it's time to learn how to actively reduce it. Our goal is to apply basic gas optimization techniques to your Solidity code. For your token to be successful, it needs to be affordable for users to acquire, transfer, and interact with. High gas fees are a major barrier to adoption, so making your contracts efficient is not just a technical exercise—it's a critical business decision.
We'll explore the most impactful strategies, focusing on the "why" behind each one. Given your background as a developer, you're familiar with optimizing for performance, like CPU time or memory usage. Here, we'll translate that mindset to optimizing for a direct monetary cost, which is a core paradigm of blockchain development.
The Hierarchy of Cost in the EVM
Before diving into specific techniques, it's crucial to understand one fundamental concept: not all operations are created equal in the Ethereum Virtual Machine (EVM). There's a clear and dramatic hierarchy of cost.

The most important takeaway is the cost difference between storage, memory, and computation:
- Storage (Most Expensive): Interacting with the blockchain's state. The
SSTOREopcode, which writes or changes a value in a contract's storage, is one of the most expensive operations in the EVM. Reading from storage withSLOADis also costly, though less so than writing. - Memory (Cheap): A temporary, volatile data area that exists only for the duration of a function's execution. Reading from and writing to memory is significantly cheaper than storage.
- Stack/Computation (Cheapest): The EVM uses a stack for most computations (e.g., arithmetic like
ADD,MUL). These are the cheapest operations of all.
The Golden Rule of Gas Optimization: Minimize expensive storage interactions. Most of the following techniques are simply different ways of applying this rule.
Technique 1: Cache Storage Variables in Memory
This is one of the most common and highest-impact optimizations you can make. If you need to read the same state variable multiple times within a single function execution, you should not read it from storage each time. Instead, load it into a local memory variable once at the beginning of the function and use that local variable for all subsequent operations.
This video provides an excellent deep dive into this exact principle, showing the gas savings at each step.
Spend Less Gas - Solidity Gas Optimization [Deep Dive]
The video "Spend Less Gas - Solidity Gas Optimization [Deep Dive]" by Moralis for Developers clearly demonstrates the power of caching. You'll see a function optimized in stages, with the gas costs compared at each step.
Watch the video from the beginning up to 04:40. Pay close attention to the evolution from functionA to functionB and finally to functionC. The narrator does a great job explaining why each change reduces gas by avoiding repeated SSTORE and SLOAD operations.
As you saw, the key is to avoid accessing storage within a loop.
- Bad (Expensive): Reading an array's length and elements directly from storage in every loop iteration.
// Inefficient: `myArray.length` and `myArray[i]` are SLOADs inside a loop for (uint i = 0; i < myArray.length; i++) { sum += myArray[i]; } - Good (Cheaper): Caching the array in a
memoryvariable first.// Efficient: The entire array is copied to memory once. // The loop then operates on the cheap memory copy. uint[] memory localArray = myArray; for (uint i = 0; i < localArray.length; i++) { sum += localArray[i]; }
Technique 2: Efficient Data Storage and Access
How you structure, store, and pass data has a massive impact on gas. Let's explore a few related techniques, which are covered in detail in the following article.
12 Solidity Gas Optimization Techniques
The article "12 Solidity Gas Optimization Techniques" from Alchemy is an excellent, comprehensive guide. We will use it as a reference for several key techniques related to efficient data management.
Read the following sections from the article: "Use mappings instead of arrays" "Store data in calldata instead of memory..." "Use immutable and constant fixed values" "Pack your variables" Focus on the code examples and the explanation of why each technique saves gas.
Let's summarize the key points from that reading.
a) Mappings vs. Arrays for Lookups
For a backend developer, this is analogous to choosing between a hash map and a list.
- Arrays: Finding an item requires iteration (O(n) complexity). This can get extremely expensive as the array grows.
- Mappings: Provide direct, key-based lookups (O(1) complexity). This is always a fixed, low cost.
Guideline: For any key-value data, like tracking user balances (address => uint256), always use a mapping. Use arrays only when you truly need to iterate over the entire collection of items.
b) calldata vs. memory for Function Arguments
When passing data like arrays or strings to an external function, you have a choice.
memory: The data is copied from the transaction payload into memory. This costs gas.calldata: This is a special, read-only location where function arguments live. Usingcalldatatells the EVM to read directly from the transaction payload, avoiding a copy.
Guideline: Always use calldata for parameters of external functions unless you need to modify the data within the function.
c) constant and immutable Variables
If you have a variable whose value will never change, don't waste a storage slot on it.
constant: For values known at compile time (e.g.,uint256 public constant DECIMALS = 18;).immutable: For values set once in the constructor (e.g.,address public immutable owner;).
Both constant and immutable variables have their values baked directly into the contract's bytecode at deployment. When you access them, the EVM reads from the cheap bytecode instead of performing an expensive SLOAD.
d) Variable Packing
The EVM stores data in 32-byte (256-bit) slots. If you declare multiple variables that are smaller than 32 bytes, Solidity can sometimes pack them into a single slot. This can save significant gas on deployment and on subsequent writes.
- Unpacked (Expensive):
uint128 a; // Slot 0 uint256 b; // Slot 1 uint128 c; // Slot 2 (3 slots used) - Packed (Cheaper):
uint128 a; // Slot 0 uint128 c; // Slot 0 (packed with a) uint256 b; // Slot 1 (2 slots used)
Guideline: Group smaller data types (e.g., uint128, bool, address) together in your state variable declarations to encourage packing.
Technique 3: Fine-Tuning with Compiler and Language Features
Beyond data structures, there are language features and compiler settings that provide further optimization.
a) Using unchecked Blocks Safely
Since Solidity version 0.8.0, all arithmetic operations include checks for overflow and underflow. This is a great safety feature, but it adds a small gas cost to every operation. In situations where you are mathematically certain that an overflow/underflow is impossible, you can tell the compiler to skip these checks.
The "Spend Less Gas" video you watched earlier introduces this for a loop counter.
Spend Less Gas - Solidity Gas Optimization [Deep Dive]
Let's revisit the "Spend Less Gas" video to see how unchecked blocks can be used safely to optimize a loop.
Watch the section from 04:40 to 08:24. The video introduces a new functionD that uses an unchecked block for the loop incrementor (i++). Notice the reasoning: it's virtually impossible for a loop counter i to overflow a uint256.
Guideline: The most common and safest use case for unchecked is with loop counters.
// Looping through an array
for (uint i = 0; i < items.length;) {
// process item...
unchecked { ++i; } // Safe and saves gas on every iteration
}
Warning: Be extremely careful with unchecked. Only use it when you have validated inputs or are 100% certain no overflow/underflow can occur. Misusing it can lead to critical vulnerabilities.
b) Enabling the Solidity Compiler Optimizer
This is one of the easiest and most effective ways to save gas. The Solidity compiler has a built-in optimizer that reduces code size and improves efficiency. You should always have it enabled for production deployments.
You can configure this in your hardhat.config.js:
// in hardhat.config.js
module.exports = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200, // The number of times you expect functions to be called
},
},
},
//...
};
The runs parameter tells the optimizer what to prioritize.
- Low
runs(e.g., 200): Optimizes for cheap deployment cost, at the expense of slightly higher function call costs. Good for contracts that are deployed often. - High
runs(e.g., 10000): Optimizes for cheap function call costs, at the expense of a more expensive one-time deployment. Good for contracts like your ERC-20 token, which will be deployed once but whose functions (transfer,approve) will be called many times. For your project, a higherrunsvalue is likely appropriate.
Test your understanding!
You are writing a function to pay out dividends to a list of whitelisted investors. The unoptimized version looks like this:
contract DividendPayout {
address[] public whitelistedInvestors;
mapping(address => uint) public balances;
// Assume whitelistedInvestors and balances are populated
function payDividends(uint totalDividend) public {
uint share = totalDividend / whitelistedInvestors.length;
for (uint i = 0; i < whitelistedInvestors.length; i++) {
address investor = whitelistedInvestors[i];
balances[investor] += share;
}
}
}
Based on what you've learned, what are at least three gas optimizations you could apply to this payDividends function and the contract structure?
Show answer
- Cache
whitelistedInvestorsin memory: Theforloop accesseswhitelistedInvestors.lengthandwhitelistedInvestors[i]from storage on every iteration. This is very expensive. You should copy the array into amemoryvariable before the loop. - Cache
whitelistedInvestors.length: Even better, you can cache the length in a local variable to avoid reading it from memory in each loop iteration. - Use
uncheckedfor the loop counter:iis a perfect candidate for anunchecked { ++i; }block, as it's not at risk of overflowing. - Use
externalvisibility: The function is intended to be called from outside the contract, so changingpublictoexternalwould be slightly more gas-efficient (though the other optimizations have a much larger impact).
A more optimized version would look like this:
function payDividends(uint totalDividend) external { // Use external
address[] memory investors = whitelistedInvestors; // Cache array in memory
uint len = investors.length;
uint share = totalDividend / len;
for (uint i = 0; i < len;) {
balances[investors[i]] += share;
unchecked { ++i; } // Use unchecked for counter
}
}
Conclusion
You've now learned the foundational techniques for writing gas-efficient Solidity code. By applying these principles, you directly reduce the costs for your users, making your entire ecosystem more appealing and sustainable. Remember, you can always use the hardhat-gas-reporter tool from our previous lesson to verify the impact of your optimizations.
Key Takeaways:
- The Cost Hierarchy: Storage is the most expensive resource. Your primary goal is to minimize reads from and writes to it.
- Cache in Memory: Always load storage variables into
memoryif you need to access them multiple times in a function. - Use Efficient Data Patterns: Prefer
mappingsfor lookups,calldatafor external function arguments, andconstant/immutablefor fixed values. - Optimize Your Code: Pack small variables to save storage slots and use
uncheckedarithmetic for safe, low-level optimizations like loop counters. - Enable the Compiler Optimizer: This is a simple and powerful way to get significant gas savings with minimal effort.
Preview of the Next Lesson:
We have now configured our project for the mainnet, learned how to estimate costs, and optimized our code for efficiency. All the preparatory work is complete. In our next lesson, we will take the final, exciting step in this module: Perform the final deployment of your token and associated contracts to the Ethereum mainnet.