Advanced Gas Optimizations in Solidity: Bitmaps for Boolean Flags

2025-11-15 · Solidity / Gas Optimizations · 12 min read

#solidity #gas #evm #optimization #bitmap

In this post we will take a very common pattern in Solidity (many boolean flags) and see how we can store them much more efficiently using a simple idea: treat a uint256 as 256 on/off switches (bits).

Gas optimization illustration

1. Why many bool flags get expensive

In Solidity, contract storage is split into 32-byte (256-bit) slots. A bool is only 1 byte, but the EVM reads and writes whole 32-byte words. If you store each flag separately, over time you end up touching many different storage slots, and each SSTORE is one of the most expensive operations in the EVM.

A typical naive approach to flags might look like this:

Naive: one bool per keysolidity
// BAD: Each bool can end up in its own storage slot
mapping(uint256 => bool) public boolFlags;
function setBoolFlagBad(uint256 index, bool value) public {
boolFlags[index] = value; // One SSTORE per flag
}

If you have hundreds of flags, this pattern means a lot of separate storage writes over the lifetime of the contract.

2. What is a bitmap?

A bitmap is just an integer where each bit (at some bit index) represents a boolean value. In Solidity, a uint256 has 256 bits, so we can store 256 boolean flags in a single number:

  • Bit index 0 → flag #0
  • Bit index 1 → flag #1
  • Bit index 255 → flag #255

You can think of this like an array of flags where the bit index plays the role of an array index: instead offlags[7] = true you flip "bit index 7" on inside a single uint256. Instead of storingbool values in separate storage slots, we store a single uint256 and turn individual bits on or off. This lines up perfectly with how the EVM already handles 256-bit storage words.

3. A small Bitmap library

To keep the code readable, we can wrap the bit operations in a tiny library. Don't worry if bitwise operators feel new – we will walk through them step by step below.

Bitmap.solsolidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
library Bitmap {
// Check whether bit at position `index` is 1 (true)
function isSet(uint256 bitmap, uint256 index) internal pure returns (bool) {
return (bitmap & (1 << index)) != 0;
}
// Return a new bitmap with bit at `index` set to 1
function set(uint256 bitmap, uint256 index) internal pure returns (uint256) {
return bitmap | (1 << index);
}
// Return a new bitmap with bit at `index` cleared to 0
function clear(uint256 bitmap, uint256 index) internal pure returns (uint256) {
return bitmap & ~(1 << index);
}
}

4. Example: user feature flags

Now let's use this library in a simple contract. Each user gets a single uint256 that stores multiple feature flags: can mint, can burn, is admin, is whitelisted, and so on.

UserFeatures.solsolidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./Bitmap.sol";
contract UserFeatures {
using Bitmap for uint256;
// Each address maps to one uint256 bitmap
mapping(address => uint256) private _features;
// Bit positions for our flags
uint256 constant FEATURE_CAN_MINT = 0;
uint256 constant FEATURE_CAN_BURN = 1;
uint256 constant FEATURE_IS_ADMIN = 2;
uint256 constant FEATURE_WHITELISTED = 3;
function enableFeature(address user, uint256 featureIndex) external {
_features[user] = _features[user].set(featureIndex);
}
function disableFeature(address user, uint256 featureIndex) external {
_features[user] = _features[user].clear(featureIndex);
}
function hasFeature(address user, uint256 featureIndex) external view returns (bool) {
return _features[user].isSet(featureIndex);
}
}

All of a user's features now live in a single storage slot (their uint256). Instead of many separate booleans, you get up to 256 flags for the storage cost of one word.

The line using Bitmap for uint256; is a Solidity directive that lets you call library functions as if they were methods on a uint256. Under the hood, the compiler rewrites calls like _features[user].set(featureIndex)to Bitmap.set(_features[user], featureIndex), automatically passing the bitmap as the first argument.

5. How the bit operations actually work

Let's zoom in on a single bitmap and a single flag. Suppose we want to work with FEATURE_IS_ADMIN = 2 (bit position 2).

