DEV Community

Cover image for Understanding Gas Optimization in Ethereum Smart Contracts
Dr milli
Dr milli

Posted on

Understanding Gas Optimization in Ethereum Smart Contracts

Understanding Gas Optimization in Ethereum Smart Contracts

Gas Optimization Banner

Introduction

Gas is the lifeblood of Ethereum. Every operation you perform on the blockchain costs gas, and users must pay for it. If your smart contracts aren't optimized, you're essentially wasting your users' money and making your dApp less competitive.

In this comprehensive guide, I'll walk you through the most important gas optimization techniques that can reduce your contract's execution costs by up to 70%.


What is Gas? πŸ›’οΈ

Gas is a unit that measures the computational effort required to execute operations on Ethereum. Each operation (storage write, computation, function call) consumes a specific amount of gas.

Why does this matter?

  • Users pay for gas with ETH
  • Higher gas costs = less adoption
  • Optimized contracts = better user experience
  • Savings compound at scale

1. Use Efficient Data Types

Problem: Storing Data Inefficiently

// ❌ BAD: Uses more storage slots
pragma solidity ^0.8.0;

contract Inefficient {
    uint256 userCount;      // 32 bytes (1 slot)
    uint256 maxUsers;       // 32 bytes (1 slot)
    bool isActive;          // 1 byte (1 slot) - WASTED SPACE!
    uint256 balance;        // 32 bytes (1 slot)
}
Enter fullscreen mode Exit fullscreen mode

Solution: Pack Your Variables

// βœ… GOOD: Packs variables into single storage slots
pragma solidity ^0.8.0;

contract Optimized {
    uint128 userCount;      // 16 bytes
    uint128 maxUsers;       // 16 bytes
    bool isActive;          // 1 byte   } All fit in ONE
    address owner;          // 20 bytes } 32-byte slot!
}
Enter fullscreen mode Exit fullscreen mode

Storage Cost Reduction: 4 slots β†’ 1 slot = 75% savings

Key Insight:

Solidity packs variables into 32-byte slots from right to left. Order variables by size (largest first) to minimize wasted space.

// βœ… BEST: Optimal ordering
pragma solidity ^0.8.0;

contract MostOptimized {
    address owner;           // 20 bytes
    uint96 balance;          // 12 bytes
    uint32 lastUpdate;       // 4 bytes   } All in ONE slot
    bool isActive;           // 1 byte    }

    uint256 largeNumber;     // 32 bytes (own slot)
}
Enter fullscreen mode Exit fullscreen mode

2. Minimize Storage Writes ✍️

Storage operations are the most expensive operations in Solidity. Reading costs 2,100 gas, but writing costs 20,000 gas initially.

Problem: Multiple Storage Writes

// ❌ BAD: Multiple storage writes in loop
pragma solidity ^0.8.0;

