Skip to main content

Overview

The BitMask library provides efficient bit manipulation functions for managing token sets in Gearbox Protocol. Bitmasks are used extensively to track enabled tokens on credit accounts and forbidden tokens in configurations, offering gas-efficient set operations.

Key Concepts

Bitmask Representation: A uint256 where the i-th bit represents the i-th item in a set:
  • Bit = 1: Item is in the set
  • Bit = 0: Item is not in the set
Token Masks: Each token has a mask equal to 2**i where i is the token’s index:
token0: 0x0001 (bit 0)
token1: 0x0002 (bit 1)
token2: 0x0004 (bit 2)
token3: 0x0008 (bit 3)
// ... up to 255 tokens
Set Inclusion Check:
bool included = (tokenMask & enabledTokensMask) != 0;
Bitmasks allow checking if any token from a set is enabled using a single bitwise AND operation, making them extremely gas-efficient compared to array iterations.

Core Functions

calcEnabledTokens

function calcEnabledTokens(
    uint256 enabledTokensMask
) internal pure returns (uint256 totalTokensEnabled)
Counts the number of 1 bits in a mask (population count). Parameters:
  • enabledTokensMask - The bitmask to count bits in
Returns: Number of enabled tokens (bits set to 1) Algorithm: Brian Kernighan’s algorithm
while (mask > 0) {
    mask &= mask - 1;  // Clears the lowest set bit
    count++;
}
This algorithm runs in O(k) where k is the number of set bits, not the total number of possible bits. For sparse masks, this is very efficient.

enable

function enable(
    uint256 enabledTokenMask,
    uint256 bitsToEnable
) internal pure returns (uint256)
Enables (sets to 1) specified bits in a mask. Parameters:
  • enabledTokenMask - Current mask
  • bitsToEnable - Mask of bits to enable
Returns: Updated mask with specified bits enabled Operation: Bitwise OR
return enabledTokenMask | bitsToEnable;

disable

function disable(
    uint256 enabledTokenMask,
    uint256 bitsToDisable
) internal pure returns (uint256)
Disables (sets to 0) specified bits in a mask. Parameters:
  • enabledTokenMask - Current mask
  • bitsToDisable - Mask of bits to disable
Returns: Updated mask with specified bits disabled Operation: Bitwise AND with complement
return enabledTokenMask & ~bitsToDisable;

enableDisable

function enableDisable(
    uint256 enabledTokensMask,
    uint256 bitsToEnable,
    uint256 bitsToDisable
) internal pure returns (uint256)
Atomically enables and disables specified bits. Parameters:
  • enabledTokensMask - Current mask
  • bitsToEnable - Mask of bits to enable
  • bitsToDisable - Mask of bits to disable
Returns: Updated mask with both operations applied Operation: Enable first, then disable
return (enabledTokensMask | bitsToEnable) & (~bitsToDisable);
Bits to enable are applied before bits to disable. If the same bit appears in both parameters, it will be disabled in the final result.

lsbMask

function lsbMask(
    uint256 mask
) internal pure returns (uint256)
Returns a mask with only the least significant bit (LSB) of the input mask. Parameters:
  • mask - Input mask
Returns: Mask with only the lowest set bit enabled Operation: Two’s complement trick
return mask & uint256(-int256(mask));
This function is useful for iterating over set bits efficiently:
while (mask != 0) {
    uint256 currentBit = mask.lsbMask();
    // Process currentBit
    mask = mask.disable(currentBit);
}

Usage Examples

Basic Token Management

import {BitMask} from "@gearbox-protocol/core-v3/contracts/libraries/BitMask.sol";

contract TokenManager {
    using BitMask for uint256;

    uint256 public enabledTokensMask;

    function enableToken(uint256 tokenMask) external {
        enabledTokensMask = enabledTokensMask.enable(tokenMask);
    }

    function disableToken(uint256 tokenMask) external {
        enabledTokensMask = enabledTokensMask.disable(tokenMask);
    }

    function isTokenEnabled(uint256 tokenMask) external view returns (bool) {
        return (enabledTokensMask & tokenMask) != 0;
    }

    function countEnabledTokens() external view returns (uint256) {
        return enabledTokensMask.calcEnabledTokens();
    }
}