Remember that a uint256 is 256 bits wide, so we can think of it as 256 tiny on/off switches:

  • uint256: 256 bits → 256 boolean flags
  • Example bitmap: 00000000...00101001
    Positions 0, 3, and 5 are TRUE (bit = 1), all others are FALSE (bit = 0).
  • Step 1 — Create a mask:
    A mask is a number with exactly one bit set to 1. We create it using the left-shift operator <<:
    1 << 0 = 00000001 (decimal 1)
    1 << 2 = 00000100 (decimal 4)
    1 << 3 = 00001000 (decimal 8)
    This mask tells us which single bit we want to work with in the next steps.
  • Step 2 — Set the bit (turn it ON):
    1 << position creates a number with only that bit set:
    1 << 0 = 00000001 (1)
    1 << 3 = 00001000 (8)
    1 << 5 = 00100000 (32)
    Then bitmap |= (1 << position) uses OR to turn that bit ON without touching the others.
    Concrete example:
    Current: 00000101 (bits 0 and 2 are on)
    Set bit 3: 00001000 (1 << 3)
    Result: 00001101 (OR combines them – bit 3 is now on too!).
  • Step 3 — Clear the bit (turn it OFF):
    1 << position gives the mask for that bit, e.g. 00001000 for position 3.
    ~(1 << position) inverts it:11110111 (all bits 1 except the one we want to clear).
    Then bitmap &= ~(1 << position) uses AND to turn that bit OFF while keeping all other bits as they were.
    Concrete example:
    Current: 00001101 (bits 0, 2, 3 are on)
    Clear bit 3: 11110111 (~(1 << 3))
    Result: 00000101 (AND clears bit 3, keeps others).
  • Step 4 — Read the bit (check if it's ON):
    To check if a specific bit is set, we use the mask again with AND:(bitmap & (1 << position)) != 0
    The AND operation isolates that one bit. If the result is non-zero, the bit was on.
    Concrete example:
    Bitmap: 00001101 (bits 0, 2, 3 are on)
    Check bit 3: 00001000 (1 << 3)
    AND result: 00001000 (non-zero → bit 3 is ON ✓)
    Check bit 1: 00000010 (1 << 1)
    AND result: 00000000 (zero → bit 1 is OFF ✗)

The key idea is: we never modify the whole number directly by hand. We always build a small mask that affects exactly one bit, and then combine it with the existing bitmap using bitwise OR (|) or AND (&).

6. Gas intuition: why this is cheaper

The EVM charges most of the cost per storage slot touched, not per bit. With naive mappings, many flags over time can mean many different storage slots are written to. With bitmaps, up to 256 flags share the same slot, so enabling or disabling a feature often just updates the same 32-byte word.

Concrete example: Suppose you need to store 256 boolean flags.

  • Bad way (256 separate bool storage slots):
    256 slots × ~20,000 gas per SSTORE = ~5,120,000 gas
  • Good way (1 uint256 bitmap):
    1 slot × ~20,000 gas = ~20,000 gas
  • Savings: ~99.6% 🎉

In write-heavy code paths (like minting, role changes, or complex workflows), this can translate to dramatically lower gas bills, especially on chains where gas is expensive.

7. Scaling beyond 256 flags

A single uint256 gives you 256 flags. If you ever need more than that per user, you can use multiple words and index them by "bucket":

Multiple bitmaps per usersolidity
mapping(address => mapping(uint256 => uint256)) private _bitmaps;
// wordIndex = flagIndex / 256
// bitIndex = flagIndex % 256

Most applications don't need thousands of independent flags. Often, a small set of well-named feature bits (plus roles or enums) is enough and keeps the code easy to reason about.

8. Practical tips and summary

✅ When to use bitmaps:

  • You have many (10+) related boolean flags per entity
  • Flags are frequently written together in transactions
  • Gas optimization is important for your use case
  • Examples: permissions, feature toggles, completed steps in a workflow

❌ When NOT to use bitmaps:

  • You only have 2-3 flags (the complexity isn't worth it)
  • Flags are rarely updated after initialization
  • Readability is more important than gas savings for your team
  • You're still prototyping and the data model is changing

Best practices:

  • Keep all your FEATURE_* constants in one place and treat their bit positions as part of your public API.
  • Avoid magic numbers like 1 << 7 scattered in the code; always use named constants.
  • Wrap bit operations in a small library (likeBitmap) so your contracts read like high-level intent: set, clear,isSet.
  • Document your bitmap layout clearly in comments – future you will thank you!

Summary: Bitmaps are a powerful pattern when you have many boolean flags. They let you pack up to 256 of them into a single storage slot, reducing storage costs by up to 99% compared to naive bool-based designs. Start with clear and simple state while prototyping, then introduce bitmaps on write-heavy paths once your data model stabilizes and gas costs matter.

© 2025 Ovodo Blog