Solidity Programming

Solidity Programming

Solidity is a high-level, statically-typed programming language designed specifically for writing smart contracts on the Ethereum blockchain and other compatible blockchains. Smart contracts are self-executing programs that automate, enforce, or execute the terms of an agreement.

Key Features of Solidity High-Level Language:

Similar to languages like JavaScript, Python, and C++, making it easier to learn for developers familiar with these languages. Statically Typed:

Variable types must be defined at the time of declaration, enhancing error detection during compilation. Compiled Language:

Solidity code is compiled into Ethereum Virtual Machine (EVM) bytecode, which is executed on the blockchain. Object-Oriented:

Supports concepts like inheritance, libraries, and user-defined data types. Blockchain-Specific:

Includes features to interact with Ethereum accounts, handle cryptocurrencies (e.g., Ether), and access blockchain data (like transaction details or block timestamps).

Tight Variable Packing

One of the most important gas optimization techniques in Solidity is tight variable packing. The Ethereum Virtual Machine (EVM) stores data in 32-byte slots. When you declare state variables, Solidity tries to pack multiple smaller types into a single slot to minimize storage costs.

How It Works

Types smaller than 32 bytes are packed into the same storage slot if they are declared consecutively. For example:

// Inefficient - uses 4 storage slots
contract GasWaste {
    uint256 a;      // slot 0 (full 32 bytes)
    uint128 b;      // slot 1 (16 bytes, but takes a full slot)
    uint128 c;      // slot 2 (16 bytes, but takes a full slot)
    bool d;         // slot 3 (1 byte, but takes a full slot)
}
// Efficient - uses only 2 storage slots
contract GasEfficient {
    uint128 b;      // ┐
    uint128 c;      // ┘ slot 0 (both uint128s packed)
    uint256 a;      // slot 1 (full 32 bytes)
    bool d;         // slot 2 (but could be packed further)
}

Even better, pack bool with other small types:

// Most efficient - uses 2 storage slots
contract GasEfficient {
    uint128 b;      // ┐
    uint64 c;       // │
    uint32 d;       // │
    bool e;         // ┘ slot 0 (all packed into 32 bytes)
    uint256 a;      // slot 1 (full 32 bytes)
}

Why It Matters

Each storage slot operation has a significant gas cost:

Operation Gas Cost
SLOAD (read) ~2100 gas
SSTORE (write from nonzero to zero) ~20,000 gas (refund on cleanup)
SSTORE (write zero to nonzero) ~20,000 gas
SSTORE (write nonzero to nonzero) ~5,000 gas

If your contract uses 4 slots instead of 2, every function that reads or writes those variables costs roughly double the gas. Over thousands of transactions, this adds up to a massive difference.

Rules of Thumb

  1. Order types by size descending, then group small types together: uint256, then uint128 + uint128, then uint64 + uint32 + uint16 + bool, etc.
  2. Use the smallest integer type that safely holds your value (e.g., uint32 for timestamps, uint8 for small counters).
  3. Use bytes32 over bytes or string for fixed-size data.
  4. For structs, define fields in packed order since structs also occupy storage slots.

Full Contract Example: User Profile System

Here is a complete contract that manages user profiles. Notice how the gas-wasteful version uses separate variables, while the gas-efficient version packs everything into as few slots as possible.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

//  GAS-WASTEFUL — uses 6 storage slots per user
contract UserProfilesGasHeavy {
    mapping(address => uint256) public balance;
    mapping(address => uint256) public lastLogin;
    mapping(address => uint256) public flags;       // only uses lower 8 bits
    mapping(address => uint256) public loginCount;  // only needs uint32
    mapping(address => uint256) public tier;        // only needs uint8
    mapping(address => bool)    public isActive;

    function register(address user, uint256 amount) external {
        balance[user] = amount;
        lastLogin[user] = block.timestamp;
        flags[user] = 0;
        loginCount[user] = 0;
        tier[user] = 1;
        isActive[user] = true;
    }
    // 6 SSTORE calls → 6 separate storage slots per user
}

//  Gas-Efficient — uses 2 storage slots per user
contract UserProfilesGasEfficient {
    struct UserData {
        uint256 balance;    // slot 0 (full 32 bytes)
        uint64  lastLogin;  // ┐
        uint32  loginCount; // │  slot 1 (packed)
        uint8   tier;       // │
        uint8   flags;      // │
        bool    isActive;   // ┘
    }

    mapping(address => UserData) public users;

    function register(address user, uint256 amount) external {
        users[user] = UserData({
            balance:    amount,
            lastLogin:  uint64(block.timestamp),
            loginCount: 0,
            tier:       1,
            flags:      0,
            isActive:   true
        });
    }
    // 2 SSTORE calls → 2 storage slots per user
    // Saves ~100,000 gas per registration
}

Sample Use Case: NFT Marketplace Listing

Consider an NFT marketplace where each listing stores the seller, token ID, price, listing time, and whether the item is a buy-now or auction.

//  Packed Listing Struct — fits in 2 storage slots
contract NFTPackedMarketplace {
    struct Listing {
        address seller;         // 20 bytes ┐
        uint64  listTime;       //  8 bytes  │  slot 0 (32 bytes)
        uint32  tokenId;        //  4 bytes  ┘
        uint128 price;          // 16 bytes ┐  slot 1 (mapping hash)
        bool    isBuyNow;       //  1 byte  ┘  (packed in mapping slot remainder)
    }

    mapping(uint256 => Listing) public listings;
    uint256 public listingCounter;

    function createListing(
        uint32 tokenId,
        uint128 price,
        bool isBuyNow
    ) external returns (uint256 id) {
        id = ++listingCounter;
        listings[id] = Listing({
            seller:   msg.sender,
            listTime: uint64(block.timestamp),
            tokenId:  tokenId,
            price:    price,
            isBuyNow: isBuyNow
        });
    }

    // To buy, the buyer sends ETH and the listing is marked sold.
    // Reading the packed struct costs 2 SLOADs instead of 5+.
    function buy(uint256 id) external payable {
        Listing storage l = listings[id];
        require(l.isBuyNow, "auction only");
        require(msg.sender != l.seller, "no self-buy");
        require(msg.value >= l.price, "insufficient");

        delete listings[id]; // gas refund on cleanup
        payable(l.seller).transfer(l.price);
    }
}

Without packing, each Listing would occupy 5 separate storage slots: address (20 bytes → 1 slot), uint256 price (1 slot), uint256 listTime (1 slot), uint256 tokenId (1 slot), bool (1 slot) = 5 slots.

With tight packing, the same data fits into 2 slots, reducing the gas cost of createListing from ~100,000 to ~40,000 and cutting buy reads by 60%.

Tight packing is one of the easiest optimizations to implement and one of the most impactful for reducing deployment and execution costs on Ethereum.

Comments

Leave a comment

What is 2 + 3?
Please solve the arithmetic question above to verify you're human.