Tutoriel·10 min de lecture·Par Solingo

Understanding Solidity Mappings — From Basics to Advanced Patterns

Master Solidity mappings with this comprehensive guide covering basic usage, nested structures, iterable patterns, and gas optimization techniques.

# Understanding Solidity Mappings — From Basics to Advanced Patterns

Mappings are one of the most fundamental data structures in Solidity, yet they're often misunderstood by developers coming from other languages. This guide will take you from basic mapping concepts to advanced patterns used in production smart contracts.

What Are Mappings?

A mapping is a hash table data structure that associates keys with values. Think of it as a dictionary or hash map in other programming languages, but with some unique blockchain-specific characteristics.

// Basic mapping declaration

mapping(address => uint256) public balances;

// Using the mapping

function deposit() public payable {

balances[msg.sender] += msg.value;

}

function getBalance(address user) public view returns (uint256) {

return balances[user];

}

Key Characteristics

  • All keys virtually exist: Unlike traditional hash maps, every possible key returns a value. Unset values return the default (0 for uint, address(0) for address, etc.).
  • No length property: You cannot iterate over a mapping or get its size.
  • Keys are not stored: Only the hash of the key is used, so you cannot retrieve all keys.
  • Very gas efficient: Reading and writing to mappings costs constant gas regardless of size.
  • Mappings vs Arrays

    Many developers wonder when to use mappings versus arrays. Here's a practical comparison:

    contract MappingVsArray {
    

    // Mapping: O(1) lookup, no iteration

    mapping(address => bool) public whitelist;

    // Array: Can iterate, but O(n) lookup

    address[] public whitelistArray;

    // Mapping approach - very gas efficient

    function addToWhitelist(address user) public {

    whitelist[user] = true;

    // Cost: ~20,000 gas for first write, ~5,000 for updates

    }

    // Array approach - can iterate but expensive

    function addToWhitelistArray(address user) public {

    whitelistArray.push(user);

    // Cost: ~44,000+ gas and grows with array size

    }

    // Checking membership

    function isWhitelisted(address user) public view returns (bool) {

    return whitelist[user]; // O(1) - constant gas

    }

    function isWhitelistedArray(address user) public view returns (bool) {

    for (uint i = 0; i < whitelistArray.length; i++) {

    if (whitelistArray[i] == user) return true;

    }

    return false; // O(n) - gas grows linearly

    }

    }

    Use mappings when:

    • You need fast lookups by key
    • You don't need to iterate over all values
    • You want predictable gas costs

    Use arrays when:

    • You need to iterate over all elements
    • You need to know the count of elements
    • Order matters

    Nested Mappings

    Nested mappings unlock powerful multi-dimensional data structures:

    contract TokenAllowances {
    

    // owner => spender => amount

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

    function approve(address spender, uint256 amount) public {

    allowances[msg.sender][spender] = amount;

    }

    function transferFrom(

    address owner,

    address recipient,

    uint256 amount

    ) public {

    require(allowances[owner][msg.sender] >= amount, "Insufficient allowance");

    allowances[owner][msg.sender] -= amount;

    // ... transfer logic

    }

    }

    Triple Nested Mappings

    For complex use cases like multi-token DEXs:

    contract MultiTokenExchange {
    

    // user => token => exchange => balance

    mapping(address => mapping(address => mapping(address => uint256))) public deposits;

    function depositToExchange(

    address token,

    address exchange,

    uint256 amount

    ) public {

    deposits[msg.sender][token][exchange] += amount;

    // ... transfer logic

    }

    }

    The Iterable Mapping Pattern

    The biggest limitation of mappings is that you cannot iterate over them. Here's a battle-tested pattern to make them iterable:

    contract IterableMapping {
    

    struct UserInfo {

    uint256 balance;

    uint256 index; // Position in the keys array

    bool exists;

    }

    mapping(address => UserInfo) public users;

    address[] public keys;

    function set(address key, uint256 value) public {

    if (!users[key].exists) {

    // New user - add to keys array

    users[key].exists = true;

    users[key].index = keys.length;

    keys.push(key);

    }

    users[key].balance = value;

    }

    function remove(address key) public {

    require(users[key].exists, "User does not exist");

    // Move the last element to the deleted spot

    uint256 indexToDelete = users[key].index;

    address lastKey = keys[keys.length - 1];

    keys[indexToDelete] = lastKey;

    users[lastKey].index = indexToDelete;

    keys.pop();

    delete users[key];

    }

    function getAll() public view returns (address[] memory) {

    return keys;

    }

    function size() public view returns (uint256) {

    return keys.length;

    }

    function iterate(uint256 start, uint256 count)

    public

    view

    returns (address[] memory addresses, uint256[] memory balances)

    {

    require(start + count <= keys.length, "Out of bounds");

    addresses = new address[](count);

    balances = new uint256[](count);

    for (uint256 i = 0; i < count; i++) {

    address key = keys[start + i];

    addresses[i] = key;

    balances[i] = users[key].balance;

    }

    }

    }

    This pattern is used in protocols like Uniswap v2 for tracking all pairs and in staking contracts for reward distribution.

    Gas Optimization Techniques

    1. Packing Values in Structs

    When using mappings with structs, pack values carefully:

    // Bad - each field takes a full slot
    

    struct User {

    uint256 balance; // 32 bytes

    uint256 lastUpdate; // 32 bytes

    bool isActive; // 32 bytes (wasteful!)

    }

    // Good - packed into fewer slots

    struct UserOptimized {

    uint128 balance; // 16 bytes

    uint64 lastUpdate; // 8 bytes

    uint32 rewardRate; // 4 bytes

    uint8 tier; // 1 byte

    bool isActive; // 1 byte

    // Total: 30 bytes = 1 storage slot

    }

    mapping(address => UserOptimized) public users;

    2. Avoid Unnecessary Reads

    // Bad - reads from storage 3 times
    

    function processUser(address user) public {

    if (users[user].balance > 0) {

    users[user].balance -= 100;

    users[user].lastUpdate = block.timestamp;

    }

    }

    // Good - read once, write once

    function processUserOptimized(address user) public {

    UserOptimized memory u = users[user];

    if (u.balance > 0) {

    u.balance -= 100;

    u.lastUpdate = uint64(block.timestamp);

    users[user] = u;

    }

    }

    3. Delete to Get Gas Refunds

    function clearUser(address user) public {
    

    delete users[user]; // Get gas refund for clearing storage

    }

    Advanced Pattern: Enumerable Set

    Using OpenZeppelin's EnumerableSet with mappings:

    import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
    
    

    contract RoleManager {

    using EnumerableSet for EnumerableSet.AddressSet;

    mapping(bytes32 => EnumerableSet.AddressSet) private roleMembers;

    function grantRole(bytes32 role, address account) public {

    roleMembers[role].add(account);

    }

    function revokeRole(bytes32 role, address account) public {

    roleMembers[role].remove(account);

    }

    function getRoleMemberCount(bytes32 role) public view returns (uint256) {

    return roleMembers[role].length();

    }

    function getRoleMember(bytes32 role, uint256 index) public view returns (address) {

    return roleMembers[role].at(index);

    }

    }

    Common Pitfalls

    1. Assuming Deletion Resets Everything

    struct User {
    

    uint256 balance;

    address[] friends;

    }

    mapping(address => User) public users;

    // This does NOT delete the friends array!

    delete users[msg.sender];

    // You must manually delete nested dynamic arrays

    delete users[msg.sender].friends;

    delete users[msg.sender];

    2. Using Mappings as Function Parameters

    Mappings can only be used as storage references:

    // This will NOT compile
    

    function process(mapping(address => uint256) memory data) public {

    // Error: mappings cannot be memory

    }

    // Instead, pass storage reference (internal/private only)

    function process(mapping(address => uint256) storage data) internal {

    // This works

    }

    Conclusion

    Mappings are the backbone of efficient Solidity development. Master these patterns and you'll write cleaner, more gas-efficient smart contracts. Remember:

    • Use mappings for O(1) lookups
    • Implement the iterable pattern when you need enumeration
    • Pack struct values to save gas
    • Use EnumerableSet for advanced use cases
    • Always consider gas costs when designing your data structures

    Start experimenting with these patterns in your next project, and you'll quickly see why mappings are so powerful in Solidity development.

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement