Section 01 · Definition
What is mapping with address in Solidity?
A mapping with address as the key gives every Ethereum account its own slot in contract storage. Each slot holds one value of the declared value type.
Quick answer
In one sentence: mapping(address => ValueType) is Solidity's built-in per account store — every Ethereum address gets one slot that starts at the zero value and changes only when you write to it.
The address type represents a 20-byte Ethereum account — a wallet, a contract, or a multisig. When you declare mapping(address => uint256) public balances, the compiler does not allocate a table of every possible address. Instead it reserves one storage slot for the mapping descriptor, then computes a unique derived slot on demand by hashing the address together with that descriptor slot number.
The result is a sparse structure with O(1) read and write performance. Looking up balances[user] for an address you have never touched costs exactly one SLOAD — it returns zero, not an error. That default-zero behaviour is why ERC20 contracts do not need a constructor that pre-fills every wallet to zero.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract BalanceDemo {
// address as KEY — one slot per account
mapping(address => uint256) public balances;
// address as VALUE — owner lookup by token ID
mapping(uint256 => address) public ownerOf;
// address as KEY, bool as VALUE — whitelist flag
mapping(address => bool) public isWhitelisted;
}All three forms are valid. The choice depends on what question you are answering. “How much does this account hold?” uses address as key. “Who owns token 42?” uses address as value. “Is this account approved?” uses address as key with a bool value.
Section 02 · Syntax
Declaring and reading address mappings
The declaration follows the same pattern as any mapping. What changes is whether address sits on the left side of the arrow, the right side, or both.
// Pattern A — address as KEY, uint256 as VALUE
// Use case: balance per account, vote weight, staked amount
mapping(address => uint256) public balances;
// Pattern B — uint256 as KEY, address as VALUE
// Use case: token owner, proposal creator, slot occupant
mapping(uint256 => address) public ownerOf;
// Pattern C — address as KEY, address as VALUE
// Use case: approved operator, referral, replacement address
mapping(address => address) public approvedOperator;
// Pattern D — address as KEY, bool as VALUE
// Use case: whitelist, has-voted flag, frozen flag
mapping(address => bool) public isWhitelisted;
// Pattern E — address as KEY, struct as VALUE
// Use case: full account record (balance + tier + joined date)
struct Account { uint256 balance; uint8 tier; uint40 joinedAt; }
mapping(address => Account) public accounts;Declaring a mapping public generates a free getter function with the same name. For Pattern A, calling balances(someAddress) from a frontend returns the stored value without any extra code. For nested mappings, the getter takes two arguments.
Mappings cannot be used as function parameters or return types
Solidity prevents you from passing a mapping to an external or public function, or returning one. If you need to expose a slice of mapping data to the outside world, collect it into a memory array inside a view function and return the array.
Section 03 · Read and write
How to read from and write to address mappings
Reading and writing use the bracket syntax. The key goes in brackets. The returned value is the stored value type, always starting at zero for any key you have not touched.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ReadWriteDemo {
mapping(address => uint256) public tokenBalance;
mapping(address => uint256) public ethBalance;
// WRITE — deposit ETH, update ethBalance for caller
function depositEth() external payable {
ethBalance[msg.sender] += msg.value;
}
// WRITE — withdraw ETH from caller's ethBalance
function withdrawEth(uint256 _amount) external {
require(ethBalance[msg.sender] >= _amount, "insufficient ETH");
ethBalance[msg.sender] -= _amount;
(bool ok, ) = msg.sender.call{value: _amount}("");
require(ok, "transfer failed");
}
// WRITE — credit internal tokens to caller
function depositTokens(uint256 _amount) external {
tokenBalance[msg.sender] += _amount;
}
// READ — retrieve tokenBalance for any address
function getTokenBalance(address _user) external view returns (uint256) {
return tokenBalance[_user]; // returns 0 for any address never deposited
}
// DELETE — reset a slot to its zero value
function clearTokenBalance() external {
delete tokenBalance[msg.sender]; // sets to 0, cheaper than writing 0 explicitly
}
}The delete keyword resets the slot to the zero value for its type. For uint256 that is zero, for address it is address(0). Ethereum refunds some gas when you clear a previously non-zero storage slot, so delete is cheaper than writing an explicit zero.
Section 04 · Production patterns
Three address mapping patterns used in real contracts
Most of the value stored on Ethereum today lives in one of these three shapes. Recognising the pattern tells you which mapping form to reach for.
Balance tracker
mapping(address => uint256) — the ERC20 workhorse. Every address holds one number. Increment on deposit, decrement on withdrawal. The public getter handles read access automatically. Most balance tracking logic fits in under 20 lines around this one mapping.
Ownership registry
mapping(uint256 => address) — the ERC721 foundation. An integer ID maps to the address that owns it. Transfer is two writes: clear the old owner slot and set the new one. This pattern handles any ID to account relationship: NFT ownership, slot reservation, subscription seat.
Approval grant
mapping(address => address) or mapping(address => mapping(address => uint256)) — one account gives another permission. The single address form tracks one approved party per account. The nested form tracks a spending limit per approved party, which is exactly how ERC20 allowances work.
Each pattern appears in production contracts deployed millions of times. ERC20 uses all three simultaneously: a balance tracker for balanceOf, a nested approval grant for allowance, and a bool mapping for any transfer guard. Learning these three shapes gives you the mental model to read any ERC token contract.
Section 05 · Full contract
Complete example: ReadWriteDemo smart contract
This contract keeps two separate address mappings — one for ETH balances, one for internal tokens — and demonstrates deposit, withdrawal, mint, transfer, and burn in one deployable unit.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ReadWriteDemo {
// ── State ──────────────────────────────────────────────────────────
mapping(address => uint256) public tokenBalance;
mapping(address => uint256) public ethBalance;
uint256 public totalSupply;
// ── Events ─────────────────────────────────────────────────────────
event TokensMinted(address indexed to, uint256 amount);
event TokensTransferred(address indexed from, address indexed to, uint256 amount);
event EthDeposited(address indexed from, uint256 amount);
event EthWithdrawn(address indexed to, uint256 amount);
// ── ETH ────────────────────────────────────────────────────────────
function depositEth() external payable {
ethBalance[msg.sender] += msg.value;
emit EthDeposited(msg.sender, msg.value);
}
function withdrawEth(uint256 _amount) external {
require(ethBalance[msg.sender] >= _amount, "insufficient ETH");
ethBalance[msg.sender] -= _amount;
(bool ok, ) = msg.sender.call{value: _amount}("");
require(ok, "transfer failed");
emit EthWithdrawn(msg.sender, _amount);
}
// ── Tokens ─────────────────────────────────────────────────────────
function depositTokens(uint256 _amount) external {
tokenBalance[msg.sender] += _amount;
totalSupply += _amount;
emit TokensMinted(msg.sender, _amount);
}
function mintTokens(address _to, uint256 _amount) external {
tokenBalance[_to] += _amount;
totalSupply += _amount;
emit TokensMinted(_to, _amount);
}
function transferTokens(address _to, uint256 _amount) external {
require(tokenBalance[msg.sender] >= _amount, "insufficient tokens");
tokenBalance[msg.sender] -= _amount;
tokenBalance[_to] += _amount;
emit TokensTransferred(msg.sender, _to, _amount);
}
function burnTokens(uint256 _amount) external {
require(tokenBalance[msg.sender] >= _amount, "insufficient tokens");
tokenBalance[msg.sender] -= _amount;
tokenBalance[address(0)] += _amount;
totalSupply -= _amount;
emit TokensTransferred(msg.sender, address(0), _amount);
}
// ── Views ──────────────────────────────────────────────────────────
function burnedSupply() external view returns (uint256) {
return tokenBalance[address(0)];
}
receive() external payable {
ethBalance[msg.sender] += msg.value;
}
}Deploy this on Remix with no constructor arguments. Call depositEth with some Wei to see ethBalance update. Call mintTokens from a second account to see tokenBalance and totalSupply grow. Call burnTokens and check burnedSupply to see the address(0) accumulator pattern live.
Why no SafeMath?
Solidity 0.8+ added overflow and underflow checks into the compiler itself. Every arithmetic operation reverts automatically if it overflows. You no longer need to import SafeMath or use checked arithmetic wrappers — the language handles it at zero extra gas cost in most cases.
Section 06 · Common mistakes
Four address mapping mistakes beginners make
Each of these trips up new Solidity developers at least once. Knowing them in advance saves a failed deployment.
Using address(0) as a valid key
The zero address is the default for any uninitialised address variable. If you allow address(0) as a key, you lose the ability to distinguish between 'nobody set this' and 'the zero address explicitly set this'. Always check require(_addr != address(0)) before writing to a mapping keyed by a user-supplied address.
Assuming unvisited keys throw an error
balances[someNewAddress] returns 0 — it does not revert, it does not return a special sentinel. If your logic branches on the returned value, make sure zero is a safe default for your use case. A vault that checks if (balances[msg.sender] > 0) works correctly; one that checks if (balances[msg.sender] != 0) on a fresh address also works, but the intent is clearer.
Forgetting that mappings cannot be iterated
There is no built-in way to loop over every key in a mapping. If you need to enumerate all addresses that have deposited — to airdrop rewards or to compute a snapshot — maintain a parallel address[] depositors array and push each new depositor into it on their first write. This is a standard pattern in ERC20 contracts that support snapshots.
Treating delete as removal
delete balances[user] resets the slot to zero — it does not remove the key from some internal list, because no such list exists. The slot still exists; it just returns zero on the next read. If your frontend checks for zero to decide whether an address has interacted with the contract, add a separate bool hasFunded mapping instead of relying on the balance being zero.
FAQ
Frequently asked questions
What is mapping(address => uint256) used for in Solidity?
It is the standard way to track one numeric value per Ethereum account — token balances in ERC20, vote weights in governance contracts, staked amounts in DeFi protocols, and unlock timestamps in vesting contracts. Every ERC20 token you have ever held uses this mapping internally for balanceOf.
Can you use address as a value type in a Solidity mapping?
Yes. mapping(uint256 => address) is how ERC721 tracks token ownership — each token ID maps to the address that owns it. mapping(address => address) tracks delegation or approval relationships. Address works correctly as either the key or the value.
What is the default value of a mapping(address => uint256) in Solidity?
Every unwritten address key returns 0 — the default value for uint256. Solidity mappings are virtually pre-filled with the zero value for their value type. For uint256 that is 0, for address it is address(0), for bool it is false. No initialisation is needed.
How do you iterate over all addresses in a Solidity mapping?
You cannot iterate a Solidity mapping directly — it has no built-in list of keys. The standard pattern is to maintain a parallel dynamic array (address[] public participants) and push each new address into it the first time it writes to the mapping. You can then loop over the array to enumerate all participants.
Is mapping(address => uint256) gas efficient in Solidity?
Yes. Every read and write is O(1): one keccak256 hash plus one SLOAD or SSTORE. The cost is the same whether the mapping has one entry or one million. The only gas concern is the cost of SSTORE itself (20 000 gas for a cold write, 2 900 for a warm write), which applies equally to any storage type.