contract BadLoop {
    uint256 public totalSupply;

    function batchMint(uint256 count) external {
        for (uint256 i = 0; i < count; i++) {
            totalSupply++; // 20,000 gas per write!
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Cost: 100 mints = 2,000,000 gas 😱

Solution: Use Memory Variables

// βœ… GOOD: Use memory, write once
pragma solidity ^0.8.0;

contract GoodLoop {
    uint256 public totalSupply;

    function batchMint(uint256 count) external {
        uint256 _totalSupply = totalSupply; // Read once
        _totalSupply += count;              // Cheap memory operation
        totalSupply = _totalSupply;         // Write once
    }
}
Enter fullscreen mode Exit fullscreen mode

Cost: 100 mints = ~44,000 gas πŸš€

Gas Saved: ~1,956,000 gas (97.8% reduction!)


3. Use Events Instead of Storage

Events are 10-50x cheaper than storing data.

Problem: Storing Historical Data

// ❌ BAD: Storing every transfer in array
pragma solidity ^0.8.0;

contract BadHistory {
    struct Transfer {
        address from;
        address to;
        uint256 amount;
        uint256 timestamp;
    }

    Transfer[] public transfers; // Storage costs 20k+ per write

    function transfer(address to, uint256 amount) external {
        transfers.push(Transfer(msg.sender, to, amount, block.timestamp));
        // Very expensive!
    }
}
Enter fullscreen mode Exit fullscreen mode

Solution: Emit Events

// βœ… GOOD: Use events for logging
pragma solidity ^0.8.0;

contract GoodHistory {
    event Transfer(
        indexed address from,
        indexed address to,
        uint256 amount,
        uint256 timestamp
    );

    function transfer(address to, uint256 amount) external {
        emit Transfer(msg.sender, to, amount, block.timestamp);
        // ~375 gas vs 20,000+ for storage
    }
}
Enter fullscreen mode Exit fullscreen mode

Gas Cost:

  • Event: ~375 gas
  • Storage: 20,000+ gas
  • Savings: 98% cheaper ✨

4. Optimize Function Visibility

Problem: Unnecessary External Calls

// ❌ BAD: Calling internal functions externally
pragma solidity ^0.8.0;

contract BadVisibility {
    uint256 public data;

    function updateData(uint256 newValue) public {
        _processData(newValue);
    }

    function _processData(uint256 value) public { // Should be internal!
        data = value;
    }
}

// Calling it externally costs extra
// contract.updateData(100);  // Expensive path
// contract._processData(100); // Even worse - creates new context
Enter fullscreen mode Exit fullscreen mode

Solution: Use Correct Visibility

// βœ… GOOD: Correct visibility modifiers
pragma solidity ^0.8.0;

contract GoodVisibility {
    uint256 public data;

    function updateData(uint256 newValue) external {
        _processData(newValue); // Cheap internal call
    }

    function _processData(uint256 value) internal { // Much better!
        data = value;
    }
}
Enter fullscreen mode Exit fullscreen mode

Tip: Use external instead of public when not calling internally.


5. Use Mapping Instead of Arrays for Lookups

Problem: Array Iteration

// ❌ BAD: O(n) lookup time, expensive
pragma solidity ^0.8.0;

contract BadLookup {
    address[] public users;

    function isUserRegistered(address user) external view returns (bool) {
        for (uint256 i = 0; i < users.length; i++) {
            if (users[i] == user) return true;
        }
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Solution: Use Mapping

// βœ… GOOD: O(1) lookup, instant
pragma solidity ^0.8.0;

contract GoodLookup {
    mapping(address => bool) public isRegistered;

    function registerUser(address user) external {
        isRegistered[user] = true;
    }

    function checkUser(address user) external view returns (bool) {
        return isRegistered[user]; // O(1) instant lookup
    }
}
Enter fullscreen mode Exit fullscreen mode

Performance:

  • Array: 100 users = 100 storage reads
  • Mapping: 100 users = 1 storage read
  • Savings: 100x faster ⚑

6. Avoid Expensive Operations

Problem: Unnecessary Computations

// ❌ BAD: Expensive operations
pragma solidity ^0.8.0;

contract ExpensiveOps {
    function inefficientMath(uint256 a, uint256 b) external pure returns (uint256) {
        // Expensive operations
        uint256 result = a ** 2 + b ** 2; // Exponentiation is expensive
        for (uint256 i = 0; i < 10; i++) {
            result = result * 2 / 3; // Multiple expensive ops
        }
        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

Solution: Optimize Calculations

// βœ… GOOD: Optimized math
pragma solidity ^0.8.0;

contract EfficientOps {
    function efficientMath(uint256 a, uint256 b) external pure returns (uint256) {
        // Pre-calculate or use bit shifts
        uint256 result = (a * a) + (b * b); // Multiplication cheaper than exponentiation
        result = (result * 1024) / 1536;    // Bit operations are cheaper
        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

Cost Reduction Tactics:

  • βœ… Use multiplication instead of exponentiation
  • βœ… Use bit shifts instead of division by powers of 2
  • βœ… Cache computed values
  • βœ… Avoid unnecessary loops

7. Use Immutable and Constant Variables

Problem: Reading State Variables Multiple Times

// ❌ BAD: Reading storage multiple times
pragma solidity ^0.8.0;

contract BadConstants {
    address public owner = msg.sender;
    uint256 public maxSupply = 1000000;

    function checkOwner() external view returns (bool) {
        if (msg.sender == owner) { // Storage read 1: 2,100 gas
            if (msg.sender == owner) { // Storage read 2: 2,100 gas
                return true;
            }
        }
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Solution: Use Immutable and Constant

// βœ… GOOD: Use immutable for write-once values
pragma solidity ^0.8.0;

contract GoodConstants {
    address immutable owner;
    uint256 constant MAX_SUPPLY = 1000000;

    constructor() {
        owner = msg.sender;
    }

    function checkOwner() external view returns (bool) {
        if (msg.sender == owner) { // No storage read! Embedded in bytecode
            return true;
        }
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Difference:

  • constant: 21 gas (embedded in bytecode)
  • immutable: 21 gas (after constructor)
  • state variable: 2,100 gas

Savings: 100x cheaper 🎯


8. Batch Operations

Problem: Multiple Transactions

// ❌ BAD: Multiple function calls
// User calls transfer 10 times = 10 separate transactions
// Each transaction pays for initialization overhead
Enter fullscreen mode Exit fullscreen mode

Solution: Batch in Single Function

// βœ… GOOD: Batch operation
pragma solidity ^0.8.0;

interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
}

contract BatchTransfer {
    IERC20 token;

    function batchTransfer(
        address[] calldata recipients,
        uint256[] calldata amounts
    ) external {
        require(recipients.length == amounts.length, "Mismatch");

        for (uint256 i = 0; i < recipients.length; i++) {
            token.transfer(recipients[i], amounts[i]);
        }
        // One transaction, one initialization cost
    }
}
Enter fullscreen mode Exit fullscreen mode

Savings: Reduces transaction overhead by 90% for batch operations


9. Use Calldata for Large Data

Problem: Memory Allocation Overhead

// ❌ BAD: Unnecessary memory copies
pragma solidity ^0.8.0;

contract BadMemory {
    function processArray(uint256[] memory data) external pure returns (uint256) {
        // 'memory' keyword copies calldata to memory (expensive!)
        uint256 sum = 0;
        for (uint256 i = 0; i < data.length; i++) {
            sum += data[i];
        }
        return sum;
    }
}
Enter fullscreen mode Exit fullscreen mode

Solution: Use Calldata When Not Modifying

// βœ… GOOD: Use calldata for read-only data
pragma solidity ^0.8.0;

contract GoodMemory {
    function processArray(uint256[] calldata data) external pure returns (uint256) {
        // 'calldata' directly reads from transaction data (cheap!)
        uint256 sum = 0;
        for (uint256 i = 0; i < data.length; i++) {
            sum += data[i];
        }
        return sum;
    }
}
Enter fullscreen mode Exit fullscreen mode

Memory cost: Quadratic (16 gas + 3 gas per word)
Calldata cost: Linear


10. Check Arguments Early (Fail Fast)

Problem: Wasting Gas Before Reverting

// ❌ BAD: Expensive operations before validation
pragma solidity ^0.8.0;

contract BadValidation {
    function transfer(address to, uint256 amount) external {
        // Expensive operation first
        uint256 result = amount * 1000;

        // Validation after (gas wasted if this fails!)
        require(to != address(0), "Invalid address");
        require(amount > 0, "Amount must be > 0");
    }
}
Enter fullscreen mode Exit fullscreen mode

Solution: Validate First

// βœ… GOOD: Validate before expensive operations
pragma solidity ^0.8.0;

contract GoodValidation {
    function transfer(address to, uint256 amount) external {
        // Validation first (cheap operations)
        require(to != address(0), "Invalid address");
        require(amount > 0, "Amount must be > 0");

        // Expensive operation after (only if valid)
        uint256 result = amount * 1000;
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefit: Users don't pay for failed operations


Gas Optimization Checklist βœ…

Use this checklist when optimizing your contracts:

  • [ ] Pack storage variables - Order by size, use smaller types
  • [ ] Minimize storage writes - Use memory for temporary values
  • [ ] Use events - Instead of storing historical data
  • [ ] Correct visibility - external over public for non-internal calls
  • [ ] Use mappings - Instead of arrays for lookups
  • [ ] Avoid expensive operations - No exponentiation, unnecessary loops
  • [ ] Use constants/immutable - For fixed values (100x cheaper)
  • [ ] Batch operations - Combine multiple actions
  • [ ] Use calldata - For read-only arrays (not memory)
  • [ ] Validate early - Check arguments before expensive operations
  • [ ] Cache values - Store frequently accessed data in memory

Real-World Example: Token Contract Optimization

Before Optimization

// ❌ UNOPTIMIZED
pragma solidity ^0.8.0;

contract MyToken {
    string public name;
    string public symbol;
    uint256 public totalSupply;
    uint256 public decimals;

    mapping(address => uint256) public balances;
    mapping(address => mapping(address => uint256)) public allowances;

    address[] public holders; // Array of all holders

    function transfer(address to, uint256 amount) public {
        require(amount > 0);
        require(balances[msg.sender] >= amount);

        balances[msg.sender] = balances[msg.sender] - amount;
        balances[to] = balances[to] + amount;

        // Store transfer history
        transfers.push(Transfer(msg.sender, to, amount));
    }
}
Enter fullscreen mode Exit fullscreen mode

After Optimization

// βœ… OPTIMIZED
pragma solidity ^0.8.0;

contract MyTokenOptimized {
    string constant name = "My Token";
    string constant symbol = "MTK";
    uint8 constant decimals = 18;
    uint256 public totalSupply;

    mapping(address => uint256) public balances;
    mapping(address => mapping(address => uint256)) public allowances;

    event Transfer(indexed address from, indexed address to, uint256 amount);

    function transfer(address to, uint256 amount) external {
        require(amount > 0, "Amount must be > 0");
        require(to != address(0), "Invalid address");
        require(balances[msg.sender] >= amount, "Insufficient balance");

        uint256 senderBalance = balances[msg.sender]; // Cache in memory
        senderBalance -= amount;
        balances[msg.sender] = senderBalance; // Single write
        balances[to] += amount;

        emit Transfer(msg.sender, to, amount); // Event instead of storage
    }
}
Enter fullscreen mode Exit fullscreen mode

Gas Savings: ~60-70% per transaction πŸš€


Tools for Gas Analysis

1. Hardhat Gas Reporter

npm install --save-dev hardhat-gas-reporter
Enter fullscreen mode Exit fullscreen mode

2. Solidity Optimizer

Enable in hardhat.config.js:

solidity: {
    version: "0.8.0",
    settings: {
        optimizer: {
            enabled: true,
            runs: 200
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Etherscan Gas Tracker

Check real costs on etherscan.io/gastracker


Key Takeaways πŸŽ“

  1. Storage is expensive - Use memory for temporary values
  2. Pack your variables - Fit multiple values into single storage slots
  3. Use events - For logging and history tracking
  4. Optimize data structures - Mappings over arrays for lookups
  5. Cache values - Read from storage once, use in memory
  6. Constants/Immutable - Use for fixed values (100x cheaper)
  7. Batch operations - Reduce transaction overhead
  8. Validate early - Check arguments before expensive operations

Conclusion

Gas optimization isn't just about saving moneyβ€”it's about building better user experiences and making your DApps competitive. Every gas unit you save multiplies across thousands of users.

Start with the low-hanging fruit (storage packing, caching, events), then profile your contracts to find the biggest savings opportunities.

Your users will thank you. πŸ™


Resources


What's Your Experience?

Have you optimized gas in your smart contracts? Share your biggest gas-saving wins in the comments below! What techniques have worked best for you? πŸ‘‡

Let's build efficient Web3 together! πŸš€


Connect with me:

Top comments (0)