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

Mappings & Arrays

A Solidity mapping is a key → value store: mapping(K => V). Backed by hash storage, O(1) read/write, no built-in iteration. The workhorse for balances, allowances, registries.

Single, nested, struct values, iteration

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

contract MappingExamples {

    // === 1. Simple mapping ===
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "insufficient");
        balances[msg.sender] -= amount;
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
    }

    // public mapping auto-generates getter: balances(address) returns (uint256)

    // === 2. Default values ===
    // mapping returns the ZERO VALUE for unset keys:
    //   uint  → 0
    //   bool  → false
    //   address → 0x0...
    //   bytes → empty
    //   struct → all zeros

    function isRegistered(address user) external view returns (bool) {
        return balances[user] > 0;     // assumes registered users always have a positive balance
    }

    // === 3. Nested mapping (ERC20 allowance pattern) ===
    mapping(address => mapping(address => uint256)) public allowance;
    //         owner       spender       amount

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        require(allowance[from][msg.sender] >= amount, "not allowed");
        allowance[from][msg.sender] -= amount;
        balances[from] -= amount;
        balances[to]   += amount;
        return true;
    }

    // === 4. Struct values ===
    struct Order {
        uint256 id;
        address buyer;
        uint256 amount;
        uint256 createdAt;
        bool    fulfilled;
    }

    mapping(uint256 => Order) public orders;
    uint256 public nextOrderId = 1;

    function placeOrder(uint256 amount) external returns (uint256) {
        uint256 id = nextOrderId++;
        orders[id] = Order({
            id:        id,
            buyer:     msg.sender,
            amount:    amount,
            createdAt: block.timestamp,
            fulfilled: false
        });
        return id;
    }

    function fulfillOrder(uint256 id) external {
        Order storage o = orders[id];      // 'storage' reference; mutates state
        require(!o.fulfilled, "done");
        o.fulfilled = true;
    }

    // === 5. Mapping to array ===
    mapping(address => uint256[]) public userOrders;

    function addUserOrder(uint256 orderId) external {
        userOrders[msg.sender].push(orderId);
    }

    function getUserOrderCount(address user) external view returns (uint256) {
        return userOrders[user].length;
    }

    // === 6. Mapping iteration — NOT supported natively ===
    // To iterate, maintain a separate array of keys:
    mapping(address => uint256) public points;
    address[] public participants;
    mapping(address => bool) public isParticipant;     // dedup

    function addPoints(address user, uint256 amount) external {
        if (!isParticipant[user]) {
            participants.push(user);
            isParticipant[user] = true;
        }
        points[user] += amount;
    }

    function getAllPoints() external view returns (address[] memory, uint256[] memory) {
        uint256 n = participants.length;
        uint256[] memory amounts = new uint256[](n);
        for (uint256 i = 0; i < n; i++) {
            amounts[i] = points[participants[i]];
        }
        return (participants, amounts);
    }

    // === 7. Removing entries ===
    function reset(address user) external {
        delete balances[user];                 // sets back to default value (0)
    }

    // delete on a mapping doesn't 'remove' the entry — it sets the value to zero.
    // The storage slot stays allocated; you don't get a gas refund post-Merge for clearing it back to zero (well, partial).

    // === 8. Mapping with complex key (workaround) ===
    // Solidity mapping keys must be: bool, intN, uintN, addr, bytesN, string, bytes
    // NOT struct, not array. Workaround: hash multiple fields.

    mapping(bytes32 => uint256) public composite;

    function setComposite(address user, uint256 nonce, uint256 value) external {
        bytes32 key = keccak256(abi.encode(user, nonce));
        composite[key] = value;
    }

    function getComposite(address user, uint256 nonce) external view returns (uint256) {
        bytes32 key = keccak256(abi.encode(user, nonce));
        return composite[key];
    }

    // === 9. Storage layout ===
    // Each mapping entry lives at storage slot:
    //   keccak256(abi.encode(key, slot))
    // where 'slot' is the position of the mapping in the contract.
    // No way to enumerate keys from storage layout alone (no 'list all keys').

    // === 10. Gas costs ===
    // Setting from 0 → non-zero:    ~22,100 gas (SSTORE)
    // Changing non-zero → other:    ~5,000 gas
    // Setting to 0:                  refunds some gas (post-Merge rules)
    // Reading:                       ~2,100 gas (cold) or 100 (warm)

    // Map updates are cheap relative to most operations but ADD UP on large batches.
    // Batch operations when possible.

    // === 11. Mapping vs array — when to use which ===
    // mapping  : O(1) lookup by key; no iteration; no 'length'
    // array    : ordered, iterable, has length; lookup is O(N) without an index
    //
    // Common combo: mapping for fast lookup + array for iteration
    //   mapping(uint256 => uint256) idToIndex;
    //   uint256[] ids;

    // === 12. EnumerableMap — OpenZeppelin helper ===
    // import "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
    // EnumerableMap.UintToAddressMap private myMap;
    // myMap.set(1, addr1);
    // myMap.get(1);                          // throws if not set
    // myMap.tryGet(1);                       // (bool, address) — safe lookup
    // myMap.length();
    // myMap.at(i);                           // (key, value) by index
    //
    // Provides O(1) get/set AND iteration. Use when you need both.

    // === 13. Internal mapping behaviours ===
    // - Cannot return entire mapping from a function (too much data)
    // - public mapping auto-getter takes the same arg shape as the key
    // - Nested mapping public getter: balances(address, address) returns uint256
    //
    // For complex state, use VIEW FUNCTIONS to return shaped data.

    // === 14. Events for off-chain mirror ===
    // Mappings live on-chain; clients can't enumerate them.
    // Pattern: emit events on every state change; off-chain indexer (subgraph, dApp) listens.

    event PointsUpdated(address indexed user, uint256 newTotal);

    function addPoints2(address user, uint256 amount) external {
        points[user] += amount;
        emit PointsUpdated(user, points[user]);
    }

    // TheGraph / Subsquid / Goldsky index events into a queryable DB.
}

