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.

Gas Optimization Techniques for Solidity Developers

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 single SSTORE 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 a struct 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

Looking for a Web3 Job?

Get the best Web3, crypto, and blockchain jobs delivered directly to you. Join our Telegram channel with over 56,000 subscribers.