Section 01 · Definition
What is a nested mapping in Solidity?
A nested mapping stores a full mapping as the value of another mapping. Two keys unlock one value — think of it as a table where the outer key is the row and the inner key is the column.
Quick answer
In one sentence: A nested mapping — mapping(A => mapping(B => C)) — is a two-key lookup table in Solidity contract storage: the outer key selects a namespace and the inner key selects the value within it, covering any relationship that requires two identifiers to reach one result.
Flat mappings answer one question per key. Nested mappings answer a question about a pair. “How much has Alice approved Bob to spend?” cannot be answered with one key — it requires two: Alice's address (whose approval budget are we checking?) and Bob's address (which spender?). A flat mapping can only give you the whole Alice row or the whole Bob column, not the intersection.
The nested form gives you the intersection directly: allowance[alice][bob] returns exactly how much Alice has approved Bob to spend. This is the data model behind every ERC20 token that implements approve and transferFrom.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract NestedMappingDemo {
// ERC20 allowance: owner => spender => amount approved
mapping(address => mapping(address => uint256)) public allowance;
// Permission grid: admin => user => can access role
mapping(address => mapping(address => bool)) public hasRole;
// NFT approvals: tokenOwner => operator => isApprovedForAll (ERC721)
mapping(address => mapping(address => bool)) public isApprovedForAll;
// Two-token pool reserves: tokenA => tokenB => liquidity (simplified AMM)
mapping(address => mapping(address => uint256)) public poolReserves;
}Section 02 · Storage layout
How nested mappings live in contract storage
Understanding the two-step hash tells you why nested mappings are O(1) and why collision between entries is cryptographically impossible.
For a mapping at slot p, Solidity stores the value for outer key k1 and inner key k2 at the slot address:
// Pseudocode — the compiler generates this for you
innerSlot = keccak256(k1 || p) // outer key hashed with the mapping's slot
finalSlot = keccak256(k2 || innerSlot) // inner key hashed with the inner slotYou never compute these slot addresses manually — the compiler generates the correct opcodes when you write allowance[owner][spender]. The key insight is that every access is O(1): exactly two keccak256 operations plus one storage read, regardless of how many entries the mapping contains.
The inner mapping has no storage descriptor of its own
In a nested mapping, the inner mapping does not occupy a storage slot. Only the final leaf values occupy slots. The inner slot computed in the first hash step is a logical address used as input to the second hash — not an actual storage location the EVM reads. This is why you cannot pass a mapping(B => C) out of a contract or return it from a function.
Section 03 · The ERC20 allowance pattern
Nested mapping powering ERC20 allowances
The approve and transferFrom mechanism in ERC20 is built entirely on one nested mapping. Understanding it unlocks how DeFi protocols spend tokens on your behalf.
When you call approve(spender, amount) on an ERC20 token, the contract writes allowance[msg.sender][spender] = amount. The spender — typically a DeFi router or lending protocol — can later call transferFrom(owner, recipient, amount), which checks allowance[owner][msg.sender] and deducts the spent amount.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title AllowanceToken — ERC20-style allowance via nested mapping
contract AllowanceToken {
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
uint256 public totalSupply;
string public name = "AllowanceToken";
string public symbol = "ALT";
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(uint256 _supply) {
totalSupply = _supply;
balanceOf[msg.sender] = _supply;
}
// ── Core transfer ────────────────────────────────────────────────
function transfer(address _to, uint256 _value) external returns (bool) {
require(balanceOf[msg.sender] >= _value, "insufficient balance");
require(_to != address(0), "zero address");
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
// ── Allowance (nested mapping) ───────────────────────────────────
/// @notice Set how much _spender can spend on your behalf.
function approve(address _spender, uint256 _value) external returns (bool) {
require(_spender != address(0), "zero address");
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
/// @notice Increase an existing allowance safely (avoids the ERC20 approval race).
function increaseAllowance(address _spender, uint256 _addedValue) external returns (bool) {
allowance[msg.sender][_spender] += _addedValue;
emit Approval(msg.sender, _spender, allowance[msg.sender][_spender]);
return true;
}
/// @notice Decrease an existing allowance safely.
function decreaseAllowance(address _spender, uint256 _subtractedValue) external returns (bool) {
uint256 current = allowance[msg.sender][_spender];
require(current >= _subtractedValue, "allowance below zero");
allowance[msg.sender][_spender] = current - _subtractedValue;
emit Approval(msg.sender, _spender, allowance[msg.sender][_spender]);
return true;
}
/// @notice Spend tokens on behalf of _from, up to the approved amount.
function transferFrom(address _from, address _to, uint256 _value) external returns (bool) {
require(balanceOf[_from] >= _value, "insufficient balance");
require(allowance[_from][msg.sender] >= _value, "allowance exceeded");
require(_to != address(0), "zero address");
// Deduct from allowance first (nested mapping write)
allowance[_from][msg.sender] -= _value;
// Then move the balance
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
emit Transfer(_from, _to, _value);
return true;
}
// ── View helper ──────────────────────────────────────────────────
/// @notice How much can _spender still spend on behalf of _owner?
function getAllowance(address _owner, address _spender)
external view returns (uint256)
{
return allowance[_owner][_spender];
}
}Test this on Remix with three accounts: a token holder (owner), a router address (spender), and a recipient. Call approve from the owner to grant the router an allowance. Switch to the router account and call transferFrom. Watch how allowance[owner][router] decrements with each call.
Section 04 · Three common uses
Where nested mappings appear in production contracts
Beyond ERC20, nested mappings power operator approval, access matrices, and two-dimensional data registries.
ERC721 operator approval
mapping(address => mapping(address => bool)) isApprovedForAll — one bool per (owner, operator) pair. If Alice sets isApprovedForAll[alice][marketplaceAddress] = true, the marketplace can transfer any of Alice's NFTs without needing per-token approval. This is the setApprovalForAll function in every ERC721 contract.
Access control matrix
mapping(address => mapping(address => bool)) hasRole — one bool per (admin, user) pair. Different admins can manage different sets of users independently. A company's HR address can grant user A access; the security address can grant user B access; neither steps on the other's approvals because the outer key separates the namespaces.
Two-token pool reserves
mapping(address => mapping(address => uint256)) reserves — liquidity for the (tokenA, tokenB) pair. A simplified AMM stores reserves[tokenA][tokenB]. Note that reserves[tokenA][tokenB] and reserves[tokenB][tokenA] are different storage slots, so you need to normalise the key order (always smaller address first) to avoid treating the same pool as two separate ones.
Section 05 · Common mistakes
Three nested mapping mistakes to avoid
Treating key order as symmetric
allowance[alice][bob] and allowance[bob][alice] are completely different storage slots. The outer key is always the owner/source and the inner key is always the spender/target. If you swap them, you read or write the wrong approval. Pick a convention — outer is always the actor granting permission — and document it in a comment.
Trying to delete the inner mapping
delete allowance[alice] does not clear all of Alice's per-spender allowances. delete on a nested mapping resets only the outer slot — which in this case is the inner mapping's descriptor, not the leaf values. To reset all allowances for Alice, you would need to know every spender address and clear them individually. There is no atomic clear for a nested mapping.
Forgetting to deduct before transferFrom proceeds
The allowance deduction must happen before any balance transfer or external call. If you transfer first and then deduct the allowance, a reentrancy attack on the balance update can re-enter transferFrom before the allowance is reduced and drain more than the approved amount. Always follow checks-effects-interactions: read allowance, update allowance, then update balances.
FAQ
Frequently asked questions
What is a nested mapping in Solidity?
A nested mapping is a mapping whose value type is another mapping: mapping(A => mapping(B => C)). Two keys are required to reach one value. The outer key selects a namespace and the inner key selects the value within it. The ERC20 allowance mapping — mapping(address => mapping(address => uint256)) — is the most widely deployed example.
How do you read a nested mapping in Solidity?
Use two bracket pairs in sequence: allowance[ownerAddress][spenderAddress]. The compiler generates a two-step keccak256 lookup that resolves to the single storage slot containing the value. You read and write nested mappings the same way you read and write flat mappings — just with two keys instead of one.
How does the ERC20 allowance mapping work in Solidity?
The ERC20 allowance is mapping(address => mapping(address => uint256)). When an owner calls approve(spender, amount), the contract writes allowance[owner][spender] = amount. When a spender calls transferFrom, the contract reads allowance[owner][spender] to check the limit, deducts the spent amount, and then moves the tokens. This lets DeFi protocols spend tokens on your behalf without holding your private key.
Can you have three levels of nesting in a Solidity mapping?
Yes. mapping(A => mapping(B => mapping(C => D))) is legal Solidity. The storage derivation adds one more keccak256 step for each level. In practice, two levels covers nearly every real use case. Three levels is occasionally useful for permission grids (domain => role => user => bool) but adds complexity that a struct-based approach often handles more clearly.
How do you delete entries from a nested mapping in Solidity?
You can delete individual leaf values: delete allowance[owner][spender] resets that specific pair to zero. You cannot delete an entire row in one operation — delete allowance[owner] resets only the outer mapping descriptor, not the individual leaf slots. To clear all allowances for a given owner, you must iterate over every known spender address and delete each leaf individually.