Section 01 · Definition
What is mapping(address => bool) in Solidity?
It is a per-address flag store. Every address starts at false. You set it to true to grant approval and back to false to revoke it.
Quick answer
In one sentence: mapping(address => bool) is Solidity's standard whitelist: every address begins at false, you flip approved addresses to true, and any function can check the flag in one SLOAD before deciding whether to proceed.
The bool type occupies a single bit of meaning but a full 32-byte storage slot when stored alone. In a mapping, each address key produces a separate slot, so each flag is one slot. The cost to check a flag is exactly one SLOAD — the same as reading a uint256 balance. The flag-check itself adds negligible gas after the slot is loaded.
Beyond whitelists, the same pattern handles any one-time or binary state per address: has claimed, has registered, is paused, is operator. If the state can be answered with yes or no and it is per account, a bool mapping is the right type.
// Flag patterns — all use mapping(address => bool)
mapping(address => bool) public isWhitelisted; // KYC / presale gate
mapping(address => bool) public hasClaimed; // airdrop one-time claim
mapping(address => bool) public isOperator; // delegated operator
mapping(address => bool) public isFrozen; // freeze by admin
mapping(address => bool) public hasVoted; // one vote per addressSection 02 · The whitelist pattern
Building a whitelist with mapping(address => bool)
Three pieces make a complete whitelist: the mapping, a management function behind an access check, and a modifier that gates protected functions.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Whitelist {
address public admin;
mapping(address => bool) public isWhitelisted;
event Added(address indexed account);
event Removed(address indexed account);
constructor() { admin = msg.sender; }
modifier onlyAdmin() {
require(msg.sender == admin, "not admin");
_;
}
modifier onlyWhitelisted() {
require(isWhitelisted[msg.sender], "not whitelisted");
_;
}
// ── Management (admin only) ──────────────────────────────────────
function add(address _account) external onlyAdmin {
require(_account != address(0), "zero address");
require(!isWhitelisted[_account], "already listed");
isWhitelisted[_account] = true;
emit Added(_account);
}
function addBatch(address[] calldata _accounts) external onlyAdmin {
for (uint256 i = 0; i < _accounts.length; i++) {
if (_accounts[i] != address(0) && !isWhitelisted[_accounts[i]]) {
isWhitelisted[_accounts[i]] = true;
emit Added(_accounts[i]);
}
}
}
function remove(address _account) external onlyAdmin {
require(isWhitelisted[_account], "not listed");
isWhitelisted[_account] = false;
emit Removed(_account);
}
// ── Gated action ────────────────────────────────────────────────
function protectedAction() external onlyWhitelisted {
// only addresses in the whitelist reach this code
}
}addBatch is safer with a length cap
An unbounded loop over a calldata array can exceed the block gas limit if the caller passes thousands of addresses. Add a cap: require(_accounts.length <= 200, 'batch too large'). This keeps gas consumption predictable and prevents accidental or malicious denial of service on the admin function.
Section 03 · Role access control
Multiple bool mappings for role-based access
When a contract has more than two permission levels, one bool mapping per role is cleaner than a single uint8 role field.
A typical DeFi protocol has at least three actor types: the admin who sets parameters, the operator who can trigger automated actions, and the user who interacts with the protocol. Each gets its own bool mapping rather than a shared enum or integer.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title RoleAccess — multi-role access control via bool mappings
contract RoleAccess {
address public owner;
mapping(address => bool) public isAdmin;
mapping(address => bool) public isOperator;
mapping(address => bool) public isMinter;
event RoleGranted(string role, address indexed account);
event RoleRevoked(string role, address indexed account);
constructor() {
owner = msg.sender;
isAdmin[msg.sender] = true;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
modifier onlyAdmin() {
require(isAdmin[msg.sender], "not admin");
_;
}
modifier onlyOperator() {
require(isOperator[msg.sender] || isAdmin[msg.sender], "not operator");
_;
}
modifier onlyMinter() {
require(isMinter[msg.sender] || isAdmin[msg.sender], "not minter");
_;
}
// ── Role management (owner only) ─────────────────────────────────
function grantAdmin(address _account) external onlyOwner {
isAdmin[_account] = true;
emit RoleGranted("admin", _account);
}
function grantOperator(address _account) external onlyAdmin {
isOperator[_account] = true;
emit RoleGranted("operator", _account);
}
function grantMinter(address _account) external onlyAdmin {
isMinter[_account] = true;
emit RoleGranted("minter", _account);
}
function revokeAdmin(address _account) external onlyOwner {
require(_account != owner, "cannot revoke owner");
isAdmin[_account] = false;
emit RoleRevoked("admin", _account);
}
function revokeOperator(address _account) external onlyAdmin {
isOperator[_account] = false;
emit RoleRevoked("operator", _account);
}
function revokeMinter(address _account) external onlyAdmin {
isMinter[_account] = false;
emit RoleRevoked("minter", _account);
}
// ── Role-gated actions ───────────────────────────────────────────
function adminAction() external onlyAdmin {
// admin-only logic here
}
function operatorAction() external onlyOperator {
// operator or admin logic here
}
function mintToken(address _to) external onlyMinter {
// minter or admin logic here
}
}Section 04 · Full contract
Complete example: PresaleGate with bool mappings
A presale contract uses three bool mappings — whitelist approval, purchase record, and refund eligibility — wired together into one deployable contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title PresaleGate
/// @notice Manages a token presale with whitelist, purchase tracking,
/// and refund eligibility using three bool mappings.
contract PresaleGate {
// ── State ────────────────────────────────────────────────────────
address public owner;
uint256 public pricePerToken; // in wei
uint256 public tokensSold;
uint256 public totalTokens;
bool public saleActive;
// ── Bool mappings ────────────────────────────────────────────────
mapping(address => bool) public isWhitelisted; // KYC approved
mapping(address => bool) public hasPurchased; // one purchase per address
mapping(address => bool) public isRefundEligible; // refund window open
// ── Purchase records ─────────────────────────────────────────────
mapping(address => uint256) public tokensBought;
mapping(address => uint256) public amountPaid;
// ── Events ───────────────────────────────────────────────────────
event Whitelisted(address indexed account);
event Purchased(address indexed buyer, uint256 tokens, uint256 paid);
event RefundIssued(address indexed buyer, uint256 amount);
event SaleToggled(bool active);
// ── Constructor ──────────────────────────────────────────────────
constructor(uint256 _price, uint256 _supply) {
owner = msg.sender;
pricePerToken = _price;
totalTokens = _supply;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
modifier onlyWhitelisted() {
require(isWhitelisted[msg.sender], "not whitelisted");
_;
}
modifier saleIsActive() {
require(saleActive, "sale not active");
_;
}
// ── Management ───────────────────────────────────────────────────
function addToWhitelist(address _account) external onlyOwner {
require(_account != address(0), "zero address");
isWhitelisted[_account] = true;
emit Whitelisted(_account);
}
function toggleSale() external onlyOwner {
saleActive = !saleActive;
emit SaleToggled(saleActive);
}
function grantRefund(address _buyer) external onlyOwner {
require(hasPurchased[_buyer], "no purchase");
isRefundEligible[_buyer] = true;
}
// ── Purchase ─────────────────────────────────────────────────────
/// @notice Purchase tokens. Whitelisted addresses only, one purchase per address.
function purchase(uint256 _tokenAmount)
external payable onlyWhitelisted saleIsActive
{
require(!hasPurchased[msg.sender], "already purchased");
require(_tokenAmount > 0, "zero amount");
require(tokensSold + _tokenAmount <= totalTokens, "insufficient supply");
uint256 cost = _tokenAmount * pricePerToken;
require(msg.value == cost, "incorrect payment");
hasPurchased[msg.sender] = true;
tokensBought[msg.sender] = _tokenAmount;
amountPaid[msg.sender] = msg.value;
tokensSold += _tokenAmount;
emit Purchased(msg.sender, _tokenAmount, msg.value);
}
// ── Refund ───────────────────────────────────────────────────────
/// @notice Claim a refund if the owner granted refund eligibility.
function claimRefund() external {
require(hasPurchased[msg.sender], "no purchase");
require(isRefundEligible[msg.sender], "not eligible for refund");
uint256 refundAmount = amountPaid[msg.sender];
hasPurchased[msg.sender] = false;
isRefundEligible[msg.sender] = false;
amountPaid[msg.sender] = 0;
(bool sent, ) = payable(msg.sender).call{value: refundAmount}("");
require(sent, "refund failed");
emit RefundIssued(msg.sender, refundAmount);
}
// ── View ─────────────────────────────────────────────────────────
function remainingSupply() external view returns (uint256) {
return totalTokens - tokensSold;
}
}Notice how the three bool mappings each carry a distinct semantic weight: isWhitelisted gates entry, hasPurchased prevents double purchases, and isRefundEligible controls the refund window. Each is one storage slot per address. Combining all three into a single uint8 role field would be more gas efficient but far harder to read and audit — the clarity tradeoff is usually worth the extra slot.
Section 05 · Common mistakes
Three bool mapping mistakes to watch for
Checking isWhitelisted[address(0)]
The zero address is the default for uninitialised address variables. If your contract ever passes address(0) to an isWhitelisted check — perhaps because a state variable was not initialised — and isWhitelisted[address(0)] happens to be true, the gate passes silently. Always validate user-supplied addresses with require(_addr != address(0)) before any mapping read that influences access control.
Using bool instead of a counter for claimed-once logic
hasClaimed[msg.sender] = true is perfect for one-time claims. But if you later need to allow a second claim window or a reset, you now have a bool with no way to track how many times the claim was made. Consider whether a uint256 claimCount[msg.sender] serves your roadmap better — you can still gate on claimCount[msg.sender] == 0 for the first claim.
Setting a bool before an external call
The checks-effects-interactions pattern requires you to update state before any external call. hasPurchased[msg.sender] = true must come before any ETH transfer or token mint, not after. If you set the bool after the call, a reentrancy attack can call your function again before the flag is set and run the purchase logic twice.