iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Variables

Solidity has different variable kinds with strict semantics: storage (persistent, costly), memory (function-local, cheap), and calldata (read-only inputs, cheapest). Visibility and immutability are key gas levers.

Storage, memory, calldata, visibility

EXAMPLE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract VariablesShowcase {
    // === 1. State variables (live in storage — persistent) ===

    uint256 public count;                          // public auto-getter
    address private owner;                          // private — internal use only
    string  internal name = "Showcase";              // internal — this contract + children

    // 'constant' — compiled into bytecode, NEVER changes
    uint256 constant MAX_SUPPLY = 1_000_000;
    address constant ZERO_ADDRESS = address(0);

    // 'immutable' — set once in constructor, then read-only (cheaper than storage)
    address public immutable creator;
    uint256 public immutable deployedAt;

    constructor() {
        creator    = msg.sender;
        deployedAt = block.timestamp;
    }

    // === 2. Visibility ===

    // public    : callable from anywhere, generates a getter for state vars
    // external  : callable only from OUTSIDE the contract (slightly cheaper than public for fns)
    // internal  : callable from this contract and inherited contracts
    // private   : only this contract (still visible on-chain — don't store secrets here)

    function readCount() external view returns (uint256) {
        return count;
    }

    function _internalHelper() internal pure returns (uint256) {
        return 42;
    }

    // === 3. Data locations: storage / memory / calldata ===

    struct User {
        string  name;
        uint256 score;
    }

    mapping(address => User) public users;

    function updateName(address userAddr, string calldata newName) external {
        // 'storage' reference — modifying it changes contract state
        User storage u = users[userAddr];
        u.name = newName;       // writes to storage; expensive (~5,000-20,000 gas)
    }

    function copyToMemory(address userAddr) external view returns (string memory) {
        // 'memory' copy — function-local, cheap, discarded at return
        User memory u = users[userAddr];
        return u.name;
    }

    function readCalldata(uint256[] calldata nums) external pure returns (uint256) {
        // 'calldata' — function arguments; read-only; cheapest of the three
        uint256 sum = 0;
        for (uint256 i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        return sum;
    }

    // === 4. Gas comparison ===

    function expensiveLoop(uint256 n) external {
        // BAD: storage read in a loop costs ~2100 gas per iteration
        for (uint256 i = 0; i < n; i++) {
            count += 1;
        }
    }

    function cheapLoop(uint256 n) external {
        // GOOD: cache to memory; one storage write at the end
        uint256 local = count;
        for (uint256 i = 0; i < n; i++) {
            local += 1;
        }
        count = local;
    }

    // === 5. Primitive types ===

    function primitives() external pure returns (
        bool b,
        int256 i,
        uint256 u,
        bytes32 b32,
        address a
    ) {
        b   = true;
        i   = -42;
        u   = 42;
        b32 = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef;
        a   = address(0xCAfe);
    }

    // uint8..uint256 (in steps of 8) — explicit size matters for packing
    // int8..int256
    // address payable  — can receive ETH via .transfer / .send
    // bytes1..bytes32 — fixed-size byte arrays

    // === 6. Variable-size types ===

    string  publicMessage;                          // dynamic string
    bytes   data;                                   // dynamic bytes
    uint256[] nums;                                 // dynamic array
    uint256[5] fixedNums;                           // fixed-size array
    mapping(address => uint256) balances;           // mapping
    mapping(address => mapping(uint256 => bool)) approved;   // nested mapping

    // Variable-size types in memory are pricier — copying is O(N).

    // === 7. Default values ===

    // No explicit init = the zero value.
    //   bool    → false
    //   uint*   → 0
    //   int*    → 0
    //   address → 0x0000...0000
    //   bytes*  → 0x00...00
    //   string  → ""
    //   array   → empty array
    //   mapping → empty (every key returns the zero value)

    function isUninitialised(address a) external view returns (bool) {
        return balances[a] == 0;     // works because uninitialised mapping value = 0
    }

    // === 8. Storage layout (packing) ===

    // Slots are 32 bytes. Variables PACKED together if they fit.
    struct Packed {
        uint128 a;     // 16 bytes
        uint128 b;     // 16 bytes — packed with a in 1 slot
        uint256 c;     // 32 bytes — new slot
    }

    struct Unpacked {
        uint128 a;     // 16 bytes
        uint256 c;     // 32 bytes — new slot (doesn't fit with a)
        uint128 b;     // 16 bytes — new slot, wastes 16 bytes
    }
    // Order matters! Group small types together.

    // === 9. Storage SLOT cost

    // Setting from 0 → non-zero:        SSTORE costs ~22,100 gas
    // Changing non-zero → other:        SSTORE costs ~5,000 gas
    // Setting non-zero → 0 (refund):    refunds ~15,000 gas (net write cheap)
    // Reading any storage slot:         SLOAD costs ~2,100 gas (cold) or 100 (warm)

    // === 10. Read-only modifiers ===

    // 'pure'  — no read / write of state, no env access
    function pureMath(uint256 a, uint256 b) external pure returns (uint256) {
        return a + b;
    }

    // 'view'  — read state, no writes
    function readCount2() external view returns (uint256) {
        return count;
    }

    // Functions without view/pure can MODIFY state and are payable-eligible.

    // === 11. Local variables

    function localDemo() external pure returns (uint256) {
        uint256 x = 10;                  // memory; lives on stack
        uint256 y = 20;
        return x + y;
    }

    // === 12. Block / msg / tx globals (special vars)

    function context() external view returns (address, uint256, address) {
        return (msg.sender, block.number, tx.origin);
        // msg.sender   — direct caller (could be a contract)
        // tx.origin    — original EOA — RARELY use; phishing risk
        // block.number, block.timestamp, block.chainid, block.basefee
        // msg.value    — wei sent with the call
    }

    // === 13. Events for off-chain reads

    event CountUpdated(uint256 newCount, address by);

    function setCount(uint256 newCount) external {
        count = newCount;
        emit CountUpdated(newCount, msg.sender);
    }

    // === 14. Common gas tips for variables ===
    //   • Pack struct fields by size
    //   • Use immutable / constant where possible
    //   • Cache storage variables to memory in loops
    //   • Prefer calldata over memory for read-only array args
    //   • Use uint256 unless you really need a smaller type (small types in storage cost the same; in calldata they save)
    //   • Set to 0 carefully — refund timing is complex post-Merge
}

// === 15. Naming conventions ===
//   constants:    SCREAMING_SNAKE_CASE   (MAX_SUPPLY)
//   state vars:   camelCase              (count, balances)
//   privates:     _underscorePrefix      (_internalHelper)
//   structs:      PascalCase             (User, Order)
//   events:       PascalCase             (Transfer, Approval)
//   functions:    camelCase              (transfer, balanceOf)

// === 16. Common bugs ===
//   • Forgetting 'storage' vs 'memory' → silent copy when you wanted a reference
//   • Public state vars that should be private (anyone can read)
//   • tx.origin authentication → phishing vector; use msg.sender
//   • Unpacked structs → wasted slots, extra gas
//   • Modifying calldata → compile error
//   • Reading from uninitialised local memory variable → unpredictable

Why it matters

storage for state, memory for function locals, calldata for read-only inputs — in increasing order of cheapness. Pack struct fields by size and prefer immutable + constant — gas costs collapse.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
uint256 public total;        // unsigned 256-bit
int256  public balance;
bool    public open = true;
address public owner;
string  public name;
bytes32 public hash;
Try it Yourself »

Discussion

Loading…