// === 15. Common patterns ===

// User registry
// mapping(address => User) public users;

// Token balance
// mapping(address => uint256) public balanceOf;

// NFT ownership
// mapping(uint256 => address) public ownerOf;
// mapping(address => uint256) public balanceOf;
// mapping(uint256 => address) public getApproved;
// mapping(address => mapping(address => bool)) public isApprovedForAll;

// Time-locked vesting
// mapping(address => uint256) public releaseTime;
// mapping(address => uint256) public amountLocked;

// Voting
// mapping(uint256 => mapping(address => bool)) public voted;
//          proposalId   voter      hasVoted

// Whitelist
// mapping(address => bool) public isWhitelisted;

// Block reward claims
// mapping(address => uint256) public lastClaimedBlock;

// === 16. Anti-patterns ===

// ❌ Iterating a 'list' of users by pushing to an unbounded array
//    → DoS risk; users can fill the array and make iteration cost too much gas
// ❌ Mappings storing huge structs that include redundant info
//    → wasted storage gas
// ❌ Mapping public getter for sensitive data (private keys, secrets)
//    → blockchains are public; nothing is truly private
// ❌ Forgetting to clean up mapping when user 'leaves'
//    → state grows; doesn't break anything but wastes gas + audit confusion

// === 17. Best practices ===
//   ✅ Use mappings for O(1) lookup by key (addresses, IDs, hashes)
//   ✅ Pair with arrays + indexed events for iteration / enumeration
//   ✅ Use EnumerableMap from OpenZeppelin when you need both lookup + iteration
//   ✅ Pack struct fields by size to save gas
//   ✅ Emit events on every mutation for off-chain mirroring
//   ✅ Test with edge cases: existing key, missing key, key zero, key max

Why it matters

Mappings are O(1) lookups but not iterable — pair with an array + dedup flag (or OpenZeppelin’s EnumerableMap) when you need both. Emit events on every change so off-chain indexers can rebuild the full picture.

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

Example

Example
mapping(address => uint256) public balances;
uint256[] public ids;

function credit(address to, uint256 amount) external {
    balances[to] += amount;
    ids.push(uint256(uint160(to)));
}
Try it Yourself »

Discussion

Loading…