Section 01 · The problem
Why you cannot loop over a Solidity mapping
Mappings in Solidity are implemented as a hash table that occupies the entire 256-bit key space. Every key has a value — unset keys simply return the zero value for their type.
Quick answer
In one sentence: A Solidity mapping does not store a list of its keys anywhere — the EVM computes each storage slot from the key hash at read/write time — so there is no built-in way to enumerate the keys that have been explicitly set.
When you write balances[addr] = amount, the EVM stores the value at the slot keccak256(addr || mappingSlot). There is no index, no linked list, and no directory of used keys. Reading a key that was never written simply returns zero. This design makes reads and writes O(1) and independent of how many keys have been set, but it makes enumeration impossible without additional bookkeeping.
mapping(address => uint256) public balances;
// This does NOT compile — mappings are not iterable
for (address user in balances) { ... } // ← syntax error
// You cannot know which addresses have been set
// without tracking them yourself.Section 02 · The solution
The enumerable mapping pattern
Pair the mapping with a keys array. Every insert adds the key to the array. Every read iterates the array and looks up each value in the mapping.
// The enumerable mapping pattern
mapping(address => uint256) private _balances;
address[] private _keys;
mapping(address => bool) private _isRegistered; // dedup guard
/// Insert a key-value pair.
function _set(address key, uint256 value) internal {
if (!_isRegistered[key]) {
_keys.push(key);
_isRegistered[key] = true;
}
_balances[key] = value;
}
/// Iterate all entries.
function totalBalance() external view returns (uint256 total) {
uint256 len = _keys.length; // cache length
for (uint256 i = 0; i < len; ) {
total += _balances[_keys[i]];
unchecked { ++i; }
}
}The dedup guard prevents duplicate keys
Without the _isRegistered mapping, calling _set twice for the same address pushes the address into _keys twice. The loop then reads its balance twice and counts it twice. The bool mapping costs one SSTORE on first insert and avoids all duplicates at O(1).
Deletion requires swap-and-pop
To remove a key, find its index in _keys, swap it with the last element, then pop the last element. This keeps the array dense and deletion O(1). Searching for the index by scanning the whole array makes deletion O(n) — for large arrays, store the index in a third mapping to make it O(1).
OpenZeppelin EnumerableSet is a production-ready alternative
OpenZeppelin's EnumerableSet library implements the enumerable pattern for address, bytes32, and uint256 sets, including O(1) add, remove, and contains, with a values() view that returns the full array. For production contracts, use it instead of rolling your own.
Section 03 · Better pattern
Pull over push: eliminating the loop entirely
Many contracts that loop over a mapping to distribute values can be restructured to let recipients pull their own values. This removes the loop from the contract entirely.
// PUSH pattern — dangerous for large recipient sets
function distributeAll() external onlyOwner {
uint256 len = _keys.length; // grows without bound
for (uint256 i = 0; i < len; ) {
_transfer(_keys[i], rewards[_keys[i]]); // O(n) gas
unchecked { ++i; }
}
} // As _keys grows past the block gas limit, this function reverts forever.
// PULL pattern — each user claims their own reward
mapping(address => uint256) public pendingReward;
function addReward(address user, uint256 amount) internal {
pendingReward[user] += amount; // O(1) SSTORE per user
}
function claimReward() external {
uint256 amount = pendingReward[msg.sender];
require(amount > 0, "nothing to claim");
pendingReward[msg.sender] = 0; // clear before transfer
_transfer(msg.sender, amount); // O(1) regardless of total users
}Pull over push is the standard for ETH and token distributions
The push pattern (looping to send to everyone) fails permanently when the array grows past the block gas limit. The pull pattern scales to any number of users because each claim is a single storage read and write. OpenZeppelin's PaymentSplitter and most well-audited airdrop contracts use pull. Prefer pull whenever the number of recipients is not strictly bounded.
Section 04 · Pagination
Paginated iteration for large key sets
When you must read all values and the set is large, add offset and limit parameters so callers can fetch in bounded chunks from off-chain.
/// @notice Return a page of (key, value) pairs from the enumerable mapping.
/// @param offset The index to start from (0 for the first page).
/// @param limit The maximum number of entries to return.
/// @return keys Slice of the keys array.
/// @return values Corresponding balance for each key.
function getPage(
uint256 offset,
uint256 limit
) external view returns (
address[] memory keys,
uint256[] memory values
) {
uint256 total = _keys.length;
if (offset >= total) {
return (new address[](0), new uint256[](0));
}
uint256 end = offset + limit;
if (end > total) end = total;
uint256 size = end - offset;
keys = new address[](size);
values = new uint256[](size);
for (uint256 i = 0; i < size; ) {
address k = _keys[offset + i];
keys[i] = k;
values[i] = _balances[k];
unchecked { ++i; }
}
}Off-chain callers page through the data by incrementing the offset by the limit on each call until the returned slice is shorter than the limit. Each on-chain call is bounded by limit, so gas is predictable regardless of total set size. Keep limit below a few hundred to stay well within block gas limits for storage-heavy reads.
Section 05 · Full contract
Complete example: MemberDAO smart contract
A DAO where members join by paying dues, vote on proposals using their membership weight, and claim ETH rewards. The contract uses the enumerable mapping pattern so the owner can iterate all members to check quorum and distribute rewards via the pull pattern. Deploy on Remix and walk through a full governance cycle.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title MemberDAO
/// @notice DAO with paid membership, on-chain voting, and reward distribution.
/// Uses an enumerable mapping (members mapping + membersArray) so the
/// contract can iterate all members for quorum checks and reward payouts.
/// Capped at MAX_MEMBERS to keep loops gas-safe.
contract MemberDAO {
// ── Constants ────────────────────────────────────────────────────
uint256 public constant MAX_MEMBERS = 100;
uint256 public constant MEMBERSHIP_FEE = 0.05 ether;
uint256 public constant QUORUM_PERCENT = 51; // 51% of members must vote
// ── State ────────────────────────────────────────────────────────
address public owner;
// Enumerable mapping: O(1) lookup + O(n) iteration
mapping(address => bool) public isMember;
mapping(address => uint256) public memberIndex; // member => index in membersArray
address[] public membersArray; // the keys array
// Proposals
struct Proposal {
string description;
uint256 votesFor;
uint256 votesAgainst;
bool executed;
}
Proposal[] public proposals;
mapping(uint256 => mapping(address => bool)) public hasVoted; // proposalId => voter => voted
// Pull-pattern rewards
mapping(address => uint256) public pendingReward;
// ── Events ───────────────────────────────────────────────────────
event MemberJoined(address indexed member);
event MemberLeft(address indexed member);
event ProposalCreated(uint256 indexed id, string description);
event VoteCast(address indexed member, uint256 indexed proposalId, bool support);
event RewardDeposited(uint256 totalAmount, uint256 perMember);
event RewardClaimed(address indexed member, uint256 amount);
// ── Modifiers ────────────────────────────────────────────────────
modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; }
modifier onlyMember() { require(isMember[msg.sender], "not a member"); _; }
constructor() { owner = msg.sender; }
// ── Join / Leave ─────────────────────────────────────────────────
/// Join the DAO by paying the membership fee.
function join() external payable {
require(msg.value == MEMBERSHIP_FEE, "wrong fee");
require(!isMember[msg.sender], "already a member");
require(membersArray.length < MAX_MEMBERS, "DAO full");
// Add to enumerable mapping
isMember[msg.sender] = true;
memberIndex[msg.sender] = membersArray.length;
membersArray.push(msg.sender);
emit MemberJoined(msg.sender);
}
/// Leave the DAO. Removes from the keys array via swap-and-pop.
function leave() external onlyMember {
// Swap-and-pop to keep the array dense and deletion O(1)
uint256 idx = memberIndex[msg.sender];
uint256 lastIdx = membersArray.length - 1;
if (idx != lastIdx) {
address last = membersArray[lastIdx];
membersArray[idx] = last;
memberIndex[last] = idx;
}
membersArray.pop();
delete isMember[msg.sender];
delete memberIndex[msg.sender];
emit MemberLeft(msg.sender);
}
// ── Proposals ────────────────────────────────────────────────────
/// Create a new proposal (members only).
function createProposal(string calldata _description) external onlyMember {
proposals.push(Proposal({ description: _description, votesFor: 0, votesAgainst: 0, executed: false }));
emit ProposalCreated(proposals.length - 1, _description);
}
/// Vote on a proposal (members only, one vote per member per proposal).
function vote(uint256 _proposalId, bool _support) external onlyMember {
require(_proposalId < proposals.length, "invalid proposal");
require(!hasVoted[_proposalId][msg.sender], "already voted");
hasVoted[_proposalId][msg.sender] = true;
if (_support) {
proposals[_proposalId].votesFor++;
} else {
proposals[_proposalId].votesAgainst++;
}
emit VoteCast(msg.sender, _proposalId, _support);
}
// ── Rewards ──────────────────────────────────────────────────────
/// Deposit ETH and split it equally among all current members (pull pattern).
/// Iterates the membersArray — this is the enumerable mapping loop in action.
function depositRewards() external payable onlyOwner {
uint256 len = membersArray.length; // cache length before loop
require(len > 0, "no members");
uint256 perMember = msg.value / len;
require(perMember > 0, "amount too small");
for (uint256 i = 0; i < len; ) {
pendingReward[membersArray[i]] += perMember; // accumulate, don't push
unchecked { ++i; }
}
emit RewardDeposited(msg.value, perMember);
}
/// Claim your pending reward (pull pattern — each member calls this themselves).
function claimReward() external onlyMember {
uint256 amount = pendingReward[msg.sender];
require(amount > 0, "nothing to claim");
pendingReward[msg.sender] = 0; // clear before transfer
(bool ok,) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
emit RewardClaimed(msg.sender, amount);
}
// ── Read ─────────────────────────────────────────────────────────
/// Total members currently in the DAO.
function memberCount() external view returns (uint256) {
return membersArray.length;
}
/// Return all member addresses. Use off-chain only for large DAOs.
function allMembers() external view returns (address[] memory) {
return membersArray;
}
/// Check whether a proposal has reached quorum (51% of members voted).
function hasQuorum(uint256 _proposalId) external view returns (bool) {
require(_proposalId < proposals.length, "invalid proposal");
Proposal storage p = proposals[_proposalId];
uint256 totalVotes = p.votesFor + p.votesAgainst;
uint256 len = membersArray.length;
if (len == 0) return false;
return (totalVotes * 100) / len >= QUORUM_PERCENT;
}
/// Return vote counts for a proposal.
function proposalResult(uint256 _proposalId)
external view
returns (uint256 votesFor, uint256 votesAgainst, bool passed)
{
require(_proposalId < proposals.length, "invalid proposal");
Proposal storage p = proposals[_proposalId];
votesFor = p.votesFor;
votesAgainst = p.votesAgainst;
passed = p.votesFor > p.votesAgainst;
}
}To test the full cycle in Remix: deploy, then call join with 0.05 ETH from three accounts, create a proposal, vote from each account, then call depositRewards with 0.03 ETH — each member gets 0.01 ETH stored in pendingReward. Each member then calls claimReward to receive their share. The enumerable mapping loop in depositRewards is the exact pattern the whole guide builds toward.
Section 07 · FAQ
Frequently asked questions
Answers to the most common questions about using loops with mappings in Solidity.
Can you iterate over a mapping in Solidity?
Not directly. A Solidity mapping does not store its keys anywhere — the EVM derives storage slots from key hashes at read/write time. To iterate, you must maintain a separate array of keys alongside the mapping. Every insert adds the key to the array; every iteration loops over the array and looks up each value in the mapping.
What is the enumerable mapping pattern in Solidity?
The enumerable mapping pattern pairs a mapping with a dynamic array. The mapping gives O(1) lookup for any key. The array records every key that has been inserted, enabling O(n) iteration. A bool mapping or index mapping prevents duplicate keys from being pushed into the array. OpenZeppelin's EnumerableSet is a production-ready implementation of this pattern.
Why is looping over a mapping with many keys dangerous?
The key array can grow without limit. As it grows, the gas cost of any function that iterates the full array grows linearly. Once the array is large enough, the function requires more gas than the block gas limit allows, and the function permanently reverts for every caller. Cap the array with a MAX constant, or restructure to use pagination or the pull pattern.
What is the pull over push pattern for mappings?
Instead of iterating over all addresses and sending each one their owed value (push), the contract stores each address's owed amount in a mapping and lets each address call a claim function to withdraw their own value (pull). This eliminates the on-chain loop entirely. Each claim is a single SLOAD and SSTORE regardless of total user count, and the contract never becomes uncallable due to gas limits.
How do I delete a key from an enumerable mapping?
Use the swap-and-pop technique: find the index of the key in the keys array, swap it with the last element, pop the last element, and update the index mapping for the element that moved. This keeps the array dense and makes deletion O(1) if you stored the index. Delete the key from the value mapping and the index mapping to complete the removal.
How do I use a for loop in Solidity?
A for loop in Solidity uses the same syntax as C or JavaScript: for (uint256 i = 0; i < array.length; i++) { ... }. Use unchecked { ++i; } instead of i++ inside gas-sensitive loops to save the overflow check on each iteration. Always bound loop length to avoid exceeding the block gas limit — an unbounded loop over a growing array will eventually revert permanently.
What is a for loop in Solidity?
A for loop in Solidity is a control flow construct that repeats a block of code a fixed number of times, usually to iterate over an array or a bounded set of keys. Because mappings are not iterable, loops almost always run over an array of keys maintained alongside the mapping. Loop cost scales linearly with iteration count, so Solidity loops must always be bounded to avoid hitting the block gas limit.
How does OpenZeppelin EnumerableSet work in Solidity?
EnumerableSet packages the enumerable mapping pattern into a library. Internally it stores a dynamic array of values plus a mapping from value to one based index. add, remove, contains, length, and at all run in constant time. Use EnumerableSet for production code instead of writing your own pattern from scratch.
Why does Solidity not allow direct mapping iteration?
Solidity mappings are designed for O(1) lookup without storing keys. Each value lives at storage slot keccak256(key, slot) and the key itself is never written anywhere. Without the keys stored, there is no list to iterate. The enumerable pattern adds the key list back as a parallel array, but at the cost of extra storage on every insert and delete.
How do I iterate a map structure in Solidity?
You cannot iterate a Solidity mapping directly because keys are not stored. The pattern is to maintain a parallel array of keys alongside the mapping. On every insert push the key to the array (after a dedup check). On read, loop over the array and look up each value in the mapping. For production code use OpenZeppelin's EnumerableSet or EnumerableMap libraries which package this pattern with O(1) add, remove, contains, and at.