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.
FAQ
Frequently asked questions
What is mapping(address => bool) used for in Solidity?
It is the standard whitelist and one-time-state pattern in Solidity. Common uses include KYC whitelists for presales, airdrop claim tracking (hasClaimed), vote recording (hasVoted), operator approval (isApproved), and freeze flags (isFrozen). Any per-address yes or no question maps to this type.
How do you create a whitelist in Solidity?
Declare mapping(address => bool) public isWhitelisted, add a management function that sets isWhitelisted[_account] = true behind an onlyOwner modifier, then add a modifier that checks require(isWhitelisted[msg.sender]) before your gated functions. That is the complete whitelist in under 20 lines.
Does mapping(address => bool) cost more gas than mapping(address => uint256)?
For an isolated mapping, no. The EVM always reads a full 32-byte storage slot regardless of the value type. bool and uint256 in separate mapping slots cost the same per read and write. The gas saving from bool comes when it is packed with other small types inside a struct in the same slot.
How do you remove an address from a bool mapping whitelist?
Set it back to false: isWhitelisted[_account] = false. You can also use delete isWhitelisted[_account], which produces the same result. Either way, the address is now not whitelisted. The storage slot still exists — it just returns false on the next read.
Can you use mapping(bool => address) in Solidity?
Technically yes, but it is almost never useful. With only two possible keys — true and false — you would overwrite the same two slots with every write. The only sensible pattern is two named state variables instead. Use bool as a value type, not a key type.