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

Value & Reference Types

Solidity is statically typed with primitives sized in bytes. Picking the right type controls gas, overflow behaviour, and security — uint8 instead of uint256 can save storage in a struct but cost more in a function call.

Primitives, arrays, structs, mappings

EXAMPLE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;   // 0.8+ has built-in overflow checks

contract Types {
    // 1) Booleans
    bool public open = true;

    // 2) Integers — sized in bits, 8 to 256, step 8
    uint256 public count;                  // most common — full word, cheapest in functions
    uint8   public smallCount;             // 0-255; only saves gas when packed in storage
    int256  public signed;                 // -2^255 to 2^255-1
    int8    public smallSigned;

    // 3) Address — 20 bytes
    address public owner;
    address payable public payee;          // can receive ETH via .transfer/.send/.call

    // 4) Fixed-size byte arrays — cheaper than bytes
    bytes32 public root;                    // 32 bytes
    bytes4  public selector;                // function selector

    // 5) Dynamic byte array / string
    bytes  public blob;                     // arbitrary bytes
    string public name;                     // UTF-8 text

    // 6) Enums — compile to uint8
    enum Status { Pending, Active, Closed }
    Status public status;

    // 7) Fixed-size arrays
    uint256[5] public top5;

    // 8) Dynamic arrays
    address[] public holders;

    // 9) Mappings — hash-table
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    // 10) Structs — group related fields
    struct Listing {
        address seller;
        uint96  priceWei;        // packs with seller into one slot (20 + 12 = 32 bytes)
        uint64  deadline;
        bool    sold;
    }
    mapping(uint256 => Listing) public listings;

    // 11) Constants and immutables
    uint256 public constant FEE_BPS = 250;            // baked into bytecode, zero storage
    address public immutable creator;                  // set once in constructor, cheaper than storage

    constructor() {
        creator = msg.sender;
        owner   = msg.sender;
    }

    // ── Storage vs memory vs calldata ──────────────────────────

    // calldata — read-only, cheapest for function inputs
    function setName(string calldata _name) external {
        require(msg.sender == owner, 'not owner');
        name = _name;                                  // copy into storage
    }

    // memory — mutable in-function, freed after
    function uppercase(string calldata s) external pure returns (string memory) {
        bytes memory b = bytes(s);
        for (uint256 i = 0; i < b.length; i++) {
            if (b[i] >= 0x61 && b[i] <= 0x7A) b[i] = bytes1(uint8(b[i]) - 32);
        }
        return string(b);
    }

    // storage — direct pointer into contract state (powerful, risky)
    function _markSold(uint256 id) internal {
        Listing storage l = listings[id];
        l.sold = true;                                  // writes to chain
    }

    // ── Conversions ────────────────────────────────────────────
    function conversions() external pure returns (uint256, int256, bytes32, address) {
        uint8  a = 200;
        uint256 b = uint256(a);                          // safe widening
        int256 c = -1;
        // uint256(c);                                  // would compile but bit-cast
        bytes32 h = bytes32(uint256(1234));
        address addr = address(0xdEaD000000000000000000000000000000000000);
        return (b, c, h, addr);
    }

    // ── Sign-extension and casting safety ──────────────────────
    function downcast(uint256 big) external pure returns (uint8) {
        require(big <= type(uint8).max, 'overflow');
        return uint8(big);                              // safe narrowing
    }

    // ── Built-in introspection ─────────────────────────────────
    function info() external pure returns (uint256 maxU8, uint256 maxU256, int256 minI256) {
        return (type(uint8).max, type(uint256).max, type(int256).min);
    }

    // ── Hashing ────────────────────────────────────────────────
    function digest(string calldata s) external pure returns (bytes32) {
        return keccak256(abi.encodePacked(s));
    }

    // ── Common bugs ────────────────────────────────────────────
    //   • Using uint8 in function args expecting gas savings → no; only saves in packed storage
    //   • Using `address` instead of `address payable` for transfers → compile error
    //   • Storing dynamic strings in mappings without realizing how expensive it is
    //   • Forgetting struct fields pack in DECLARATION order → wrong order wastes slots
    //   • Casting int256 to uint256 with negative value → huge positive number
    //   • Using `bytes32` for arbitrary text and truncating silently → use `string` or `bytes`
}

Why it matters

Pick types for storage layout, not just range. A struct with address (20) + uint96 (12) packs into a single 32-byte slot — rearranging fields by size can cut a write by 20,000 gas. For function arguments, prefer uint256 unless there’s a specific encoding reason.

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

Example

Example
// Value:     uint, int, bool, address, bytes1..32, enum
// Reference: arrays, bytes, string, mapping, struct
// Reference types live in storage / memory / calldata — pick where they reside.
Try it Yourself »

Discussion

Loading…