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…