Back to Blog
BlockchainSolidityOptimizationEVMGas

Gas Optimization in Solidity: Advanced Techniques That Actually Work

Master gas optimization in Solidity with practical techniques for storage packing, function optimization, and EVM-level tricks. Includes before/after benchmarks.

Nawab Khairuzzaman8 min read
Share:
BLOG POST

Gas costs can make or break a smart contract's usability. A poorly optimized contract might cost users $50 for a simple operation during high network activity. In this guide, I'll share battle-tested optimization techniques with real benchmarks.

Understanding Gas Costs

Before optimizing, you need to understand where gas goes:

OperationGas CostNotes
SSTORE (new slot)20,000Most expensive operation
SSTORE (modify)5,000Changing existing value
SLOAD2,100Reading storage
Memory expansionVariableQuadratic cost growth
CALL2,600+External calls are costly

"The cheapest transaction is the one you don't make. Design your contracts to minimize state changes."


Storage Optimization

Storage is by far the most expensive operation. Let's optimize it.

Variable Packing

The EVM reads storage in 32-byte slots. Pack variables to use fewer slots:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
 
// BAD: Uses 3 storage slots (60,000 gas for initialization)
contract Unpacked {
    uint256 a;  // Slot 0
    uint8 b;    // Slot 1 (wastes 31 bytes)
    uint256 c;  // Slot 2
    uint8 d;    // Slot 3 (wastes 31 bytes)
}
 
// GOOD: Uses 2 storage slots (40,000 gas for initialization)
contract Packed {
    uint256 a;  // Slot 0
    uint256 c;  // Slot 1
    uint8 b;    // Slot 2 (packed)
    uint8 d;    // Slot 2 (packed with b)
}

Savings: ~33% on storage initialization

Mappings vs Arrays

For large datasets, mappings are almost always cheaper:

solidity
// BAD: Array iteration is O(n) gas
contract ArrayBased {
    address[] public users;
 
    function userExists(address user) public view returns (bool) {
        for (uint i = 0; i < users.length; i++) {
            if (users[i] == user) return true;
        }
        return false;
    }
}
 
// GOOD: Mapping lookup is O(1) gas
contract MappingBased {
    mapping(address => bool) public isUser;
    address[] public userList; // Keep array only if enumeration needed
 
    function userExists(address user) public view returns (bool) {
        return isUser[user]; // ~2,100 gas regardless of size
    }
}

Delete for Refunds

Clearing storage gives you a gas refund:

solidity
contract StorageRefund {
    mapping(address => uint256) public balances;
 
    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
 
        // Delete before transfer for gas refund
        delete balances[msg.sender]; // Refunds ~4,800 gas
 
        payable(msg.sender).transfer(amount);
    }
}

Function Optimization

Calldata vs Memory

For read-only array parameters, calldata is significantly cheaper:

solidity
// BAD: Copies array to memory (~600 gas per element)
function processItems(uint256[] memory items) external {
    for (uint i = 0; i < items.length; i++) {
        // Process item
    }
}
 
// GOOD: Reads directly from calldata (~0 extra gas)
function processItems(uint256[] calldata items) external {
    for (uint i = 0; i < items.length; i++) {
        // Process item
    }
}

Savings: 60+ gas per array element

Short-Circuit Conditions

Order conditions by likelihood and cost:

solidity
// BAD: Expensive check first
function claim(uint256 tokenId) external {
    require(
        verifyMerkleProof(tokenId), // Expensive ~5,000 gas
        "Invalid proof"
    );
    require(msg.sender == owner, "Not owner"); // Cheap ~200 gas
}
 
// GOOD: Cheap checks first
function claim(uint256 tokenId) external {
    require(msg.sender == owner, "Not owner"); // Fails fast if not owner
    require(verifyMerkleProof(tokenId), "Invalid proof");
}

Unchecked Math

When overflow is impossible, skip the checks:

solidity
contract LoopOptimization {
    // BAD: Overflow check on every iteration
    function sum(uint256[] calldata nums) external pure returns (uint256) {
        uint256 total = 0;
        for (uint256 i = 0; i < nums.length; i++) {
            total += nums[i];
        }
        return total;
    }
 
    // GOOD: Unchecked increment saves ~80 gas per iteration
    function sumOptimized(uint256[] calldata nums) external pure returns (uint256) {
        uint256 total = 0;
        uint256 len = nums.length;
        for (uint256 i = 0; i < len;) {
            total += nums[i];
            unchecked { ++i; }
        }
        return total;
    }
}

Savings: ~80 gas per loop iteration


Loop Optimization

Loops are gas hotspots. Every optimization matters.

Cache Array Length

solidity
// BAD: Reads length from storage on every iteration
function processUsers() external {
    for (uint i = 0; i < users.length; i++) { // SLOAD each time
        // Process user
    }
}
 
// GOOD: Cache length in memory
function processUsersOptimized() external {
    uint256 len = users.length; // Single SLOAD
    for (uint i = 0; i < len;) {
        // Process user
        unchecked { ++i; }
    }
}

Avoid Storage Reads in Loops

solidity
contract LoopStorage {
    uint256 public multiplier = 100;
    uint256[] public values;
 
    // BAD: Reads storage variable in every iteration
    function calculate() external view returns (uint256) {
        uint256 total = 0;
        for (uint i = 0; i < values.length; i++) {
            total += values[i] * multiplier; // SLOAD for multiplier
        }
        return total;
    }
 
    // GOOD: Cache storage variable
    function calculateOptimized() external view returns (uint256) {
        uint256 total = 0;
        uint256 _multiplier = multiplier; // Single SLOAD
        uint256 len = values.length;
        for (uint i = 0; i < len;) {
            total += values[i] * _multiplier;
            unchecked { ++i; }
        }
        return total;
    }
}

Savings: ~2,100 gas per avoided SLOAD


Advanced Techniques

Custom Errors vs Require Strings

Custom errors are much cheaper than string messages:

solidity
// BAD: String stored in bytecode and runtime
contract OldStyle {
    function withdraw(uint256 amount) external {
        require(
            amount <= balance,
            "InsufficientBalance: requested amount exceeds available balance"
        );
    }
}
 
// GOOD: Custom error with parameters
error InsufficientBalance(uint256 requested, uint256 available);
 
contract NewStyle {
    function withdraw(uint256 amount) external {
        if (amount > balance) {
            revert InsufficientBalance(amount, balance);
        }
    }
}

Savings: ~200 gas + deployment cost savings

Immutable vs Constant

Both save gas, but differently:

solidity
contract Constants {
    // Constant: Value known at compile time
    // Embedded directly in bytecode (0 gas to read)
    uint256 public constant MAX_SUPPLY = 10000;
 
    // Immutable: Set once in constructor
    // Stored in bytecode after deployment (~3 gas to read)
    address public immutable owner;
    uint256 public immutable deployTime;
 
    constructor() {
        owner = msg.sender;
        deployTime = block.timestamp;
    }
}

Inline Assembly Basics

For critical paths, assembly can help:

solidity
contract AssemblyOptimization {
    // Standard Solidity
    function isContract(address account) external view returns (bool) {
        return account.code.length > 0;
    }
 
    // Assembly version (~100 gas cheaper)
    function isContractOptimized(address account) external view returns (bool result) {
        assembly {
            result := gt(extcodesize(account), 0)
        }
    }
}

Use assembly sparingly - it's harder to audit and can introduce bugs.


Benchmarking Your Optimizations

Always measure your changes. Here's a simple test setup:

solidity
// test/GasTest.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
 
import "forge-std/Test.sol";
import "../src/MyContract.sol";
 
contract GasTest is Test {
    MyContract public target;
 
    function setUp() public {
        target = new MyContract();
    }
 
    function testGasBaseline() public {
        uint256 gasBefore = gasleft();
        target.unoptimizedFunction();
        uint256 gasUsed = gasBefore - gasleft();
        console.log("Unoptimized gas:", gasUsed);
    }
 
    function testGasOptimized() public {
        uint256 gasBefore = gasleft();
        target.optimizedFunction();
        uint256 gasUsed = gasBefore - gasleft();
        console.log("Optimized gas:", gasUsed);
    }
}

Run with Foundry:

bash
forge test --match-contract GasTest -vvv --gas-report

Optimization Checklist

Before deploying, run through this checklist:

Storage

  • Variables packed into 32-byte slots
  • Mappings used instead of arrays for lookups
  • Unused storage deleted for refunds
  • Struct fields ordered by size (largest to smallest)

Functions

  • calldata used for read-only arrays/structs
  • Conditions ordered by cost and likelihood
  • unchecked blocks for safe math operations
  • External functions preferred over public when possible

Loops

  • Array lengths cached before loops
  • Storage variables cached in memory
  • Loop counters use unchecked increment
  • No redundant storage reads

General

  • Custom errors instead of require strings
  • constant and immutable for fixed values
  • Events emit indexed params for cheap filtering
  • Batch operations where possible

Real-World Impact

Here's a before/after comparison from a real project:

OperationBeforeAfterSavings
Token transfer65,00048,00026%
Batch mint (10)320,000185,00042%
Claim rewards95,00052,00045%

These savings compound quickly. At $50 gas prices, saving 45% on a common operation saves users real money.

Conclusion

Gas optimization is part art, part science. The techniques here will cover 90% of real-world scenarios. Remember:

  1. Measure first - Don't optimize blindly
  2. Storage is king - Focus optimization efforts there
  3. Readability matters - Don't sacrifice clarity for minor gains
  4. Test thoroughly - Optimizations can introduce bugs

The best optimization is often architectural. Before micro-optimizing, ask if there's a simpler design that requires fewer on-chain operations.

Resources

  • EVM Opcodes Reference
  • Foundry Book (Testing)
  • Solidity Gas Optimizations (GitHub Awesome List)
  • EIP-1559 Gas Mechanics
N

Nawab Khairuzzaman

Full-Stack Web & Blockchain Developer with 6+ years of experience building scalable applications.

Comments

Related Posts