Gas Optimization Techniques for Solidity Developers
A practical guide for Ethereum developers on how to write more gas-efficient smart contracts. Learn techniques to reduce the execution cost of your Solidity code.
On the Ethereum blockchain, every computational step costs money. This cost, paid in "gas," is a critical constraint for smart contract developers. Writing code that is not just secure and functional, but also gas-efficient, is the hallmark of a skilled Solidity developer. High gas costs can make a dApp unusable, while optimized contracts can save users significant amounts of money and provide a competitive advantage.
This guide provides a list of practical, high-impact gas optimization techniques that every Ethereum developer should know.
1. Minimize State Changes
The most expensive operations in the EVM are those that modify the blockchain's state. Reading data is cheap, but writing or changing data is expensive.
- SSTORE: The
SSTORE
opcode, which is used to write to storage, is the most expensive operation. A singleSSTORE
can cost 20,000 gas or more. - Technique: Structure your code to minimize the number of writes to storage. If you need to perform multiple calculations on a state variable, load it into a local memory variable first, perform the operations, and then write the final result back to storage only once.
Example:
// Inefficient: 3 SSTORE operations
function calculateBad() public {
myStateVar += 1; // SSTORE 1
myStateVar *= 2; // SSTORE 2
myStateVar -= 5; // SSTORE 3
}
// Efficient: 1 SSTORE operation
function calculateGood() public {
uint256 local_myStateVar = myStateVar; // SLOAD (cheap)
local_myStateVar += 1;
local_myStateVar *= 2;
local_myStateVar -= 5;
myStateVar = local_myStateVar; // SSTORE (once)
}
2. Use the Right Data Types
Solidity's data types can have a significant impact on gas costs due to how the EVM packs data into 256-bit (32-byte) storage slots.
- The Rule: If you have multiple
uint
variables inside astruct
or as contiguous state variables, try to use smaller types (uint128
,uint64
, etc.) if you know the values won't exceed their limits. The EVM can often "pack" these smaller variables into a single 32-byte storage slot, saving gas.
Example:
// Inefficient: Uses two 32-byte slots
struct BadStruct {
uint256 a; // Slot 1
uint256 b; // Slot 2
}
// Efficient: Uses one 32-byte slot
struct GoodStruct {
uint128 a; // Slot 1 (first 128 bits)
uint128 b; // Slot 1 (last 128 bits)
}
Caution: This only works for storage variables. For local variables in memory
or calldata
, it's almost always cheaper to use the full uint256
, as the EVM is optimized to handle 32-byte words.
3. Use calldata
for External Function Parameters
When defining an external
function that takes dynamic data types (like string
or bytes
) as arguments, prefer calldata
over memory
.
- The Difference:
calldata
is a non-modifiable, non-persistent area where function arguments are stored.memory
is a modifiable area. - The Optimization: Since
calldata
doesn't need to be copied into memory, it skips a memory allocation and copying step, saving gas.
// Inefficient
function doSomething(string memory _myString) external { ... }
// Efficient
function doSomething(string calldata _myString) external { ... }
4. Use Custom Errors Instead of require
Strings
Introduced in Solidity 0.8.4, custom errors are a more gas-efficient way to handle failed require
statements.
- The Problem:
require(condition, "Error string")
stores the error string on the blockchain, which costs gas. - The Solution: Define a custom error and use it in your
require
. This doesn't store any string data, saving significant gas on deployment and on revert.
Example:
// Inefficient
require(msg.sender == owner, "Caller is not the owner");
// Efficient
error NotOwner();
...
if (msg.sender != owner) {
revert NotOwner();
}
5. Use unchecked
for Safe Arithmetic (Solidity 0.8.0+)
Since Solidity 0.8.0, arithmetic operations automatically include checks for overflow and underflow, which adds a small gas cost. If you are certain that an operation cannot overflow or underflow, you can wrap it in an unchecked
block to save gas.
// Example: A for loop where `i` will never overflow
for (uint256 i = 0; i < length; i++) {
unchecked {
// ... operations with i
}
}
Warning: Only use this if you are absolutely sure the arithmetic is safe. An unexpected overflow can be a major security vulnerability.
Gas optimization is a deep and complex topic, but applying these fundamental techniques can lead to significant savings. It requires a developer to think not just about what the code does, but how the EVM actually