Iterating Over Enabled Tokens

function processAllEnabledTokens(
    uint256 enabledMask,
    address[] memory allTokens
) internal {
    uint256 mask = enabledMask;
    uint256 index = 0;

    while (mask != 0) {
        // Get the lowest set bit
        uint256 currentTokenMask = mask.lsbMask();

        // Find token index (log2 of mask)
        uint256 tokenIndex = 0;
        uint256 temp = currentTokenMask;
        while (temp > 1) {
            temp >>= 1;
            tokenIndex++;
        }

        // Process token
        address token = allTokens[tokenIndex];
        // ... do something with token

        // Remove processed bit
        mask = mask.disable(currentTokenMask);
    }
}

Batch Operations

function updateTokens(
    uint256 currentMask,
    uint256 tokensToAdd,
    uint256 tokensToRemove
) external pure returns (uint256 newMask) {
    // Atomically add and remove tokens
    newMask = currentMask.enableDisable(tokensToAdd, tokensToRemove);

    // Ensure we don't exceed maximum tokens
    require(
        newMask.calcEnabledTokens() <= 20,
        "Too many enabled tokens"
    );
}

Checking Multiple Tokens

function hasAnyToken(
    uint256 accountMask,
    uint256 requiredTokensMask
) external pure returns (bool) {
    // Check if account has ANY of the required tokens
    return (accountMask & requiredTokensMask) != 0;
}

function hasAllTokens(
    uint256 accountMask,
    uint256 requiredTokensMask
) external pure returns (bool) {
    // Check if account has ALL of the required tokens
    return (accountMask & requiredTokensMask) == requiredTokensMask;
}

function hasForbiddenToken(
    uint256 accountMask,
    uint256 forbiddenTokensMask
) external pure returns (bool) {
    // Check if account has any forbidden token
    return (accountMask & forbiddenTokensMask) != 0;
}

Bitmask Optimization Benefits

Gas Efficiency

Array Approach (expensive):
address[] enabledTokens;  // Storage array

// Check if token is enabled: O(n) loop
for (uint i = 0; i < enabledTokens.length; i++) {
    if (enabledTokens[i] == token) return true;
}
Bitmask Approach (cheap):
uint256 enabledTokensMask;  // Single storage slot

// Check if token is enabled: O(1) operation
return (enabledTokensMask & tokenMask) != 0;
Gas Savings:
  • Checking token enabled: ~2,100 gas (array) vs ~100 gas (bitmask)
  • Adding token: ~20,000 gas (array) vs ~5,000 gas (bitmask)
  • Storage: 1 slot (bitmask) vs N slots (array)

Set Operations

Bitmasks enable efficient set operations:
// Union: tokens in A OR B
uint256 union = maskA | maskB;

// Intersection: tokens in A AND B
uint256 intersection = maskA & maskB;

// Difference: tokens in A but not in B
uint256 difference = maskA & ~maskB;

// Symmetric difference: tokens in A XOR B
uint256 symDiff = maskA ^ maskB;

Limitations

Maximum Items: A uint256 has 256 bits, so you can track at most 256 different items (tokens) in a single mask. For most use cases, this is sufficient.

Practical Example: Credit Account

struct CreditAccount {
    address owner;
    uint256 debt;
    uint256 enabledTokensMask;  // Tracks all tokens held
}

// Enable tokens when received
function addCollateral(
    CreditAccount storage account,
    address token,
    uint256 tokenMask
) internal {
    // Transfer token to account
    // ...

    // Mark token as enabled
    account.enabledTokensMask = account.enabledTokensMask.enable(tokenMask);
}

// Disable tokens when balance reaches zero
function removeCollateral(
    CreditAccount storage account,
    uint256 tokenMask
) internal {
    account.enabledTokensMask = account.enabledTokensMask.disable(tokenMask);
}

// Check if account holds any forbidden tokens
function hasForbiddenTokens(
    CreditAccount storage account,
    uint256 forbiddenMask
) internal view returns (bool) {
    return (account.enabledTokensMask & forbiddenMask) != 0;
}

Build docs developers (and LLMs) love