Bitmap & Masks
Last updated
Last updated
A bitmap refers to a data structure that uses a sequence of bits to represent the status or presence of certain elements or flags. It is commonly used to efficiently store and manipulate binary data, such as flags, permissions, or states.
In Solidity, a bitmap is typically represented using an unsigned integer (uint256), where each bit within the integer represents a specific element or flag. Each bit can be either set (1) or unset (0), indicating the presence or absence of a particular attribute or state.
By using bitwise operations, such as bitwise AND (&), bitwise OR (|), bitwise XOR (^), and bitwise negation (~), it is possible to manipulate individual bits within the bitmap efficiently. These operations allow for checking and setting specific bits, querying the status of flags, and performing other bitwise operations.
For a uint256 bitmap, imagine that there are 256 light bulbs in total. Each light bulb representing 1 bit. Each switch can either be off (set to 0
) or on (set to 1
).
each bulb represents a state
e.g. is an asset active
if the bulb is set to 1
-> active
Using bitmaps can provide several benefits in terms of storage efficiency and gas efficiency.
Compact Storage: Bitmaps allow for efficient storage of multiple boolean or binary attributes within a single variable. Instead of using separate boolean variables or larger data types, a bitmap can represent multiple flags or states using a minimal amount of storage.
Gas Efficiency: Working with bitmaps can lead to gas savings in contract execution. Since bitwise operations operate on the binary representation of the data, they are generally less costly in terms of gas consumption compared to other operations like conditional statements or looping through individual flags.
Aave uses two types of bitmaps:
ReserveConfiguration: to track information related to each asset within Aave
UserConfiguration: to track which assets a user is supplying as collateral and borrowing
From Aave-v2-whitepaper:
In the initial V1 release, the protocol loops through all the active assets to identify the user deposits and loans. This resulted in high gas consumption and reduced scalability - as the cost of withdrawing/borrowing/repaying/liquidating assets would increase as more assets are listed on the protocol.
UserConfiguration
The bitmask has a 256 bit size, it is divided in pairs of bits, one for each asset. The first bit of the pair indicates if an asset is used as collateral by the user, the second whether an asset is borrowed by the user.
Advantages
If you need to traverse the list of assets for a user, to check what is being borrowed, just need to sload the bitmap to memory once; then read from memory.
Able to do complex queries cheaply and efficiently.
Especially useful for checking isolation and e-mode -> if certain assets are being used by the user.
Disadvantages:
Only 128 assets can be supported, to add more, another uint256 needs to be used.
For the calculation of the account data, the protocol would still need to query all listed assets.
An 128 asset limit is acceptable. Considering the case where a user is supplying a huge range of assets; it would be gas intensive to traverse the entire list to calculate his position health.
A bitmask has also been introduced to store the reserve configuration, defined in figure 5. A similar packing could have been achieved by using uint32 and booleans, the bitmask benefits from more gas efficiency, and more so when updating multiple configurations at once.
If there is a need to store more data, this bitmask can be extended by adding another uint256 into the struct.
The bitmap is held within a struct that contains a single uint256 variable.
&
: ANDThe bitwise AND operator ( &
) compares each bit of the first operand to the corresponding bit of the second operand.
If both bits are 1, the corresponding result bit is set to 1.
Else, the result bit is set to 0.
~
: NOTThe bitwise NOT operator ( ~
) inverts the bits of the binary sequence.
input bit 1 -> 0
input bit 0 -> 1
This is also known as One's compliment
<<
and >>
: Left shift and right shiftThe bitwise shift operators move the bit values of a binary object, by the defined number of bits.
<<
)When shifting left, the most-significant bit is lost, and a 0
bit is inserted on the other end. Essentially, all the bits take a step to the left, with the MSB falling off; right-padded with 0
.
A single left shift multiplies a binary number by 2:
When shifting right, the least-significant bit is lost and a 0
is inserted on the other end. Essentially, all the bits take a step to the right, with the LSB falling off; left-padded with 0
.
For positive numbers, a single logical right shift divides a number by 2, throwing out any remainders.
Aave utilizes 2 bitmaps, ReserveConfigurationMap and UserConfigurationMap, both defined within the library DataTypes
.
Let's examine how reserve factor (bit 64-79) can be retrieved from this bitmap.
The mask is a 64 digit hexadecimal number, representing 256 bits.
each hexadecimal digit represents 4 bits => 64 * 4 = 256 bits
first 64 digits are set to 1
, followed by 16 bits set to 0
, then 176 bits set to 1
remember, binary is read from right to left. The rightmost place number being zero.
And that is how the reserve factor is extracted from the bitmap as a uint256 value.
consider isUsingAsCollateralOrBorrowing