From 65b9638007096b6408fba9e46216c84f77eef36f Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 14:31:53 -0600 Subject: [PATCH 01/10] Add PairMap, NibbleMap and UintXMap to Bitmaps --- contracts/utils/structs/BitMaps.sol | 198 ++++++++++- scripts/generate/run.js | 1 + scripts/generate/templates/BitMaps.js | 151 +++++++++ scripts/generate/templates/BitMaps.opts.js | 8 + test/utils/structs/BitMap.test.js | 370 +++++++++++++++------ 5 files changed, 618 insertions(+), 110 deletions(-) create mode 100644 scripts/generate/templates/BitMaps.js create mode 100644 scripts/generate/templates/BitMaps.opts.js diff --git a/contracts/utils/structs/BitMaps.sol b/contracts/utils/structs/BitMaps.sol index 40cceb90bd4..7064bcd9cee 100644 --- a/contracts/utils/structs/BitMaps.sol +++ b/contracts/utils/structs/BitMaps.sol @@ -1,19 +1,30 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/BitMaps.sol) +// This file was procedurally generated from scripts/generate/templates/BitMaps.js. + pragma solidity ^0.8.20; +import {Panic} from "../Panic.sol"; + /** - * @dev Library for managing uint256 to bool mapping in a compact and efficient way, provided the keys are sequential. + * @dev Library for managing bytes-based mappings in a compact and efficient way, provided the keys are sequential. * Largely inspired by Uniswap's https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol[merkle-distributor]. * - * BitMaps pack 256 booleans across each bit of a single 256-bit slot of `uint256` type. - * Hence booleans corresponding to 256 _sequential_ indices would only consume a single slot, - * unlike the regular `bool` which would consume an entire slot for a single value. + * The library provides several map types that pack multiple values into single 256-bit storage slots: + * + * * `BitMap`: 256 booleans per slot (1 bit each) + * * `PairMap`: 128 2-bit values per slot + * * `NibbleMap`: 64 4-bit values per slot + * * `Uint8Map`: 32 8-bit values per slot + * * `Uint16Map`: 16 16-bit values per slot + * * `Uint32Map`: 8 32-bit values per slot + * * `Uint64Map`: 4 64-bit values per slot + * * `Uint128Map`: 2 128-bit values per slot * - * This results in gas savings in two ways: + * This approach provides significant gas savings compared to using individual storage slots for each value: * - * - Setting a zero value to non-zero only once every 256 times - * - Accessing the same warm slot for every 256 _sequential_ indices + * * Setting a zero value to non-zero only once every N times (where N is the packing density) + * * Accessing the same warm slot for every N _sequential_ indices */ library BitMaps { struct BitMap { @@ -57,4 +68,177 @@ library BitMaps { uint256 mask = 1 << (index & 0xff); bitmap._data[bucket] &= ~mask; } + + struct PairMap { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 2-bit value at `index` in `pairMap`. + */ + function get(PairMap storage pairMap, uint256 index) internal view returns (uint8) { + uint256 bucket = index >> 7; // 128 values per bucket (256/2) + uint256 shift = (index & 0x7f) << 1; // i.e. (index % 128) * 2 = position * 2 + return uint8((pairMap._data[bucket] >> shift) & 0x03); + } + + /** + * @dev Sets the 2-bit value at `index` in `pairMap`. + * Values larger than 3 are truncated to 2 bits (e.g., 4 becomes 0). + */ + function set(PairMap storage pairMap, uint256 index, uint8 value) internal { + uint256 bucket = index >> 7; // 128 values per bucket (256/2) + uint256 shift = (index & 0x7f) << 1; // i.e. (index % 128) * 2 = position * 2 + uint256 mask = 0x03 << shift; + pairMap._data[bucket] = (pairMap._data[bucket] & ~mask) | (uint256(value) << shift); // set the 2 bits + } + + struct NibbleMap { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 4-bit value at `index` in `nibbleMap`. + */ + function get(NibbleMap storage nibbleMap, uint256 index) internal view returns (uint8) { + uint256 bucket = index >> 6; // 64 values per bucket (256/4) + uint256 shift = (index & 0x3f) << 2; // i.e. (index % 64) * 4 = position * 4 + return uint8((nibbleMap._data[bucket] >> shift) & 0x0f); + } + + /** + * @dev Sets the 4-bit value at `index` in `nibbleMap`. + * Values larger than 15 are truncated to 4 bits (e.g., 16 becomes 0). + */ + function set(NibbleMap storage nibbleMap, uint256 index, uint8 value) internal { + uint256 bucket = index >> 6; // 64 values per bucket (256/4) + uint256 shift = (index & 0x3f) << 2; // i.e. (index % 64) * 4 = position * 4 + uint256 mask = 0x0f << shift; + nibbleMap._data[bucket] = (nibbleMap._data[bucket] & ~mask) | (uint256(value) << shift); // set the 4 bits + } + + struct Uint8Map { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 8-bit value at `index` in `map`. + */ + function get(Uint8Map storage map, uint256 index) internal view returns (uint8) { + uint256 bucket = index >> 5; // 32 values per bucket (256/8) + uint256 shift = (index & 0x1f) << 3; // i.e. (index % 32) * 8 = position * 8 + return uint8(map._data[bucket] >> shift); + } + + /** + * @dev Sets the 8-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 8 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(Uint8Map storage map, uint256 index, uint8 value) internal { + uint256 bucket = index >> 5; // 32 values per bucket (256/8) + uint256 shift = (index & 0x1f) << 3; // i.e. (index % 32) * 8 = position * 8 + uint256 mask = 0xff << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 8 bits + } + + struct Uint16Map { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 16-bit value at `index` in `map`. + */ + function get(Uint16Map storage map, uint256 index) internal view returns (uint16) { + uint256 bucket = index >> 4; // 16 values per bucket (256/16) + uint256 shift = (index & 0x0f) << 4; // i.e. (index % 16) * 16 = position * 16 + return uint16(map._data[bucket] >> shift); + } + + /** + * @dev Sets the 16-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 16 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(Uint16Map storage map, uint256 index, uint16 value) internal { + uint256 bucket = index >> 4; // 16 values per bucket (256/16) + uint256 shift = (index & 0x0f) << 4; // i.e. (index % 16) * 16 = position * 16 + uint256 mask = 0xffff << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 16 bits + } + + struct Uint32Map { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 32-bit value at `index` in `map`. + */ + function get(Uint32Map storage map, uint256 index) internal view returns (uint32) { + uint256 bucket = index >> 3; // 8 values per bucket (256/32) + uint256 shift = (index & 0x07) << 5; // i.e. (index % 8) * 32 = position * 32 + return uint32(map._data[bucket] >> shift); + } + + /** + * @dev Sets the 32-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 32 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(Uint32Map storage map, uint256 index, uint32 value) internal { + uint256 bucket = index >> 3; // 8 values per bucket (256/32) + uint256 shift = (index & 0x07) << 5; // i.e. (index % 8) * 32 = position * 32 + uint256 mask = 0xffffffff << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 32 bits + } + + struct Uint64Map { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 64-bit value at `index` in `map`. + */ + function get(Uint64Map storage map, uint256 index) internal view returns (uint64) { + uint256 bucket = index >> 2; // 4 values per bucket (256/64) + uint256 shift = (index & 0x03) << 6; // i.e. (index % 4) * 64 = position * 64 + return uint64(map._data[bucket] >> shift); + } + + /** + * @dev Sets the 64-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 64 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(Uint64Map storage map, uint256 index, uint64 value) internal { + uint256 bucket = index >> 2; // 4 values per bucket (256/64) + uint256 shift = (index & 0x03) << 6; // i.e. (index % 4) * 64 = position * 64 + uint256 mask = 0xffffffffffffffff << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 64 bits + } + + struct Uint128Map { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the 128-bit value at `index` in `map`. + */ + function get(Uint128Map storage map, uint256 index) internal view returns (uint128) { + uint256 bucket = index >> 1; // 2 values per bucket (256/128) + uint256 shift = (index & 0x01) << 7; // i.e. (index % 2) * 128 = position * 128 + return uint128(map._data[bucket] >> shift); + } + + /** + * @dev Sets the 128-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 128 bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(Uint128Map storage map, uint256 index, uint128 value) internal { + uint256 bucket = index >> 1; // 2 values per bucket (256/128) + uint256 shift = (index & 0x01) << 7; // i.e. (index % 2) * 128 = position * 128 + uint256 mask = 0xffffffffffffffffffffffffffffffff << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 128 bits + } } diff --git a/scripts/generate/run.js b/scripts/generate/run.js index 6779c93f44b..dc40eeb223f 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -37,6 +37,7 @@ for (const [file, template] of Object.entries({ 'utils/structs/Checkpoints.sol': './templates/Checkpoints.js', 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', + 'utils/structs/BitMaps.sol': './templates/BitMaps.js', 'utils/SlotDerivation.sol': './templates/SlotDerivation.js', 'utils/StorageSlot.sol': './templates/StorageSlot.js', 'utils/TransientSlot.sol': './templates/TransientSlot.js', diff --git a/scripts/generate/templates/BitMaps.js b/scripts/generate/templates/BitMaps.js new file mode 100644 index 00000000000..5a8cab17b8a --- /dev/null +++ b/scripts/generate/templates/BitMaps.js @@ -0,0 +1,151 @@ +const { ethers } = require('ethers'); +const { capitalize } = require('../../helpers'); +const format = require('../format-lines'); +const { SUBBYTE_TYPES, BYTEMAP_TYPES } = require('./BitMaps.opts'); + +const typeList = [ + ' * * `BitMap`: 256 booleans per slot (1 bit each)', + ...SUBBYTE_TYPES.map(({ bits, name }) => ` * * \`${capitalize(name)}\`: ${256n / bits} ${bits}-bit values per slot`), + ...BYTEMAP_TYPES.map(({ bits }) => ` * * \`Uint${bits}Map\`: ${256n / bits} ${bits}-bit values per slot`), +].join('\n'); + +const header = `\ +pragma solidity ^0.8.20; + +import {Panic} from "../Panic.sol"; + +/** + * @dev Library for managing bytes-based mappings in a compact and efficient way, provided the keys are sequential. + * Largely inspired by Uniswap's https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol[merkle-distributor]. + * + * The library provides several map types that pack multiple values into single 256-bit storage slots: + * +${typeList} + * + * This approach provides significant gas savings compared to using individual storage slots for each value: + * + * * Setting a zero value to non-zero only once every N times (where N is the packing density) + * * Accessing the same warm slot for every N _sequential_ indices + */ +library BitMaps { +`; + +const bitmapTemplate = `\ + struct BitMap { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns whether the bit at \`index\` is set. + */ + function get(BitMap storage bitmap, uint256 index) internal view returns (bool) { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + return bitmap._data[bucket] & mask != 0; + } + + /** + * @dev Sets the bit at \`index\` to the boolean \`value\`. + */ + function setTo(BitMap storage bitmap, uint256 index, bool value) internal { + if (value) { + set(bitmap, index); + } else { + unset(bitmap, index); + } + } + + /** + * @dev Sets the bit at \`index\`. + */ + function set(BitMap storage bitmap, uint256 index) internal { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + bitmap._data[bucket] |= mask; + } + + /** + * @dev Unsets the bit at \`index\`. + */ + function unset(BitMap storage bitmap, uint256 index) internal { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + bitmap._data[bucket] &= ~mask; + }`; + +const subByteTemplates = opts => { + const maxMask = ethers.toBeHex((1n << opts.bits) - 1n); + const valueShift = Math.log2(Number(opts.bits)); + const bucketShift = Math.log2(Number(256n / opts.bits)); + const valueMask = ethers.toBeHex(256n / opts.bits - 1n); + + return `\ + + struct ${capitalize(opts.name)} { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the ${opts.bits}-bit value at \`index\` in \`${opts.name}\`. + */ + function get(${capitalize(opts.name)} storage ${opts.name}, uint256 index) internal view returns (uint8) { + uint256 bucket = index >> ${bucketShift}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) + uint256 shift = (index & ${valueMask}) << ${valueShift}; // i.e. (index % ${Number(valueMask) + 1}) * ${opts.bits} = position * ${opts.bits} + return uint8((${opts.name}._data[bucket] >> shift) & ${maxMask}); + } + + /** + * @dev Sets the ${opts.bits}-bit value at \`index\` in \`${opts.name}\`. + * Values larger than ${(1n << opts.bits) - 1n} are truncated to ${opts.bits} bits (e.g., ${1n << opts.bits} becomes 0). + */ + function set(${capitalize(opts.name)} storage ${opts.name}, uint256 index, uint8 value) internal { + uint256 bucket = index >> ${bucketShift}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) + uint256 shift = (index & ${valueMask}) << ${valueShift}; // i.e. (index % ${Number(valueMask) + 1}) * ${opts.bits} = position * ${opts.bits} + uint256 mask = ${maxMask} << shift; + ${opts.name}._data[bucket] = (${opts.name}._data[bucket] & ~mask) | (uint256(value) << shift); // set the ${opts.bits} bits + }`; +}; + +const byteTemplates = opts => { + const maxMask = ethers.toBeHex((1n << opts.bits) - 1n); + const valueShift = Math.log2(Number(opts.bits)); + const bucketShift = Math.log2(Number(256n / opts.bits)); + const valueMask = ethers.toBeHex(256n / opts.bits - 1n); + const name = `uint${opts.bits}Map`; + + return `\ + + struct ${capitalize(name)} { + mapping(uint256 bucket => uint256) _data; + } + + /** + * @dev Returns the ${opts.bits}-bit value at \`index\` in \`map\`. + */ + function get(${capitalize(name)} storage map, uint256 index) internal view returns (uint${opts.bits}) { + uint256 bucket = index >> ${bucketShift}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) + uint256 shift = (index & ${valueMask}) << ${valueShift}; // i.e. (index % ${Number(valueMask) + 1}) * ${opts.bits} = position * ${opts.bits} + return uint${opts.bits}(map._data[bucket] >> shift); + } + + /** + * @dev Sets the ${opts.bits}-bit value at \`index\` in \`map\`. + * + * NOTE: Assumes \`value\` fits in ${opts.bits} bits. Assembly-manipulated values may corrupt adjacent data. + */ + function set(${capitalize(name)} storage map, uint256 index, uint${opts.bits} value) internal { + uint256 bucket = index >> ${bucketShift}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) + uint256 shift = (index & ${valueMask}) << ${valueShift}; // i.e. (index % ${Number(valueMask) + 1}) * ${opts.bits} = position * ${opts.bits} + uint256 mask = ${maxMask} << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the ${opts.bits} bits + }`; +}; + +// GENERATE +module.exports = format( + header.trimEnd(), + bitmapTemplate, + ...SUBBYTE_TYPES.map(subByteTemplates), + ...BYTEMAP_TYPES.map(byteTemplates), + '}', +); diff --git a/scripts/generate/templates/BitMaps.opts.js b/scripts/generate/templates/BitMaps.opts.js new file mode 100644 index 00000000000..e799b8a788a --- /dev/null +++ b/scripts/generate/templates/BitMaps.opts.js @@ -0,0 +1,8 @@ +const SUBBYTE_TYPES = [ + { bits: 2n, name: 'pairMap' }, + { bits: 4n, name: 'nibbleMap' }, +]; + +const BYTEMAP_TYPES = [{ bits: 8n }, { bits: 16n }, { bits: 32n }, { bits: 64n }, { bits: 128n }]; + +module.exports = { BYTEMAP_TYPES, SUBBYTE_TYPES }; diff --git a/test/utils/structs/BitMap.test.js b/test/utils/structs/BitMap.test.js index 5662ab13f88..05bdca17cb4 100644 --- a/test/utils/structs/BitMap.test.js +++ b/test/utils/structs/BitMap.test.js @@ -7,143 +7,307 @@ async function fixture() { return { bitmap }; } -describe('BitMap', function () { - const keyA = 7891n; - const keyB = 451n; - const keyC = 9592328n; - +describe('BitMaps', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); - it('starts empty', async function () { - expect(await this.bitmap.$get(0, keyA)).to.be.false; - expect(await this.bitmap.$get(0, keyB)).to.be.false; - expect(await this.bitmap.$get(0, keyC)).to.be.false; - }); + describe('BitMap', function () { + const keyA = 7891n; + const keyB = 451n; + const keyC = 9592328n; - describe('setTo', function () { - it('set a key to true', async function () { - await this.bitmap.$setTo(0, keyA, true); - expect(await this.bitmap.$get(0, keyA)).to.be.true; - expect(await this.bitmap.$get(0, keyB)).to.be.false; - expect(await this.bitmap.$get(0, keyC)).to.be.false; - }); - - it('set a key to false', async function () { - await this.bitmap.$setTo(0, keyA, true); - await this.bitmap.$setTo(0, keyA, false); - expect(await this.bitmap.$get(0, keyA)).to.be.false; - expect(await this.bitmap.$get(0, keyB)).to.be.false; - expect(await this.bitmap.$get(0, keyC)).to.be.false; - }); - - it('set several consecutive keys', async function () { - await this.bitmap.$setTo(0, keyA + 0n, true); - await this.bitmap.$setTo(0, keyA + 1n, true); - await this.bitmap.$setTo(0, keyA + 2n, true); - await this.bitmap.$setTo(0, keyA + 3n, true); - await this.bitmap.$setTo(0, keyA + 4n, true); - await this.bitmap.$setTo(0, keyA + 2n, false); - await this.bitmap.$setTo(0, keyA + 4n, false); - expect(await this.bitmap.$get(0, keyA + 0n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 1n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 2n)).to.be.false; - expect(await this.bitmap.$get(0, keyA + 3n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 4n)).to.be.false; + it('starts empty', async function () { + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; + }); + + describe('setTo', function () { + it('set a key to true', async function () { + await this.bitmap.$setTo(0, keyA, true); + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.false; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.false; + }); + + it('set a key to false', async function () { + await this.bitmap.$setTo(0, keyA, true); + await this.bitmap.$setTo(0, keyA, false); + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.false; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.false; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.false; + }); + + it('set several consecutive keys', async function () { + await this.bitmap.$setTo(0, keyA + 0n, true); + await this.bitmap.$setTo(0, keyA + 1n, true); + await this.bitmap.$setTo(0, keyA + 2n, true); + await this.bitmap.$setTo(0, keyA + 3n, true); + await this.bitmap.$setTo(0, keyA + 4n, true); + await this.bitmap.$setTo(0, keyA + 2n, false); + await this.bitmap.$setTo(0, keyA + 4n, false); + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.be.false; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.be.false; + }); + }); + + describe('set', function () { + it('adds a key', async function () { + await this.bitmap.$set(0, keyA); + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.false; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.false; + }); + + it('adds several keys', async function () { + await this.bitmap.$set(0, keyA); + await this.bitmap.$set(0, keyB); + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.false; + }); + + it('adds several consecutive keys', async function () { + await this.bitmap.$set(0, keyA + 0n); + await this.bitmap.$set(0, keyA + 1n); + await this.bitmap.$set(0, keyA + 3n); + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.be.false; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.be.false; + }); + }); + + describe('unset', function () { + it('removes added keys', async function () { + await this.bitmap.$set(0, keyA); + await this.bitmap.$set(0, keyB); + await this.bitmap.$unset(0, keyA); + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.false; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.false; + }); + + it('removes consecutive added keys', async function () { + await this.bitmap.$set(0, keyA + 0n); + await this.bitmap.$set(0, keyA + 1n); + await this.bitmap.$set(0, keyA + 3n); + await this.bitmap.$unset(0, keyA + 1n); + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.be.false; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.be.false; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.be.false; + }); + + it('adds and removes multiple keys', async function () { + // [] + + await this.bitmap.$set(0, keyA); + await this.bitmap.$set(0, keyC); + + // [A, C] + + await this.bitmap.$unset(0, keyA); + await this.bitmap.$unset(0, keyB); + + // [C] + + await this.bitmap.$set(0, keyB); + + // [C, B] + + await this.bitmap.$set(0, keyA); + await this.bitmap.$unset(0, keyC); + + // [A, B] + + await this.bitmap.$set(0, keyA); + await this.bitmap.$set(0, keyB); + + // [A, B] + + await this.bitmap.$set(0, keyC); + await this.bitmap.$unset(0, keyA); + + // [B, C] + + await this.bitmap.$set(0, keyA); + await this.bitmap.$unset(0, keyB); + + // [A, C] + + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.true; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.false; + expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.true; + }); }); }); - describe('set', function () { - it('adds a key', async function () { - await this.bitmap.$set(0, keyA); - expect(await this.bitmap.$get(0, keyA)).to.be.true; - expect(await this.bitmap.$get(0, keyB)).to.be.false; - expect(await this.bitmap.$get(0, keyC)).to.be.false; + describe('PairMap', function () { + it('stores and retrieves 2-bit values', async function () { + await this.bitmap.$set_BitMaps_PairMap(1, 0n, 0); + await this.bitmap.$set_BitMaps_PairMap(1, 1n, 1); + await this.bitmap.$set_BitMaps_PairMap(1, 2n, 2); + await this.bitmap.$set_BitMaps_PairMap(1, 3n, 3); + + expect(await this.bitmap.$get_BitMaps_PairMap(1, 0n)).to.equal(0); + expect(await this.bitmap.$get_BitMaps_PairMap(1, 1n)).to.equal(1); + expect(await this.bitmap.$get_BitMaps_PairMap(1, 2n)).to.equal(2); + expect(await this.bitmap.$get_BitMaps_PairMap(1, 3n)).to.equal(3); }); - it('adds several keys', async function () { - await this.bitmap.$set(0, keyA); - await this.bitmap.$set(0, keyB); - expect(await this.bitmap.$get(0, keyA)).to.be.true; - expect(await this.bitmap.$get(0, keyB)).to.be.true; - expect(await this.bitmap.$get(0, keyC)).to.be.false; + it('truncates values larger than 3', async function () { + await this.bitmap.$set_BitMaps_PairMap(1, 0n, 4); // Should become 0 + await this.bitmap.$set_BitMaps_PairMap(1, 1n, 5); // Should become 1 + await this.bitmap.$set_BitMaps_PairMap(1, 2n, 6); // Should become 2 + await this.bitmap.$set_BitMaps_PairMap(1, 3n, 7); // Should become 3 + + expect(await this.bitmap.$get_BitMaps_PairMap(1, 0n)).to.equal(0); + expect(await this.bitmap.$get_BitMaps_PairMap(1, 1n)).to.equal(1); + expect(await this.bitmap.$get_BitMaps_PairMap(1, 2n)).to.equal(2); + expect(await this.bitmap.$get_BitMaps_PairMap(1, 3n)).to.equal(3); }); - it('adds several consecutive keys', async function () { - await this.bitmap.$set(0, keyA + 0n); - await this.bitmap.$set(0, keyA + 1n); - await this.bitmap.$set(0, keyA + 3n); - expect(await this.bitmap.$get(0, keyA + 0n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 1n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 2n)).to.be.false; - expect(await this.bitmap.$get(0, keyA + 3n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 4n)).to.be.false; + it('handles multiple buckets', async function () { + // Test across bucket boundary (128 values per bucket) + await this.bitmap.$set_BitMaps_PairMap(1, 127n, 2); + await this.bitmap.$set_BitMaps_PairMap(1, 128n, 3); + + expect(await this.bitmap.$get_BitMaps_PairMap(1, 127n)).to.equal(2); + expect(await this.bitmap.$get_BitMaps_PairMap(1, 128n)).to.equal(3); }); }); - describe('unset', function () { - it('removes added keys', async function () { - await this.bitmap.$set(0, keyA); - await this.bitmap.$set(0, keyB); - await this.bitmap.$unset(0, keyA); - expect(await this.bitmap.$get(0, keyA)).to.be.false; - expect(await this.bitmap.$get(0, keyB)).to.be.true; - expect(await this.bitmap.$get(0, keyC)).to.be.false; + describe('NibbleMap', function () { + it('stores and retrieves 4-bit values', async function () { + await this.bitmap.$set_BitMaps_NibbleMap(2, 0n, 0); + await this.bitmap.$set_BitMaps_NibbleMap(2, 1n, 5); + await this.bitmap.$set_BitMaps_NibbleMap(2, 2n, 10); + await this.bitmap.$set_BitMaps_NibbleMap(2, 3n, 15); + + expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 0n)).to.equal(0); + expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 1n)).to.equal(5); + expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 2n)).to.equal(10); + expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 3n)).to.equal(15); }); - it('removes consecutive added keys', async function () { - await this.bitmap.$set(0, keyA + 0n); - await this.bitmap.$set(0, keyA + 1n); - await this.bitmap.$set(0, keyA + 3n); - await this.bitmap.$unset(0, keyA + 1n); - expect(await this.bitmap.$get(0, keyA + 0n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 1n)).to.be.false; - expect(await this.bitmap.$get(0, keyA + 2n)).to.be.false; - expect(await this.bitmap.$get(0, keyA + 3n)).to.be.true; - expect(await this.bitmap.$get(0, keyA + 4n)).to.be.false; + it('truncates values larger than 15', async function () { + await this.bitmap.$set_BitMaps_NibbleMap(2, 0n, 16); // Should become 0 + await this.bitmap.$set_BitMaps_NibbleMap(2, 1n, 17); // Should become 1 + await this.bitmap.$set_BitMaps_NibbleMap(2, 2n, 30); // Should become 14 + await this.bitmap.$set_BitMaps_NibbleMap(2, 3n, 31); // Should become 15 + + expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 0n)).to.equal(0); + expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 1n)).to.equal(1); + expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 2n)).to.equal(14); + expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 3n)).to.equal(15); }); + }); - it('adds and removes multiple keys', async function () { - // [] + describe('Uint8Map', function () { + it('stores and retrieves uint8 values', async function () { + await this.bitmap.$set_BitMaps_Uint8Map(3, 0n, ethers.Typed.uint8(0)); + await this.bitmap.$set_BitMaps_Uint8Map(3, 1n, ethers.Typed.uint8(42)); + await this.bitmap.$set_BitMaps_Uint8Map(3, 2n, ethers.Typed.uint8(255)); - await this.bitmap.$set(0, keyA); - await this.bitmap.$set(0, keyC); + expect(await this.bitmap.$get_BitMaps_Uint8Map(3, 0n)).to.equal(0); + expect(await this.bitmap.$get_BitMaps_Uint8Map(3, 1n)).to.equal(42); + expect(await this.bitmap.$get_BitMaps_Uint8Map(3, 2n)).to.equal(255); + }); - // [A, C] + it('handles bucket boundaries', async function () { + // 32 values per bucket for Uint8Map + await this.bitmap.$set_BitMaps_Uint8Map(3, 31n, ethers.Typed.uint8(100)); + await this.bitmap.$set_BitMaps_Uint8Map(3, 32n, ethers.Typed.uint8(200)); - await this.bitmap.$unset(0, keyA); - await this.bitmap.$unset(0, keyB); + expect(await this.bitmap.$get_BitMaps_Uint8Map(3, 31n)).to.equal(100); + expect(await this.bitmap.$get_BitMaps_Uint8Map(3, 32n)).to.equal(200); + }); + }); - // [C] + describe('Uint16Map', function () { + it('stores and retrieves uint16 values', async function () { + await this.bitmap.$set(4, 0n, ethers.Typed.uint16(0)); + await this.bitmap.$set(4, 1n, ethers.Typed.uint16(1000)); + await this.bitmap.$set(4, 2n, ethers.Typed.uint16(65535)); - await this.bitmap.$set(0, keyB); + expect(await this.bitmap.$get_BitMaps_Uint16Map(4, 0n)).to.equal(0); + expect(await this.bitmap.$get_BitMaps_Uint16Map(4, 1n)).to.equal(1000); + expect(await this.bitmap.$get_BitMaps_Uint16Map(4, 2n)).to.equal(65535); + }); - // [C, B] + it('handles bucket boundaries', async function () { + // 16 values per bucket for Uint16Map + await this.bitmap.$set(4, 15n, ethers.Typed.uint16(100)); + await this.bitmap.$set(4, 16n, ethers.Typed.uint16(200)); - await this.bitmap.$set(0, keyA); - await this.bitmap.$unset(0, keyC); + expect(await this.bitmap.$get_BitMaps_Uint16Map(4, 15n)).to.equal(100); + expect(await this.bitmap.$get_BitMaps_Uint16Map(4, 16n)).to.equal(200); + }); + }); + + describe('Uint32Map', function () { + it('stores and retrieves uint32 values', async function () { + await this.bitmap.$set(5, 0n, ethers.Typed.uint32(0)); + await this.bitmap.$set(5, 1n, ethers.Typed.uint32(1000000)); + await this.bitmap.$set(5, 2n, ethers.Typed.uint32(4294967295)); // 2^32 - 1 + + expect(await this.bitmap.$get_BitMaps_Uint32Map(5, 0n)).to.equal(0); + expect(await this.bitmap.$get_BitMaps_Uint32Map(5, 1n)).to.equal(1000000); + expect(await this.bitmap.$get_BitMaps_Uint32Map(5, 2n)).to.equal(4294967295); + }); + + it('handles bucket boundaries', async function () { + // 32 values per bucket for Uint32Map + await this.bitmap.$set(5, 31n, ethers.Typed.uint32(100)); + await this.bitmap.$set(5, 32n, ethers.Typed.uint32(200)); - // [A, B] + expect(await this.bitmap.$get_BitMaps_Uint32Map(5, 31n)).to.equal(100); + expect(await this.bitmap.$get_BitMaps_Uint32Map(5, 32n)).to.equal(200); + }); + }); + + describe('Uint64Map', function () { + it('stores and retrieves uint64 values', async function () { + const maxUint64 = (1n << 64n) - 1n; - await this.bitmap.$set(0, keyA); - await this.bitmap.$set(0, keyB); + await this.bitmap.$set(6, 0n, ethers.Typed.uint64(0)); + await this.bitmap.$set(6, 1n, ethers.Typed.uint64(1000000000)); + await this.bitmap.$set(6, 2n, ethers.Typed.uint64(maxUint64)); - // [A, B] + expect(await this.bitmap.$get_BitMaps_Uint64Map(6, 0n)).to.equal(0); + expect(await this.bitmap.$get_BitMaps_Uint64Map(6, 1n)).to.equal(1000000000); + expect(await this.bitmap.$get_BitMaps_Uint64Map(6, 2n)).to.equal(maxUint64); + }); + }); - await this.bitmap.$set(0, keyC); - await this.bitmap.$unset(0, keyA); + describe('Uint128Map', function () { + it('stores and retrieves uint128 values', async function () { + const maxUint128 = (1n << 128n) - 1n; + const largeValue = 1n << 100n; - // [B, C] + await this.bitmap.$set(7, 0n, ethers.Typed.uint128(0)); + await this.bitmap.$set(7, 1n, ethers.Typed.uint128(largeValue)); + await this.bitmap.$set(7, 2n, ethers.Typed.uint128(maxUint128)); - await this.bitmap.$set(0, keyA); - await this.bitmap.$unset(0, keyB); + expect(await this.bitmap.$get_BitMaps_Uint128Map(7, 0n)).to.equal(0); + expect(await this.bitmap.$get_BitMaps_Uint128Map(7, 1n)).to.equal(largeValue); + expect(await this.bitmap.$get_BitMaps_Uint128Map(7, 2n)).to.equal(maxUint128); + }); - // [A, C] + it('handles bucket boundaries', async function () { + // Uint128Map has 2 values per bucket + await this.bitmap.$set(7, 31n, ethers.Typed.uint128(100)); + await this.bitmap.$set(7, 32n, ethers.Typed.uint128(200)); - expect(await this.bitmap.$get(0, keyA)).to.be.true; - expect(await this.bitmap.$get(0, keyB)).to.be.false; - expect(await this.bitmap.$get(0, keyC)).to.be.true; + expect(await this.bitmap.$get_BitMaps_Uint128Map(7, 31n)).to.equal(100); + expect(await this.bitmap.$get_BitMaps_Uint128Map(7, 32n)).to.equal(200); }); }); }); From 1f8a11fde6ff4dc5de76fde385343bd1bd05f868 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 14:34:23 -0600 Subject: [PATCH 02/10] Add changeset --- .changeset/long-flowers-itch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/long-flowers-itch.md diff --git a/.changeset/long-flowers-itch.md b/.changeset/long-flowers-itch.md new file mode 100644 index 00000000000..f89ea4e2471 --- /dev/null +++ b/.changeset/long-flowers-itch.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`BitMaps`: Add new map types `PairMap`, `NibbleMap`, `Uint8Map`, `Uint16Map`, `Uint32Map`, `Uint64Map`, and `Uint128Map` for gas-efficient storage of packed values with different bit widths. From 264a7e2ff2d8cb4afd9a390921b7a32d15c7e262 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 15:20:27 -0600 Subject: [PATCH 03/10] Add fuzz tests --- test/utils/structs/BitMaps.t.sol | 261 ++++++++++++++++++ .../{BitMap.test.js => BitMaps.test.js} | 0 2 files changed, 261 insertions(+) create mode 100644 test/utils/structs/BitMaps.t.sol rename test/utils/structs/{BitMap.test.js => BitMaps.test.js} (100%) diff --git a/test/utils/structs/BitMaps.t.sol b/test/utils/structs/BitMaps.t.sol new file mode 100644 index 00000000000..1315f0738b9 --- /dev/null +++ b/test/utils/structs/BitMaps.t.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {BitMaps} from "../../../contracts/utils/structs/BitMaps.sol"; + +contract BitMapsTest is Test { + using BitMaps for *; + + // Test state for different map types + BitMaps.BitMap[2] private _bitmaps; + BitMaps.PairMap[2] private _pairMaps; + BitMaps.NibbleMap[2] private _nibbleMaps; + BitMaps.Uint8Map[2] private _uint8Maps; + BitMaps.Uint16Map[2] private _uint16Maps; + BitMaps.Uint32Map[2] private _uint32Maps; + BitMaps.Uint64Map[2] private _uint64Maps; + BitMaps.Uint128Map[2] private _uint128Maps; + + // ========== BitMap Tests ========== + + function testBitMapSetAndGet(uint256 value) public { + assertFalse(_bitmaps[0].get(value)); // initial state + _bitmaps[0].set(value); + assertTrue(_bitmaps[0].get(value)); // after set + } + + function testBitMapSetToAndGet(uint256 value) public { + _bitmaps[0].setTo(value, true); + assertTrue(_bitmaps[0].get(value)); // after setTo true + _bitmaps[0].setTo(value, false); + assertFalse(_bitmaps[0].get(value)); // after setTo false + } + + function testBitMapIsolation(uint256 value1, uint256 value2) public { + vm.assume(value1 != value2); + + _bitmaps[0].set(value1); + _bitmaps[1].set(value2); + + assertTrue(_bitmaps[0].get(value1)); + assertTrue(_bitmaps[1].get(value2)); + assertFalse(_bitmaps[0].get(value2)); + assertFalse(_bitmaps[1].get(value1)); + } + + // ========== PairMap Tests ========== + + function testPairMapSetAndGet(uint256 index, uint8 value) public { + vm.assume(value <= 3); // PairMap only supports 2-bit values (0-3) + assertEq(_pairMaps[0].get(index), 0); // initial state + _pairMaps[0].set(index, value); + assertEq(_pairMaps[0].get(index), value); // after set + } + + function testPairMapTruncation(uint256 index, uint8 value) public { + vm.assume(value > 3); // Test values that need truncation + _pairMaps[0].set(index, value); + assertEq(_pairMaps[0].get(index), value & 3); // Should be truncated to 2 bits + } + + function testPairMapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { + vm.assume(index1 != index2); + vm.assume(value1 <= 3); + vm.assume(value2 <= 3); + + _pairMaps[0].set(index1, value1); + _pairMaps[1].set(index2, value2); + + assertEq(_pairMaps[0].get(index1), value1); + assertEq(_pairMaps[1].get(index2), value2); + assertEq(_pairMaps[0].get(index2), 0); + assertEq(_pairMaps[1].get(index1), 0); + } + + // ========== NibbleMap Tests ========== + + function testNibbleMapSetAndGet(uint256 index, uint8 value) public { + vm.assume(value <= 15); // NibbleMap only supports 4-bit values (0-15) + assertEq(_nibbleMaps[0].get(index), 0); // initial state + _nibbleMaps[0].set(index, value); + assertEq(_nibbleMaps[0].get(index), value); // after set + } + + function testNibbleMapTruncation(uint256 index, uint8 value) public { + vm.assume(value > 15); // Test values that need truncation + _nibbleMaps[0].set(index, value); + assertEq(_nibbleMaps[0].get(index), value & 15); // Should be truncated to 4 bits + } + + function testNibbleMapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { + vm.assume(index1 != index2); + vm.assume(value1 <= 15); + vm.assume(value2 <= 15); + + _nibbleMaps[0].set(index1, value1); + _nibbleMaps[1].set(index2, value2); + + assertEq(_nibbleMaps[0].get(index1), value1); + assertEq(_nibbleMaps[1].get(index2), value2); + assertEq(_nibbleMaps[0].get(index2), 0); + assertEq(_nibbleMaps[1].get(index1), 0); + } + + // ========== Uint8Map Tests ========== + + function testUint8MapSetAndGet(uint256 index, uint8 value) public { + assertEq(_uint8Maps[0].get(index), 0); // initial state + _uint8Maps[0].set(index, value); + assertEq(_uint8Maps[0].get(index), value); // after set + } + + function testUint8MapAssemblyCorruption(uint256 index, uint8 value) public { + uint256 corrupted = _corruptValue(value); + uint8 corruptedValue; + assembly { + corruptedValue := corrupted + } + _uint8Maps[0].set(index, corruptedValue); + assertEq(_uint8Maps[0].get(index), uint8(corrupted)); // Should match truncated value + } + + function testUint8MapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { + vm.assume(index1 != index2); + + _uint8Maps[0].set(index1, value1); + _uint8Maps[1].set(index2, value2); + + assertEq(_uint8Maps[0].get(index1), value1); + assertEq(_uint8Maps[1].get(index2), value2); + assertEq(_uint8Maps[0].get(index2), 0); + assertEq(_uint8Maps[1].get(index1), 0); + } + + // ========== Uint16Map Tests ========== + + function testUint16MapSetAndGet(uint256 index, uint16 value) public { + assertEq(_uint16Maps[0].get(index), 0); // initial state + _uint16Maps[0].set(index, value); + assertEq(_uint16Maps[0].get(index), value); // after set + } + + function testUint16MapAssemblyCorruption(uint256 index, uint16 value) public { + uint256 corrupted = _corruptValue(value); + uint16 corruptedValue; + assembly { + corruptedValue := corrupted + } + _uint16Maps[0].set(index, corruptedValue); + assertEq(_uint16Maps[0].get(index), uint16(corrupted)); // Should match truncated value + } + + function testUint16MapIsolation(uint256 index1, uint256 index2, uint16 value1, uint16 value2) public { + vm.assume(index1 != index2); + + _uint16Maps[0].set(index1, value1); + _uint16Maps[1].set(index2, value2); + + assertEq(_uint16Maps[0].get(index1), value1); + assertEq(_uint16Maps[1].get(index2), value2); + assertEq(_uint16Maps[0].get(index2), 0); + assertEq(_uint16Maps[1].get(index1), 0); + } + + // ========== Uint32Map Tests ========== + + function testUint32MapSetAndGet(uint256 index, uint32 value) public { + assertEq(_uint32Maps[0].get(index), 0); // initial state + _uint32Maps[0].set(index, value); + assertEq(_uint32Maps[0].get(index), value); // after set + } + + function testUint32MapAssemblyCorruption(uint256 index, uint32 value) public { + uint256 corrupted = _corruptValue(value); + uint32 corruptedValue; + assembly { + corruptedValue := corrupted + } + _uint32Maps[0].set(index, corruptedValue); + assertEq(_uint32Maps[0].get(index), uint32(corrupted)); // Should match truncated value + } + + function testUint32MapIsolation(uint256 index1, uint256 index2, uint32 value1, uint32 value2) public { + vm.assume(index1 != index2); + + _uint32Maps[0].set(index1, value1); + _uint32Maps[1].set(index2, value2); + + assertEq(_uint32Maps[0].get(index1), value1); + assertEq(_uint32Maps[1].get(index2), value2); + assertEq(_uint32Maps[0].get(index2), 0); + assertEq(_uint32Maps[1].get(index1), 0); + } + + // ========== Uint64Map Tests ========== + + function testUint64MapSetAndGet(uint256 index, uint64 value) public { + assertEq(_uint64Maps[0].get(index), 0); // initial state + _uint64Maps[0].set(index, value); + assertEq(_uint64Maps[0].get(index), value); // after set + } + + function testUint64MapAssemblyCorruption(uint256 index, uint64 value) public { + uint256 corrupted = _corruptValue(value); + uint64 corruptedValue; + assembly { + corruptedValue := corrupted + } + _uint64Maps[0].set(index, corruptedValue); + assertEq(_uint64Maps[0].get(index), uint64(corrupted)); // Should match truncated value + } + + function testUint64MapIsolation(uint256 index1, uint256 index2, uint64 value1, uint64 value2) public { + vm.assume(index1 != index2); + + _uint64Maps[0].set(index1, value1); + _uint64Maps[1].set(index2, value2); + + assertEq(_uint64Maps[0].get(index1), value1); + assertEq(_uint64Maps[1].get(index2), value2); + assertEq(_uint64Maps[0].get(index2), 0); + assertEq(_uint64Maps[1].get(index1), 0); + } + + // ========== Uint128Map Tests ========== + + function testUint128MapSetAndGet(uint256 index, uint128 value) public { + assertEq(_uint128Maps[0].get(index), 0); // initial state + _uint128Maps[0].set(index, value); + assertEq(_uint128Maps[0].get(index), value); // after set + } + + function testUint128MapAssemblyCorruption(uint256 index, uint128 value) public { + uint256 corrupted = _corruptValue(value); + uint128 corruptedValue; + assembly { + corruptedValue := corrupted + } + _uint128Maps[0].set(index, corruptedValue); + assertEq(_uint128Maps[0].get(index), uint128(corrupted)); // Should match truncated value + } + + function testUint128MapIsolation(uint256 index1, uint256 index2, uint128 value1, uint128 value2) public { + vm.assume(index1 != index2); + + _uint128Maps[0].set(index1, value1); + _uint128Maps[1].set(index2, value2); + + assertEq(_uint128Maps[0].get(index1), value1); + assertEq(_uint128Maps[1].get(index2), value2); + assertEq(_uint128Maps[0].get(index2), 0); + assertEq(_uint128Maps[1].get(index1), 0); + } + + function _corruptValue(uint256 value) private pure returns (uint256 corrupted) { + // Simulate assembly corruption by adding high bits + assembly { + corrupted := or(value, shl(200, 0xffffffff)) + } + } +} diff --git a/test/utils/structs/BitMap.test.js b/test/utils/structs/BitMaps.test.js similarity index 100% rename from test/utils/structs/BitMap.test.js rename to test/utils/structs/BitMaps.test.js From eccb96d894ad70aaa67a1b1d13672fc687330953 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 15:24:55 -0600 Subject: [PATCH 04/10] Tests nits --- test/utils/structs/BitMaps.test.js | 148 ++++++++++++++--------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/test/utils/structs/BitMaps.test.js b/test/utils/structs/BitMaps.test.js index 05bdca17cb4..88fdf35aa77 100644 --- a/test/utils/structs/BitMaps.test.js +++ b/test/utils/structs/BitMaps.test.js @@ -26,17 +26,17 @@ describe('BitMaps', function () { describe('setTo', function () { it('set a key to true', async function () { await this.bitmap.$setTo(0, keyA, true); - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.false; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; }); it('set a key to false', async function () { await this.bitmap.$setTo(0, keyA, true); await this.bitmap.$setTo(0, keyA, false); - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.false; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.false; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; }); it('set several consecutive keys', async function () { @@ -47,39 +47,39 @@ describe('BitMaps', function () { await this.bitmap.$setTo(0, keyA + 4n, true); await this.bitmap.$setTo(0, keyA + 2n, false); await this.bitmap.$setTo(0, keyA + 4n, false); - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.be.false; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.eventually.be.false; }); }); describe('set', function () { it('adds a key', async function () { await this.bitmap.$set(0, keyA); - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.false; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; }); it('adds several keys', async function () { await this.bitmap.$set(0, keyA); await this.bitmap.$set(0, keyB); - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; }); it('adds several consecutive keys', async function () { await this.bitmap.$set(0, keyA + 0n); await this.bitmap.$set(0, keyA + 1n); await this.bitmap.$set(0, keyA + 3n); - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.be.false; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.eventually.be.false; }); }); @@ -88,9 +88,9 @@ describe('BitMaps', function () { await this.bitmap.$set(0, keyA); await this.bitmap.$set(0, keyB); await this.bitmap.$unset(0, keyA); - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.false; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.false; }); it('removes consecutive added keys', async function () { @@ -98,11 +98,11 @@ describe('BitMaps', function () { await this.bitmap.$set(0, keyA + 1n); await this.bitmap.$set(0, keyA + 3n); await this.bitmap.$unset(0, keyA + 1n); - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.be.false; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.be.false; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 0n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 1n)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 2n)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 3n)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA + 4n)).to.eventually.be.false; }); it('adds and removes multiple keys', async function () { @@ -142,9 +142,9 @@ describe('BitMaps', function () { // [A, C] - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.be.true; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.be.false; - expect(await this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyA)).to.eventually.be.true; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyB)).to.eventually.be.false; + await expect(this.bitmap.$get_BitMaps_BitMap(0, keyC)).to.eventually.be.true; }); }); }); @@ -156,10 +156,10 @@ describe('BitMaps', function () { await this.bitmap.$set_BitMaps_PairMap(1, 2n, 2); await this.bitmap.$set_BitMaps_PairMap(1, 3n, 3); - expect(await this.bitmap.$get_BitMaps_PairMap(1, 0n)).to.equal(0); - expect(await this.bitmap.$get_BitMaps_PairMap(1, 1n)).to.equal(1); - expect(await this.bitmap.$get_BitMaps_PairMap(1, 2n)).to.equal(2); - expect(await this.bitmap.$get_BitMaps_PairMap(1, 3n)).to.equal(3); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 1n)).to.eventually.equal(1); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 2n)).to.eventually.equal(2); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 3n)).to.eventually.equal(3); }); it('truncates values larger than 3', async function () { @@ -168,10 +168,10 @@ describe('BitMaps', function () { await this.bitmap.$set_BitMaps_PairMap(1, 2n, 6); // Should become 2 await this.bitmap.$set_BitMaps_PairMap(1, 3n, 7); // Should become 3 - expect(await this.bitmap.$get_BitMaps_PairMap(1, 0n)).to.equal(0); - expect(await this.bitmap.$get_BitMaps_PairMap(1, 1n)).to.equal(1); - expect(await this.bitmap.$get_BitMaps_PairMap(1, 2n)).to.equal(2); - expect(await this.bitmap.$get_BitMaps_PairMap(1, 3n)).to.equal(3); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 1n)).to.eventually.equal(1); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 2n)).to.eventually.equal(2); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 3n)).to.eventually.equal(3); }); it('handles multiple buckets', async function () { @@ -179,8 +179,8 @@ describe('BitMaps', function () { await this.bitmap.$set_BitMaps_PairMap(1, 127n, 2); await this.bitmap.$set_BitMaps_PairMap(1, 128n, 3); - expect(await this.bitmap.$get_BitMaps_PairMap(1, 127n)).to.equal(2); - expect(await this.bitmap.$get_BitMaps_PairMap(1, 128n)).to.equal(3); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 127n)).to.eventually.equal(2); + await expect(this.bitmap.$get_BitMaps_PairMap(1, 128n)).to.eventually.equal(3); }); }); @@ -191,10 +191,10 @@ describe('BitMaps', function () { await this.bitmap.$set_BitMaps_NibbleMap(2, 2n, 10); await this.bitmap.$set_BitMaps_NibbleMap(2, 3n, 15); - expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 0n)).to.equal(0); - expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 1n)).to.equal(5); - expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 2n)).to.equal(10); - expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 3n)).to.equal(15); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 1n)).to.eventually.equal(5); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 2n)).to.eventually.equal(10); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 3n)).to.eventually.equal(15); }); it('truncates values larger than 15', async function () { @@ -203,10 +203,10 @@ describe('BitMaps', function () { await this.bitmap.$set_BitMaps_NibbleMap(2, 2n, 30); // Should become 14 await this.bitmap.$set_BitMaps_NibbleMap(2, 3n, 31); // Should become 15 - expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 0n)).to.equal(0); - expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 1n)).to.equal(1); - expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 2n)).to.equal(14); - expect(await this.bitmap.$get_BitMaps_NibbleMap(2, 3n)).to.equal(15); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 1n)).to.eventually.equal(1); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 2n)).to.eventually.equal(14); + await expect(this.bitmap.$get_BitMaps_NibbleMap(2, 3n)).to.eventually.equal(15); }); }); @@ -216,9 +216,9 @@ describe('BitMaps', function () { await this.bitmap.$set_BitMaps_Uint8Map(3, 1n, ethers.Typed.uint8(42)); await this.bitmap.$set_BitMaps_Uint8Map(3, 2n, ethers.Typed.uint8(255)); - expect(await this.bitmap.$get_BitMaps_Uint8Map(3, 0n)).to.equal(0); - expect(await this.bitmap.$get_BitMaps_Uint8Map(3, 1n)).to.equal(42); - expect(await this.bitmap.$get_BitMaps_Uint8Map(3, 2n)).to.equal(255); + await expect(this.bitmap.$get_BitMaps_Uint8Map(3, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_Uint8Map(3, 1n)).to.eventually.equal(42); + await expect(this.bitmap.$get_BitMaps_Uint8Map(3, 2n)).to.eventually.equal(255); }); it('handles bucket boundaries', async function () { @@ -226,8 +226,8 @@ describe('BitMaps', function () { await this.bitmap.$set_BitMaps_Uint8Map(3, 31n, ethers.Typed.uint8(100)); await this.bitmap.$set_BitMaps_Uint8Map(3, 32n, ethers.Typed.uint8(200)); - expect(await this.bitmap.$get_BitMaps_Uint8Map(3, 31n)).to.equal(100); - expect(await this.bitmap.$get_BitMaps_Uint8Map(3, 32n)).to.equal(200); + await expect(this.bitmap.$get_BitMaps_Uint8Map(3, 31n)).to.eventually.equal(100); + await expect(this.bitmap.$get_BitMaps_Uint8Map(3, 32n)).to.eventually.equal(200); }); }); @@ -237,9 +237,9 @@ describe('BitMaps', function () { await this.bitmap.$set(4, 1n, ethers.Typed.uint16(1000)); await this.bitmap.$set(4, 2n, ethers.Typed.uint16(65535)); - expect(await this.bitmap.$get_BitMaps_Uint16Map(4, 0n)).to.equal(0); - expect(await this.bitmap.$get_BitMaps_Uint16Map(4, 1n)).to.equal(1000); - expect(await this.bitmap.$get_BitMaps_Uint16Map(4, 2n)).to.equal(65535); + await expect(this.bitmap.$get_BitMaps_Uint16Map(4, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_Uint16Map(4, 1n)).to.eventually.equal(1000); + await expect(this.bitmap.$get_BitMaps_Uint16Map(4, 2n)).to.eventually.equal(65535); }); it('handles bucket boundaries', async function () { @@ -247,8 +247,8 @@ describe('BitMaps', function () { await this.bitmap.$set(4, 15n, ethers.Typed.uint16(100)); await this.bitmap.$set(4, 16n, ethers.Typed.uint16(200)); - expect(await this.bitmap.$get_BitMaps_Uint16Map(4, 15n)).to.equal(100); - expect(await this.bitmap.$get_BitMaps_Uint16Map(4, 16n)).to.equal(200); + await expect(this.bitmap.$get_BitMaps_Uint16Map(4, 15n)).to.eventually.equal(100); + await expect(this.bitmap.$get_BitMaps_Uint16Map(4, 16n)).to.eventually.equal(200); }); }); @@ -258,9 +258,9 @@ describe('BitMaps', function () { await this.bitmap.$set(5, 1n, ethers.Typed.uint32(1000000)); await this.bitmap.$set(5, 2n, ethers.Typed.uint32(4294967295)); // 2^32 - 1 - expect(await this.bitmap.$get_BitMaps_Uint32Map(5, 0n)).to.equal(0); - expect(await this.bitmap.$get_BitMaps_Uint32Map(5, 1n)).to.equal(1000000); - expect(await this.bitmap.$get_BitMaps_Uint32Map(5, 2n)).to.equal(4294967295); + await expect(this.bitmap.$get_BitMaps_Uint32Map(5, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_Uint32Map(5, 1n)).to.eventually.equal(1000000); + await expect(this.bitmap.$get_BitMaps_Uint32Map(5, 2n)).to.eventually.equal(4294967295); }); it('handles bucket boundaries', async function () { @@ -268,8 +268,8 @@ describe('BitMaps', function () { await this.bitmap.$set(5, 31n, ethers.Typed.uint32(100)); await this.bitmap.$set(5, 32n, ethers.Typed.uint32(200)); - expect(await this.bitmap.$get_BitMaps_Uint32Map(5, 31n)).to.equal(100); - expect(await this.bitmap.$get_BitMaps_Uint32Map(5, 32n)).to.equal(200); + await expect(this.bitmap.$get_BitMaps_Uint32Map(5, 31n)).to.eventually.equal(100); + await expect(this.bitmap.$get_BitMaps_Uint32Map(5, 32n)).to.eventually.equal(200); }); }); @@ -281,9 +281,9 @@ describe('BitMaps', function () { await this.bitmap.$set(6, 1n, ethers.Typed.uint64(1000000000)); await this.bitmap.$set(6, 2n, ethers.Typed.uint64(maxUint64)); - expect(await this.bitmap.$get_BitMaps_Uint64Map(6, 0n)).to.equal(0); - expect(await this.bitmap.$get_BitMaps_Uint64Map(6, 1n)).to.equal(1000000000); - expect(await this.bitmap.$get_BitMaps_Uint64Map(6, 2n)).to.equal(maxUint64); + await expect(this.bitmap.$get_BitMaps_Uint64Map(6, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_Uint64Map(6, 1n)).to.eventually.equal(1000000000); + await expect(this.bitmap.$get_BitMaps_Uint64Map(6, 2n)).to.eventually.equal(maxUint64); }); }); @@ -296,9 +296,9 @@ describe('BitMaps', function () { await this.bitmap.$set(7, 1n, ethers.Typed.uint128(largeValue)); await this.bitmap.$set(7, 2n, ethers.Typed.uint128(maxUint128)); - expect(await this.bitmap.$get_BitMaps_Uint128Map(7, 0n)).to.equal(0); - expect(await this.bitmap.$get_BitMaps_Uint128Map(7, 1n)).to.equal(largeValue); - expect(await this.bitmap.$get_BitMaps_Uint128Map(7, 2n)).to.equal(maxUint128); + await expect(this.bitmap.$get_BitMaps_Uint128Map(7, 0n)).to.eventually.equal(0); + await expect(this.bitmap.$get_BitMaps_Uint128Map(7, 1n)).to.eventually.equal(largeValue); + await expect(this.bitmap.$get_BitMaps_Uint128Map(7, 2n)).to.eventually.equal(maxUint128); }); it('handles bucket boundaries', async function () { @@ -306,8 +306,8 @@ describe('BitMaps', function () { await this.bitmap.$set(7, 31n, ethers.Typed.uint128(100)); await this.bitmap.$set(7, 32n, ethers.Typed.uint128(200)); - expect(await this.bitmap.$get_BitMaps_Uint128Map(7, 31n)).to.equal(100); - expect(await this.bitmap.$get_BitMaps_Uint128Map(7, 32n)).to.equal(200); + await expect(this.bitmap.$get_BitMaps_Uint128Map(7, 31n)).to.eventually.equal(100); + await expect(this.bitmap.$get_BitMaps_Uint128Map(7, 32n)).to.eventually.equal(200); }); }); }); From d51205cece8c23fd4507fad4938548a8d995ad99 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 15:34:32 -0600 Subject: [PATCH 05/10] Automatically generate tests --- scripts/generate/run.js | 1 + scripts/generate/templates/BitMaps.t.js | 142 ++++++++++++++++++++++++ test/utils/structs/BitMaps.t.sol | 28 +++-- 3 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 scripts/generate/templates/BitMaps.t.js diff --git a/scripts/generate/run.js b/scripts/generate/run.js index dc40eeb223f..1e0e23ca83e 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -52,6 +52,7 @@ for (const [file, template] of Object.entries({ // Tests for (const [file, template] of Object.entries({ 'utils/structs/Checkpoints.t.sol': './templates/Checkpoints.t.js', + 'utils/structs/BitMaps.t.sol': './templates/BitMaps.t.js', 'utils/Packing.t.sol': './templates/Packing.t.js', 'utils/SlotDerivation.t.sol': './templates/SlotDerivation.t.js', })) { diff --git a/scripts/generate/templates/BitMaps.t.js b/scripts/generate/templates/BitMaps.t.js new file mode 100644 index 00000000000..7cce3d07017 --- /dev/null +++ b/scripts/generate/templates/BitMaps.t.js @@ -0,0 +1,142 @@ +const { capitalize } = require('../../helpers'); +const format = require('../format-lines'); +const { SUBBYTE_TYPES, BYTEMAP_TYPES } = require('./BitMaps.opts'); + +const header = `\ +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {BitMaps} from "../../../contracts/utils/structs/BitMaps.sol"; + +contract BitMapsTest is Test { + using BitMaps for *; +`; + +const bitmapTests = `\ + + // ========== BitMap Tests ========== + + BitMaps.BitMap[2] private _bitmaps; + + function testBitMapSetAndGet(uint256 value) public { + assertFalse(_bitmaps[0].get(value)); // initial state + _bitmaps[0].set(value); + assertTrue(_bitmaps[0].get(value)); // after set + } + + function testBitMapSetToAndGet(uint256 value) public { + _bitmaps[0].setTo(value, true); + assertTrue(_bitmaps[0].get(value)); // after setTo true + _bitmaps[0].setTo(value, false); + assertFalse(_bitmaps[0].get(value)); // after setTo false + } + + function testBitMapIsolation(uint256 value1, uint256 value2) public { + vm.assume(value1 != value2); + + _bitmaps[0].set(value1); + _bitmaps[1].set(value2); + + assertTrue(_bitmaps[0].get(value1)); + assertTrue(_bitmaps[1].get(value2)); + assertFalse(_bitmaps[0].get(value2)); + assertFalse(_bitmaps[1].get(value1)); + }`; + +const subByteTestTemplate = opts => { + const maxValue = (1n << opts.bits) - 1n; + const capitalizedName = capitalize(opts.name); + const mapName = `_${opts.name}s`; + + return `\ + + // ========== ${capitalizedName} Tests ========== + + BitMaps.${capitalizedName}[2] private ${mapName}; + + function test${capitalizedName}SetAndGet(uint256 index, uint8 value) public { + vm.assume(value <= ${maxValue}); // ${capitalizedName} only supports ${opts.bits}-bit values (0-${maxValue}) + assertEq(${mapName}[0].get(index), 0); // initial state + ${mapName}[0].set(index, value); + assertEq(${mapName}[0].get(index), value); // after set + } + + function test${capitalizedName}Truncation(uint256 index, uint8 value) public { + vm.assume(value > ${maxValue}); // Test values that need truncation + ${mapName}[0].set(index, value); + assertEq(${mapName}[0].get(index), value & ${maxValue}); // Should be truncated to ${opts.bits} bits + } + + function test${capitalizedName}Isolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { + vm.assume(index1 != index2); + vm.assume(value1 <= ${maxValue}); + vm.assume(value2 <= ${maxValue}); + + ${mapName}[0].set(index1, value1); + ${mapName}[1].set(index2, value2); + + assertEq(${mapName}[0].get(index1), value1); + assertEq(${mapName}[1].get(index2), value2); + assertEq(${mapName}[0].get(index2), 0); + assertEq(${mapName}[1].get(index1), 0); + }`; +}; + +const byteTestTemplate = opts => { + const capitalizedName = `Uint${opts.bits}Map`; + const mapName = `_uint${opts.bits}Maps`; + const valueType = `uint${opts.bits}`; + + return `\ + + // ========== ${capitalizedName} Tests ========== + + BitMaps.${capitalizedName}[2] private ${mapName}; + + function test${capitalizedName}SetAndGet(uint256 index, ${valueType} value) public { + assertEq(${mapName}[0].get(index), 0); // initial state + ${mapName}[0].set(index, value); + assertEq(${mapName}[0].get(index), value); // after set + } + + function test${capitalizedName}AssemblyCorruption(uint256 index, ${valueType} value) public { + uint256 corrupted = _corruptValue(value); + ${valueType} corruptedValue; + assembly { + corruptedValue := corrupted + } + ${mapName}[0].set(index, corruptedValue); + assertEq(${mapName}[0].get(index), ${valueType}(corrupted)); // Should match truncated value + } + + function test${capitalizedName}Isolation(uint256 index1, uint256 index2, ${valueType} value1, ${valueType} value2) public { + vm.assume(index1 != index2); + + ${mapName}[0].set(index1, value1); + ${mapName}[1].set(index2, value2); + + assertEq(${mapName}[0].get(index1), value1); + assertEq(${mapName}[1].get(index2), value2); + assertEq(${mapName}[0].get(index2), 0); + assertEq(${mapName}[1].get(index1), 0); + }`; +}; + +const footer = `\ + + function _corruptValue(uint256 value) private pure returns (uint256 corrupted) { + // Simulate assembly corruption by adding high bits + assembly { + corrupted := or(value, shl(200, 0xffffffff)) + } + } +}`; + +// GENERATE +module.exports = format( + header.trimEnd(), + bitmapTests, + ...SUBBYTE_TYPES.map(subByteTestTemplate), + ...BYTEMAP_TYPES.map(byteTestTemplate), + footer.trimEnd(), +); diff --git a/test/utils/structs/BitMaps.t.sol b/test/utils/structs/BitMaps.t.sol index 1315f0738b9..552c927ae17 100644 --- a/test/utils/structs/BitMaps.t.sol +++ b/test/utils/structs/BitMaps.t.sol @@ -1,4 +1,6 @@ // SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/BitMaps.t.js. + pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; @@ -7,18 +9,10 @@ import {BitMaps} from "../../../contracts/utils/structs/BitMaps.sol"; contract BitMapsTest is Test { using BitMaps for *; - // Test state for different map types - BitMaps.BitMap[2] private _bitmaps; - BitMaps.PairMap[2] private _pairMaps; - BitMaps.NibbleMap[2] private _nibbleMaps; - BitMaps.Uint8Map[2] private _uint8Maps; - BitMaps.Uint16Map[2] private _uint16Maps; - BitMaps.Uint32Map[2] private _uint32Maps; - BitMaps.Uint64Map[2] private _uint64Maps; - BitMaps.Uint128Map[2] private _uint128Maps; - // ========== BitMap Tests ========== + BitMaps.BitMap[2] private _bitmaps; + function testBitMapSetAndGet(uint256 value) public { assertFalse(_bitmaps[0].get(value)); // initial state _bitmaps[0].set(value); @@ -46,6 +40,8 @@ contract BitMapsTest is Test { // ========== PairMap Tests ========== + BitMaps.PairMap[2] private _pairMaps; + function testPairMapSetAndGet(uint256 index, uint8 value) public { vm.assume(value <= 3); // PairMap only supports 2-bit values (0-3) assertEq(_pairMaps[0].get(index), 0); // initial state @@ -75,6 +71,8 @@ contract BitMapsTest is Test { // ========== NibbleMap Tests ========== + BitMaps.NibbleMap[2] private _nibbleMaps; + function testNibbleMapSetAndGet(uint256 index, uint8 value) public { vm.assume(value <= 15); // NibbleMap only supports 4-bit values (0-15) assertEq(_nibbleMaps[0].get(index), 0); // initial state @@ -104,6 +102,8 @@ contract BitMapsTest is Test { // ========== Uint8Map Tests ========== + BitMaps.Uint8Map[2] private _uint8Maps; + function testUint8MapSetAndGet(uint256 index, uint8 value) public { assertEq(_uint8Maps[0].get(index), 0); // initial state _uint8Maps[0].set(index, value); @@ -134,6 +134,8 @@ contract BitMapsTest is Test { // ========== Uint16Map Tests ========== + BitMaps.Uint16Map[2] private _uint16Maps; + function testUint16MapSetAndGet(uint256 index, uint16 value) public { assertEq(_uint16Maps[0].get(index), 0); // initial state _uint16Maps[0].set(index, value); @@ -164,6 +166,8 @@ contract BitMapsTest is Test { // ========== Uint32Map Tests ========== + BitMaps.Uint32Map[2] private _uint32Maps; + function testUint32MapSetAndGet(uint256 index, uint32 value) public { assertEq(_uint32Maps[0].get(index), 0); // initial state _uint32Maps[0].set(index, value); @@ -194,6 +198,8 @@ contract BitMapsTest is Test { // ========== Uint64Map Tests ========== + BitMaps.Uint64Map[2] private _uint64Maps; + function testUint64MapSetAndGet(uint256 index, uint64 value) public { assertEq(_uint64Maps[0].get(index), 0); // initial state _uint64Maps[0].set(index, value); @@ -224,6 +230,8 @@ contract BitMapsTest is Test { // ========== Uint128Map Tests ========== + BitMaps.Uint128Map[2] private _uint128Maps; + function testUint128MapSetAndGet(uint256 index, uint128 value) public { assertEq(_uint128Maps[0].get(index), 0); // initial state _uint128Maps[0].set(index, value); From 912c79e3122dc97a3bc55fe2db63e8e6005b9fb1 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 15:39:18 -0600 Subject: [PATCH 06/10] Formally verify --- scripts/generate/templates/BitMaps.t.js | 18 +++++----- test/utils/structs/BitMaps.t.sol | 48 ++++++++++++------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/scripts/generate/templates/BitMaps.t.js b/scripts/generate/templates/BitMaps.t.js index 7cce3d07017..bcbaf1054c9 100644 --- a/scripts/generate/templates/BitMaps.t.js +++ b/scripts/generate/templates/BitMaps.t.js @@ -18,20 +18,20 @@ const bitmapTests = `\ BitMaps.BitMap[2] private _bitmaps; - function testBitMapSetAndGet(uint256 value) public { + function testSymbolicBitMapSetAndGet(uint256 value) public { assertFalse(_bitmaps[0].get(value)); // initial state _bitmaps[0].set(value); assertTrue(_bitmaps[0].get(value)); // after set } - function testBitMapSetToAndGet(uint256 value) public { + function testSymbolicBitMapSetToAndGet(uint256 value) public { _bitmaps[0].setTo(value, true); assertTrue(_bitmaps[0].get(value)); // after setTo true _bitmaps[0].setTo(value, false); assertFalse(_bitmaps[0].get(value)); // after setTo false } - function testBitMapIsolation(uint256 value1, uint256 value2) public { + function testSymbolicBitMapIsolation(uint256 value1, uint256 value2) public { vm.assume(value1 != value2); _bitmaps[0].set(value1); @@ -54,20 +54,20 @@ const subByteTestTemplate = opts => { BitMaps.${capitalizedName}[2] private ${mapName}; - function test${capitalizedName}SetAndGet(uint256 index, uint8 value) public { + function testSymbolic${capitalizedName}SetAndGet(uint256 index, uint8 value) public { vm.assume(value <= ${maxValue}); // ${capitalizedName} only supports ${opts.bits}-bit values (0-${maxValue}) assertEq(${mapName}[0].get(index), 0); // initial state ${mapName}[0].set(index, value); assertEq(${mapName}[0].get(index), value); // after set } - function test${capitalizedName}Truncation(uint256 index, uint8 value) public { + function testSymbolic${capitalizedName}Truncation(uint256 index, uint8 value) public { vm.assume(value > ${maxValue}); // Test values that need truncation ${mapName}[0].set(index, value); assertEq(${mapName}[0].get(index), value & ${maxValue}); // Should be truncated to ${opts.bits} bits } - function test${capitalizedName}Isolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { + function testSymbolic${capitalizedName}Isolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { vm.assume(index1 != index2); vm.assume(value1 <= ${maxValue}); vm.assume(value2 <= ${maxValue}); @@ -93,13 +93,13 @@ const byteTestTemplate = opts => { BitMaps.${capitalizedName}[2] private ${mapName}; - function test${capitalizedName}SetAndGet(uint256 index, ${valueType} value) public { + function testSymbolic${capitalizedName}SetAndGet(uint256 index, ${valueType} value) public { assertEq(${mapName}[0].get(index), 0); // initial state ${mapName}[0].set(index, value); assertEq(${mapName}[0].get(index), value); // after set } - function test${capitalizedName}AssemblyCorruption(uint256 index, ${valueType} value) public { + function testSymbolic${capitalizedName}AssemblyCorruption(uint256 index, ${valueType} value) public { uint256 corrupted = _corruptValue(value); ${valueType} corruptedValue; assembly { @@ -109,7 +109,7 @@ const byteTestTemplate = opts => { assertEq(${mapName}[0].get(index), ${valueType}(corrupted)); // Should match truncated value } - function test${capitalizedName}Isolation(uint256 index1, uint256 index2, ${valueType} value1, ${valueType} value2) public { + function testSymbolic${capitalizedName}Isolation(uint256 index1, uint256 index2, ${valueType} value1, ${valueType} value2) public { vm.assume(index1 != index2); ${mapName}[0].set(index1, value1); diff --git a/test/utils/structs/BitMaps.t.sol b/test/utils/structs/BitMaps.t.sol index 552c927ae17..d33dc1a9def 100644 --- a/test/utils/structs/BitMaps.t.sol +++ b/test/utils/structs/BitMaps.t.sol @@ -13,20 +13,20 @@ contract BitMapsTest is Test { BitMaps.BitMap[2] private _bitmaps; - function testBitMapSetAndGet(uint256 value) public { + function testSymbolicBitMapSetAndGet(uint256 value) public { assertFalse(_bitmaps[0].get(value)); // initial state _bitmaps[0].set(value); assertTrue(_bitmaps[0].get(value)); // after set } - function testBitMapSetToAndGet(uint256 value) public { + function testSymbolicBitMapSetToAndGet(uint256 value) public { _bitmaps[0].setTo(value, true); assertTrue(_bitmaps[0].get(value)); // after setTo true _bitmaps[0].setTo(value, false); assertFalse(_bitmaps[0].get(value)); // after setTo false } - function testBitMapIsolation(uint256 value1, uint256 value2) public { + function testSymbolicBitMapIsolation(uint256 value1, uint256 value2) public { vm.assume(value1 != value2); _bitmaps[0].set(value1); @@ -42,20 +42,20 @@ contract BitMapsTest is Test { BitMaps.PairMap[2] private _pairMaps; - function testPairMapSetAndGet(uint256 index, uint8 value) public { + function testSymbolicPairMapSetAndGet(uint256 index, uint8 value) public { vm.assume(value <= 3); // PairMap only supports 2-bit values (0-3) assertEq(_pairMaps[0].get(index), 0); // initial state _pairMaps[0].set(index, value); assertEq(_pairMaps[0].get(index), value); // after set } - function testPairMapTruncation(uint256 index, uint8 value) public { + function testSymbolicPairMapTruncation(uint256 index, uint8 value) public { vm.assume(value > 3); // Test values that need truncation _pairMaps[0].set(index, value); assertEq(_pairMaps[0].get(index), value & 3); // Should be truncated to 2 bits } - function testPairMapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { + function testSymbolicPairMapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { vm.assume(index1 != index2); vm.assume(value1 <= 3); vm.assume(value2 <= 3); @@ -73,20 +73,20 @@ contract BitMapsTest is Test { BitMaps.NibbleMap[2] private _nibbleMaps; - function testNibbleMapSetAndGet(uint256 index, uint8 value) public { + function testSymbolicNibbleMapSetAndGet(uint256 index, uint8 value) public { vm.assume(value <= 15); // NibbleMap only supports 4-bit values (0-15) assertEq(_nibbleMaps[0].get(index), 0); // initial state _nibbleMaps[0].set(index, value); assertEq(_nibbleMaps[0].get(index), value); // after set } - function testNibbleMapTruncation(uint256 index, uint8 value) public { + function testSymbolicNibbleMapTruncation(uint256 index, uint8 value) public { vm.assume(value > 15); // Test values that need truncation _nibbleMaps[0].set(index, value); assertEq(_nibbleMaps[0].get(index), value & 15); // Should be truncated to 4 bits } - function testNibbleMapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { + function testSymbolicNibbleMapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { vm.assume(index1 != index2); vm.assume(value1 <= 15); vm.assume(value2 <= 15); @@ -104,13 +104,13 @@ contract BitMapsTest is Test { BitMaps.Uint8Map[2] private _uint8Maps; - function testUint8MapSetAndGet(uint256 index, uint8 value) public { + function testSymbolicUint8MapSetAndGet(uint256 index, uint8 value) public { assertEq(_uint8Maps[0].get(index), 0); // initial state _uint8Maps[0].set(index, value); assertEq(_uint8Maps[0].get(index), value); // after set } - function testUint8MapAssemblyCorruption(uint256 index, uint8 value) public { + function testSymbolicUint8MapAssemblyCorruption(uint256 index, uint8 value) public { uint256 corrupted = _corruptValue(value); uint8 corruptedValue; assembly { @@ -120,7 +120,7 @@ contract BitMapsTest is Test { assertEq(_uint8Maps[0].get(index), uint8(corrupted)); // Should match truncated value } - function testUint8MapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { + function testSymbolicUint8MapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { vm.assume(index1 != index2); _uint8Maps[0].set(index1, value1); @@ -136,13 +136,13 @@ contract BitMapsTest is Test { BitMaps.Uint16Map[2] private _uint16Maps; - function testUint16MapSetAndGet(uint256 index, uint16 value) public { + function testSymbolicUint16MapSetAndGet(uint256 index, uint16 value) public { assertEq(_uint16Maps[0].get(index), 0); // initial state _uint16Maps[0].set(index, value); assertEq(_uint16Maps[0].get(index), value); // after set } - function testUint16MapAssemblyCorruption(uint256 index, uint16 value) public { + function testSymbolicUint16MapAssemblyCorruption(uint256 index, uint16 value) public { uint256 corrupted = _corruptValue(value); uint16 corruptedValue; assembly { @@ -152,7 +152,7 @@ contract BitMapsTest is Test { assertEq(_uint16Maps[0].get(index), uint16(corrupted)); // Should match truncated value } - function testUint16MapIsolation(uint256 index1, uint256 index2, uint16 value1, uint16 value2) public { + function testSymbolicUint16MapIsolation(uint256 index1, uint256 index2, uint16 value1, uint16 value2) public { vm.assume(index1 != index2); _uint16Maps[0].set(index1, value1); @@ -168,13 +168,13 @@ contract BitMapsTest is Test { BitMaps.Uint32Map[2] private _uint32Maps; - function testUint32MapSetAndGet(uint256 index, uint32 value) public { + function testSymbolicUint32MapSetAndGet(uint256 index, uint32 value) public { assertEq(_uint32Maps[0].get(index), 0); // initial state _uint32Maps[0].set(index, value); assertEq(_uint32Maps[0].get(index), value); // after set } - function testUint32MapAssemblyCorruption(uint256 index, uint32 value) public { + function testSymbolicUint32MapAssemblyCorruption(uint256 index, uint32 value) public { uint256 corrupted = _corruptValue(value); uint32 corruptedValue; assembly { @@ -184,7 +184,7 @@ contract BitMapsTest is Test { assertEq(_uint32Maps[0].get(index), uint32(corrupted)); // Should match truncated value } - function testUint32MapIsolation(uint256 index1, uint256 index2, uint32 value1, uint32 value2) public { + function testSymbolicUint32MapIsolation(uint256 index1, uint256 index2, uint32 value1, uint32 value2) public { vm.assume(index1 != index2); _uint32Maps[0].set(index1, value1); @@ -200,13 +200,13 @@ contract BitMapsTest is Test { BitMaps.Uint64Map[2] private _uint64Maps; - function testUint64MapSetAndGet(uint256 index, uint64 value) public { + function testSymbolicUint64MapSetAndGet(uint256 index, uint64 value) public { assertEq(_uint64Maps[0].get(index), 0); // initial state _uint64Maps[0].set(index, value); assertEq(_uint64Maps[0].get(index), value); // after set } - function testUint64MapAssemblyCorruption(uint256 index, uint64 value) public { + function testSymbolicUint64MapAssemblyCorruption(uint256 index, uint64 value) public { uint256 corrupted = _corruptValue(value); uint64 corruptedValue; assembly { @@ -216,7 +216,7 @@ contract BitMapsTest is Test { assertEq(_uint64Maps[0].get(index), uint64(corrupted)); // Should match truncated value } - function testUint64MapIsolation(uint256 index1, uint256 index2, uint64 value1, uint64 value2) public { + function testSymbolicUint64MapIsolation(uint256 index1, uint256 index2, uint64 value1, uint64 value2) public { vm.assume(index1 != index2); _uint64Maps[0].set(index1, value1); @@ -232,13 +232,13 @@ contract BitMapsTest is Test { BitMaps.Uint128Map[2] private _uint128Maps; - function testUint128MapSetAndGet(uint256 index, uint128 value) public { + function testSymbolicUint128MapSetAndGet(uint256 index, uint128 value) public { assertEq(_uint128Maps[0].get(index), 0); // initial state _uint128Maps[0].set(index, value); assertEq(_uint128Maps[0].get(index), value); // after set } - function testUint128MapAssemblyCorruption(uint256 index, uint128 value) public { + function testSymbolicUint128MapAssemblyCorruption(uint256 index, uint128 value) public { uint256 corrupted = _corruptValue(value); uint128 corruptedValue; assembly { @@ -248,7 +248,7 @@ contract BitMapsTest is Test { assertEq(_uint128Maps[0].get(index), uint128(corrupted)); // Should match truncated value } - function testUint128MapIsolation(uint256 index1, uint256 index2, uint128 value1, uint128 value2) public { + function testSymbolicUint128MapIsolation(uint256 index1, uint256 index2, uint128 value1, uint128 value2) public { vm.assume(index1 != index2); _uint128Maps[0].set(index1, value1); From 279fee0e492eb514a79f2ba0003b8d2a4f48f031 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 15:47:00 -0600 Subject: [PATCH 07/10] Nit --- scripts/generate/templates/BitMaps.t.js | 2 +- test/utils/structs/BitMaps.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/generate/templates/BitMaps.t.js b/scripts/generate/templates/BitMaps.t.js index bcbaf1054c9..67011e57b9e 100644 --- a/scripts/generate/templates/BitMaps.t.js +++ b/scripts/generate/templates/BitMaps.t.js @@ -6,7 +6,7 @@ const header = `\ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {BitMaps} from "../../../contracts/utils/structs/BitMaps.sol"; +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; contract BitMapsTest is Test { using BitMaps for *; diff --git a/test/utils/structs/BitMaps.t.sol b/test/utils/structs/BitMaps.t.sol index d33dc1a9def..254dcaceb84 100644 --- a/test/utils/structs/BitMaps.t.sol +++ b/test/utils/structs/BitMaps.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {BitMaps} from "../../../contracts/utils/structs/BitMaps.sol"; +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; contract BitMapsTest is Test { using BitMaps for *; From 7af552fef4f84d10a3d7218b538bf6f569966026 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 15:48:10 -0600 Subject: [PATCH 08/10] Remove Panic --- contracts/utils/structs/BitMaps.sol | 2 -- scripts/generate/templates/BitMaps.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/contracts/utils/structs/BitMaps.sol b/contracts/utils/structs/BitMaps.sol index 7064bcd9cee..9772c821833 100644 --- a/contracts/utils/structs/BitMaps.sol +++ b/contracts/utils/structs/BitMaps.sol @@ -4,8 +4,6 @@ pragma solidity ^0.8.20; -import {Panic} from "../Panic.sol"; - /** * @dev Library for managing bytes-based mappings in a compact and efficient way, provided the keys are sequential. * Largely inspired by Uniswap's https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol[merkle-distributor]. diff --git a/scripts/generate/templates/BitMaps.js b/scripts/generate/templates/BitMaps.js index 5a8cab17b8a..840eb1d0761 100644 --- a/scripts/generate/templates/BitMaps.js +++ b/scripts/generate/templates/BitMaps.js @@ -12,8 +12,6 @@ const typeList = [ const header = `\ pragma solidity ^0.8.20; -import {Panic} from "../Panic.sol"; - /** * @dev Library for managing bytes-based mappings in a compact and efficient way, provided the keys are sequential. * Largely inspired by Uniswap's https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol[merkle-distributor]. From 871f086049d78afe918b9aaee5d38f0881c4ba83 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 15:53:47 -0600 Subject: [PATCH 09/10] Add docs --- docs/modules/ROOT/pages/utilities.adoc | 77 +++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 6fe2de63b9d..67e17325b43 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -193,6 +193,81 @@ Some use cases require more powerful data structures than arrays and mappings of The `Enumerable*` structures are similar to mappings in that they store and remove elements in constant time and don't allow for repeated entries, but they also support _enumeration_, which means you can easily query all stored entries both on and off-chain. +=== Efficient Storage with BitMaps + +The xref:api:utils.adoc#BitMaps[`BitMaps`] library provides a collection of gas-efficient data structures that pack multiple values into single storage slots. This approach can significantly reduce gas costs by minimizing storage operations, especially when dealing with sequential indices. + +==== Using BitMaps for Boolean Flags + +The most common use case is `BitMap` for storing boolean flags efficiently: + +[source,solidity] +---- +using BitMaps for BitMaps.BitMap; + +contract VotingContract { + BitMaps.BitMap private _hasVoted; + + function vote(uint256 proposalId) external { + require(!_hasVoted.get(proposalId), "Already voted"); + _hasVoted.set(proposalId); + // ... voting logic + } + + function hasVoted(uint256 proposalId) external view returns (bool) { + return _hasVoted.get(proposalId); + } +} +---- + +==== Working with Small Integer Values + +For scenarios requiring small integer values, use the appropriate map type. For example, storing user roles that fit in 2 bits: + +[source,solidity] +---- +using BitMaps for BitMaps.PairMap; + +contract AccessControl { + // 0: no access, 1: read, 2: write, 3: admin + BitMaps.PairMap private _userRoles; + + function setUserRole(uint256 userId, uint8 role) external { + require(role <= 3, "Invalid role"); + _userRoles.set(userId, role); + } + + function getUserRole(uint256 userId) external view returns (uint8) { + return _userRoles.get(userId); + } +} +---- + +==== Handling Larger Values + +For larger values, use the appropriate sized map. Here's an example with time delays that fit in 32 bits: + +[source,solidity] +---- +using BitMaps for BitMaps.Uint32Map; + +contract DelaysTracker { + BitMaps.Uint32Map private _delays; + + function recordDelay(uint256 id) external { + _delays.set(id, 12 hours); + } + + function getDelay(uint256 id) external view returns (uint32) { + return _delays.get(id); + } +} +---- + +IMPORTANT: Values larger than the map's bit size are truncated. For example, setting a value of 16 in a `NibbleMap` (4-bit) will result in 0. Always ensure your values fit within the allocated bits. + +TIP: BitMaps are most efficient when indices are sequential or clustered, as they share storage slots. Sparse indices may not provide significant gas savings over regular mappings. + === Building a Merkle Tree Building an on-chain Merkle Tree allows developers to keep track of the history of roots in a decentralized manner. For these cases, the xref:api:utils.adoc#MerkleTree[`MerkleTree`] includes a predefined structure with functions to manipulate the tree (e.g. pushing values or resetting the tree). @@ -482,4 +557,4 @@ function _setTargetAdminDelay(address target, uint32 newDelay) internal virtual emit TargetAdminDelayUpdated(target, newDelay, effect); } ----- \ No newline at end of file +---- From a8da062c67fc6180ffb991b80088052bec4e5df6 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 2 Jun 2025 12:28:48 +0200 Subject: [PATCH 10/10] refactor --- contracts/utils/structs/BitMaps.sol | 30 +-- scripts/generate/templates/BitMaps.js | 181 +++++++---------- scripts/generate/templates/BitMaps.opts.js | 24 ++- scripts/generate/templates/BitMaps.t.js | 212 ++++++++++---------- test/utils/structs/BitMaps.t.sol | 213 ++++++++++++--------- 5 files changed, 318 insertions(+), 342 deletions(-) diff --git a/contracts/utils/structs/BitMaps.sol b/contracts/utils/structs/BitMaps.sol index 9772c821833..18ac3ab0b24 100644 --- a/contracts/utils/structs/BitMaps.sol +++ b/contracts/utils/structs/BitMaps.sol @@ -72,23 +72,24 @@ library BitMaps { } /** - * @dev Returns the 2-bit value at `index` in `pairMap`. + * @dev Returns the 2-bit value at `index` in `map`. */ - function get(PairMap storage pairMap, uint256 index) internal view returns (uint8) { + function get(PairMap storage map, uint256 index) internal view returns (uint8) { uint256 bucket = index >> 7; // 128 values per bucket (256/2) uint256 shift = (index & 0x7f) << 1; // i.e. (index % 128) * 2 = position * 2 - return uint8((pairMap._data[bucket] >> shift) & 0x03); + return uint8((map._data[bucket] >> shift) & 0x03); } /** - * @dev Sets the 2-bit value at `index` in `pairMap`. - * Values larger than 3 are truncated to 2 bits (e.g., 4 becomes 0). + * @dev Sets the 2-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 2 bits. Assembly-manipulated values may corrupt adjacent data. */ - function set(PairMap storage pairMap, uint256 index, uint8 value) internal { + function set(PairMap storage map, uint256 index, uint8 value) internal { uint256 bucket = index >> 7; // 128 values per bucket (256/2) uint256 shift = (index & 0x7f) << 1; // i.e. (index % 128) * 2 = position * 2 uint256 mask = 0x03 << shift; - pairMap._data[bucket] = (pairMap._data[bucket] & ~mask) | (uint256(value) << shift); // set the 2 bits + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 2 bits } struct NibbleMap { @@ -96,23 +97,24 @@ library BitMaps { } /** - * @dev Returns the 4-bit value at `index` in `nibbleMap`. + * @dev Returns the 4-bit value at `index` in `map`. */ - function get(NibbleMap storage nibbleMap, uint256 index) internal view returns (uint8) { + function get(NibbleMap storage map, uint256 index) internal view returns (uint8) { uint256 bucket = index >> 6; // 64 values per bucket (256/4) uint256 shift = (index & 0x3f) << 2; // i.e. (index % 64) * 4 = position * 4 - return uint8((nibbleMap._data[bucket] >> shift) & 0x0f); + return uint8((map._data[bucket] >> shift) & 0x0f); } /** - * @dev Sets the 4-bit value at `index` in `nibbleMap`. - * Values larger than 15 are truncated to 4 bits (e.g., 16 becomes 0). + * @dev Sets the 4-bit value at `index` in `map`. + * + * NOTE: Assumes `value` fits in 4 bits. Assembly-manipulated values may corrupt adjacent data. */ - function set(NibbleMap storage nibbleMap, uint256 index, uint8 value) internal { + function set(NibbleMap storage map, uint256 index, uint8 value) internal { uint256 bucket = index >> 6; // 64 values per bucket (256/4) uint256 shift = (index & 0x3f) << 2; // i.e. (index % 64) * 4 = position * 4 uint256 mask = 0x0f << shift; - nibbleMap._data[bucket] = (nibbleMap._data[bucket] & ~mask) | (uint256(value) << shift); // set the 4 bits + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the 4 bits } struct Uint8Map { diff --git a/scripts/generate/templates/BitMaps.js b/scripts/generate/templates/BitMaps.js index 840eb1d0761..9a046f79572 100644 --- a/scripts/generate/templates/BitMaps.js +++ b/scripts/generate/templates/BitMaps.js @@ -1,13 +1,5 @@ -const { ethers } = require('ethers'); -const { capitalize } = require('../../helpers'); const format = require('../format-lines'); -const { SUBBYTE_TYPES, BYTEMAP_TYPES } = require('./BitMaps.opts'); - -const typeList = [ - ' * * `BitMap`: 256 booleans per slot (1 bit each)', - ...SUBBYTE_TYPES.map(({ bits, name }) => ` * * \`${capitalize(name)}\`: ${256n / bits} ${bits}-bit values per slot`), - ...BYTEMAP_TYPES.map(({ bits }) => ` * * \`Uint${bits}Map\`: ${256n / bits} ${bits}-bit values per slot`), -].join('\n'); +const { TYPES } = require('./BitMaps.opts'); const header = `\ pragma solidity ^0.8.20; @@ -18,132 +10,91 @@ pragma solidity ^0.8.20; * * The library provides several map types that pack multiple values into single 256-bit storage slots: * -${typeList} + * * \`BitMap\`: 256 booleans per slot (1 bit each) +${TYPES.map(({ bits, name }) => ` * * \`${name}\`: ${256n / bits} ${bits}-bit values per slot`).join('\n')} * * This approach provides significant gas savings compared to using individual storage slots for each value: * * * Setting a zero value to non-zero only once every N times (where N is the packing density) * * Accessing the same warm slot for every N _sequential_ indices */ -library BitMaps { `; const bitmapTemplate = `\ - struct BitMap { - mapping(uint256 bucket => uint256) _data; - } - - /** - * @dev Returns whether the bit at \`index\` is set. - */ - function get(BitMap storage bitmap, uint256 index) internal view returns (bool) { - uint256 bucket = index >> 8; - uint256 mask = 1 << (index & 0xff); - return bitmap._data[bucket] & mask != 0; - } - - /** - * @dev Sets the bit at \`index\` to the boolean \`value\`. - */ - function setTo(BitMap storage bitmap, uint256 index, bool value) internal { - if (value) { - set(bitmap, index); - } else { - unset(bitmap, index); - } - } - - /** - * @dev Sets the bit at \`index\`. - */ - function set(BitMap storage bitmap, uint256 index) internal { - uint256 bucket = index >> 8; - uint256 mask = 1 << (index & 0xff); - bitmap._data[bucket] |= mask; - } +struct BitMap { + mapping(uint256 bucket => uint256) _data; +} - /** - * @dev Unsets the bit at \`index\`. - */ - function unset(BitMap storage bitmap, uint256 index) internal { - uint256 bucket = index >> 8; - uint256 mask = 1 << (index & 0xff); - bitmap._data[bucket] &= ~mask; - }`; - -const subByteTemplates = opts => { - const maxMask = ethers.toBeHex((1n << opts.bits) - 1n); - const valueShift = Math.log2(Number(opts.bits)); - const bucketShift = Math.log2(Number(256n / opts.bits)); - const valueMask = ethers.toBeHex(256n / opts.bits - 1n); - - return `\ - - struct ${capitalize(opts.name)} { - mapping(uint256 bucket => uint256) _data; - } +/** + * @dev Returns whether the bit at \`index\` is set. + */ +function get(BitMap storage bitmap, uint256 index) internal view returns (bool) { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + return bitmap._data[bucket] & mask != 0; +} - /** - * @dev Returns the ${opts.bits}-bit value at \`index\` in \`${opts.name}\`. - */ - function get(${capitalize(opts.name)} storage ${opts.name}, uint256 index) internal view returns (uint8) { - uint256 bucket = index >> ${bucketShift}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) - uint256 shift = (index & ${valueMask}) << ${valueShift}; // i.e. (index % ${Number(valueMask) + 1}) * ${opts.bits} = position * ${opts.bits} - return uint8((${opts.name}._data[bucket] >> shift) & ${maxMask}); +/** + * @dev Sets the bit at \`index\` to the boolean \`value\`. + */ +function setTo(BitMap storage bitmap, uint256 index, bool value) internal { + if (value) { + set(bitmap, index); + } else { + unset(bitmap, index); } +} - /** - * @dev Sets the ${opts.bits}-bit value at \`index\` in \`${opts.name}\`. - * Values larger than ${(1n << opts.bits) - 1n} are truncated to ${opts.bits} bits (e.g., ${1n << opts.bits} becomes 0). - */ - function set(${capitalize(opts.name)} storage ${opts.name}, uint256 index, uint8 value) internal { - uint256 bucket = index >> ${bucketShift}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) - uint256 shift = (index & ${valueMask}) << ${valueShift}; // i.e. (index % ${Number(valueMask) + 1}) * ${opts.bits} = position * ${opts.bits} - uint256 mask = ${maxMask} << shift; - ${opts.name}._data[bucket] = (${opts.name}._data[bucket] & ~mask) | (uint256(value) << shift); // set the ${opts.bits} bits - }`; -}; - -const byteTemplates = opts => { - const maxMask = ethers.toBeHex((1n << opts.bits) - 1n); - const valueShift = Math.log2(Number(opts.bits)); - const bucketShift = Math.log2(Number(256n / opts.bits)); - const valueMask = ethers.toBeHex(256n / opts.bits - 1n); - const name = `uint${opts.bits}Map`; +/** + * @dev Sets the bit at \`index\`. + */ +function set(BitMap storage bitmap, uint256 index) internal { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + bitmap._data[bucket] |= mask; +} - return `\ +/** + * @dev Unsets the bit at \`index\`. + */ +function unset(BitMap storage bitmap, uint256 index) internal { + uint256 bucket = index >> 8; + uint256 mask = 1 << (index & 0xff); + bitmap._data[bucket] &= ~mask; +} +`; - struct ${capitalize(name)} { - mapping(uint256 bucket => uint256) _data; - } +const byteTemplates = opts => `\ +struct ${opts.name} { + mapping(uint256 bucket => uint256) _data; +} - /** - * @dev Returns the ${opts.bits}-bit value at \`index\` in \`map\`. - */ - function get(${capitalize(name)} storage map, uint256 index) internal view returns (uint${opts.bits}) { - uint256 bucket = index >> ${bucketShift}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) - uint256 shift = (index & ${valueMask}) << ${valueShift}; // i.e. (index % ${Number(valueMask) + 1}) * ${opts.bits} = position * ${opts.bits} - return uint${opts.bits}(map._data[bucket] >> shift); - } +/** + * @dev Returns the ${opts.bits}-bit value at \`index\` in \`map\`. + */ +function get(${opts.name} storage map, uint256 index) internal view returns (${opts.type}) { + uint256 bucket = index >> ${8 - opts.width}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) + uint256 shift = (index & ${opts.mask}) << ${opts.width}; // i.e. (index % ${Number(opts.mask) + 1}) * ${opts.bits} = position * ${opts.bits} + return ${opts.type}(${opts.bits < 8 ? `(map._data[bucket] >> shift) & ${opts.max}` : `map._data[bucket] >> shift`}); +} - /** - * @dev Sets the ${opts.bits}-bit value at \`index\` in \`map\`. - * - * NOTE: Assumes \`value\` fits in ${opts.bits} bits. Assembly-manipulated values may corrupt adjacent data. - */ - function set(${capitalize(name)} storage map, uint256 index, uint${opts.bits} value) internal { - uint256 bucket = index >> ${bucketShift}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) - uint256 shift = (index & ${valueMask}) << ${valueShift}; // i.e. (index % ${Number(valueMask) + 1}) * ${opts.bits} = position * ${opts.bits} - uint256 mask = ${maxMask} << shift; - map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the ${opts.bits} bits - }`; -}; +/** + * @dev Sets the ${opts.bits}-bit value at \`index\` in \`map\`. + * + * NOTE: Assumes \`value\` fits in ${opts.bits} bits. Assembly-manipulated values may corrupt adjacent data. + */ +function set(${opts.name} storage map, uint256 index, ${opts.type} value) internal { + uint256 bucket = index >> ${8 - opts.width}; // ${256n / opts.bits} values per bucket (256/${opts.bits}) + uint256 shift = (index & ${opts.mask}) << ${opts.width}; // i.e. (index % ${Number(opts.mask) + 1}) * ${opts.bits} = position * ${opts.bits} + uint256 mask = ${opts.max} << shift; + map._data[bucket] = (map._data[bucket] & ~mask) | (uint256(value) << shift); // set the ${opts.bits} bits +} +`; // GENERATE module.exports = format( header.trimEnd(), - bitmapTemplate, - ...SUBBYTE_TYPES.map(subByteTemplates), - ...BYTEMAP_TYPES.map(byteTemplates), + `library BitMaps {`, + format([].concat(bitmapTemplate, TYPES.map(byteTemplates))).trimEnd(), '}', ); diff --git a/scripts/generate/templates/BitMaps.opts.js b/scripts/generate/templates/BitMaps.opts.js index e799b8a788a..66f558e3957 100644 --- a/scripts/generate/templates/BitMaps.opts.js +++ b/scripts/generate/templates/BitMaps.opts.js @@ -1,8 +1,20 @@ -const SUBBYTE_TYPES = [ - { bits: 2n, name: 'pairMap' }, - { bits: 4n, name: 'nibbleMap' }, -]; +const { ethers } = require('ethers'); -const BYTEMAP_TYPES = [{ bits: 8n }, { bits: 16n }, { bits: 32n }, { bits: 64n }, { bits: 128n }]; +const TYPES = [ + { bits: 2n, name: 'PairMap' }, + { bits: 4n, name: 'NibbleMap' }, + { bits: 8n }, + { bits: 16n }, + { bits: 32n }, + { bits: 64n }, + { bits: 128n }, +].map(({ bits, name = `Uint${bits}Map` }) => ({ + bits, + name, + max: ethers.toBeHex((1n << bits) - 1n), + mask: ethers.toBeHex(256n / bits - 1n), + type: `uint${Math.max(Number(bits), 8)}`, + width: Math.log2(Number(bits)), +})); -module.exports = { BYTEMAP_TYPES, SUBBYTE_TYPES }; +module.exports = { TYPES }; diff --git a/scripts/generate/templates/BitMaps.t.js b/scripts/generate/templates/BitMaps.t.js index 67011e57b9e..23dc2fcb954 100644 --- a/scripts/generate/templates/BitMaps.t.js +++ b/scripts/generate/templates/BitMaps.t.js @@ -1,142 +1,124 @@ -const { capitalize } = require('../../helpers'); const format = require('../format-lines'); -const { SUBBYTE_TYPES, BYTEMAP_TYPES } = require('./BitMaps.opts'); +const { TYPES } = require('./BitMaps.opts'); const header = `\ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; - -contract BitMapsTest is Test { - using BitMaps for *; `; const bitmapTests = `\ +// ========== BitMap Tests ========== + +BitMaps.BitMap[2] private _bitmaps; + +function testSymbolicBitMapSetAndGet(uint256 value) public { + assertFalse(_bitmaps[0].get(value)); // initial state + _bitmaps[0].set(value); + assertTrue(_bitmaps[0].get(value)); // after set +} + +function testSymbolicBitMapSetToAndGet(uint256 value) public { + _bitmaps[0].setTo(value, true); + assertTrue(_bitmaps[0].get(value)); // after setTo true + _bitmaps[0].setTo(value, false); + assertFalse(_bitmaps[0].get(value)); // after setTo false +} + +function testSymbolicBitMapIsolation(uint256 value1, uint256 value2) public { + vm.assume(value1 != value2); + + _bitmaps[0].set(value1); + _bitmaps[1].set(value2); + + assertTrue(_bitmaps[0].get(value1)); + assertTrue(_bitmaps[1].get(value2)); + assertFalse(_bitmaps[0].get(value2)); + assertFalse(_bitmaps[1].get(value1)); +} +`; - // ========== BitMap Tests ========== - - BitMaps.BitMap[2] private _bitmaps; - - function testSymbolicBitMapSetAndGet(uint256 value) public { - assertFalse(_bitmaps[0].get(value)); // initial state - _bitmaps[0].set(value); - assertTrue(_bitmaps[0].get(value)); // after set - } - - function testSymbolicBitMapSetToAndGet(uint256 value) public { - _bitmaps[0].setTo(value, true); - assertTrue(_bitmaps[0].get(value)); // after setTo true - _bitmaps[0].setTo(value, false); - assertFalse(_bitmaps[0].get(value)); // after setTo false - } - - function testSymbolicBitMapIsolation(uint256 value1, uint256 value2) public { - vm.assume(value1 != value2); - - _bitmaps[0].set(value1); - _bitmaps[1].set(value2); - - assertTrue(_bitmaps[0].get(value1)); - assertTrue(_bitmaps[1].get(value2)); - assertFalse(_bitmaps[0].get(value2)); - assertFalse(_bitmaps[1].get(value1)); - }`; - -const subByteTestTemplate = opts => { - const maxValue = (1n << opts.bits) - 1n; - const capitalizedName = capitalize(opts.name); - const mapName = `_${opts.name}s`; - - return `\ - - // ========== ${capitalizedName} Tests ========== - - BitMaps.${capitalizedName}[2] private ${mapName}; - - function testSymbolic${capitalizedName}SetAndGet(uint256 index, uint8 value) public { - vm.assume(value <= ${maxValue}); // ${capitalizedName} only supports ${opts.bits}-bit values (0-${maxValue}) - assertEq(${mapName}[0].get(index), 0); // initial state - ${mapName}[0].set(index, value); - assertEq(${mapName}[0].get(index), value); // after set - } - - function testSymbolic${capitalizedName}Truncation(uint256 index, uint8 value) public { - vm.assume(value > ${maxValue}); // Test values that need truncation - ${mapName}[0].set(index, value); - assertEq(${mapName}[0].get(index), value & ${maxValue}); // Should be truncated to ${opts.bits} bits - } - - function testSymbolic${capitalizedName}Isolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { - vm.assume(index1 != index2); - vm.assume(value1 <= ${maxValue}); - vm.assume(value2 <= ${maxValue}); - - ${mapName}[0].set(index1, value1); - ${mapName}[1].set(index2, value2); - - assertEq(${mapName}[0].get(index1), value1); - assertEq(${mapName}[1].get(index2), value2); - assertEq(${mapName}[0].get(index2), 0); - assertEq(${mapName}[1].get(index1), 0); - }`; -}; - -const byteTestTemplate = opts => { - const capitalizedName = `Uint${opts.bits}Map`; - const mapName = `_uint${opts.bits}Maps`; - const valueType = `uint${opts.bits}`; +const testSymbolicSetAndGet = opts => `\ +function testSymbolic${opts.name}SetAndGet(uint256 index, ${opts.type} value) public { + value = ${opts.type}(bound(value, 0, ${opts.max})); // ${opts.name} only supports ${opts.bits}-bit values (0-${opts.max}) - return `\ + assertEq(${opts.store}[0].get(index), 0); // initial state + ${opts.store}[0].set(index, value); + assertEq(${opts.store}[0].get(index), value); // after set +} +`; - // ========== ${capitalizedName} Tests ========== +const testSymbolicTruncation = opts => `\ +function testSymbolic${opts.name}Truncation(uint256 index, ${opts.type} value) public { + value = ${opts.type}(bound(value, ${1n << opts.bits}, type(${opts.type}).max)); // Test values that need truncation - BitMaps.${capitalizedName}[2] private ${mapName}; + ${opts.store}[0].set(index, value); + assertEq(${opts.store}[0].get(index), value & ${opts.max}); // Should be truncated to ${opts.bits} bits +} +`; - function testSymbolic${capitalizedName}SetAndGet(uint256 index, ${valueType} value) public { - assertEq(${mapName}[0].get(index), 0); // initial state - ${mapName}[0].set(index, value); - assertEq(${mapName}[0].get(index), value); // after set +const testSymbolicAssemblyCorruption = opts => `\ +function testSymbolic${opts.name}AssemblyCorruption(uint256 index, ${opts.type} value) public { + uint256 corrupted = _corruptValue(value); + ${opts.type} corruptedValue; + assembly { + corruptedValue := corrupted } - function testSymbolic${capitalizedName}AssemblyCorruption(uint256 index, ${valueType} value) public { - uint256 corrupted = _corruptValue(value); - ${valueType} corruptedValue; - assembly { - corruptedValue := corrupted - } - ${mapName}[0].set(index, corruptedValue); - assertEq(${mapName}[0].get(index), ${valueType}(corrupted)); // Should match truncated value - } + ${opts.store}[0].set(index, corruptedValue); + assertEq(${opts.store}[0].get(index), ${opts.type}(corrupted)); // Should match truncated value +} +`; - function testSymbolic${capitalizedName}Isolation(uint256 index1, uint256 index2, ${valueType} value1, ${valueType} value2) public { - vm.assume(index1 != index2); +const testSymbolicIsolation = opts => `\ +function testSymbolic${opts.name}Isolation(uint256 index1, uint256 index2, ${opts.type} value1, ${opts.type} value2) public { + vm.assume(index1 != index2); + value1 = ${opts.type}(bound(value1, 0, ${opts.max})); + value2 = ${opts.type}(bound(value2, 0, ${opts.max})); - ${mapName}[0].set(index1, value1); - ${mapName}[1].set(index2, value2); + ${opts.store}[0].set(index1, value1); + ${opts.store}[1].set(index2, value2); - assertEq(${mapName}[0].get(index1), value1); - assertEq(${mapName}[1].get(index2), value2); - assertEq(${mapName}[0].get(index2), 0); - assertEq(${mapName}[1].get(index1), 0); - }`; -}; + assertEq(${opts.store}[0].get(index1), value1); + assertEq(${opts.store}[1].get(index2), value2); + assertEq(${opts.store}[0].get(index2), 0); + assertEq(${opts.store}[1].get(index1), 0); +} +`; const footer = `\ - - function _corruptValue(uint256 value) private pure returns (uint256 corrupted) { - // Simulate assembly corruption by adding high bits - assembly { - corrupted := or(value, shl(200, 0xffffffff)) - } +function _corruptValue(uint256 value) private pure returns (uint256 corrupted) { + // Simulate assembly corruption by adding high bits + assembly { + corrupted := or(value, shl(200, 0xffffffff)) } -}`; +} +`; // GENERATE module.exports = format( - header.trimEnd(), - bitmapTests, - ...SUBBYTE_TYPES.map(subByteTestTemplate), - ...BYTEMAP_TYPES.map(byteTestTemplate), - footer.trimEnd(), + header, + `contract BitMapsTest is Test {`, + format( + [].concat( + `using BitMaps for *;`, + ``, + bitmapTests, + TYPES.map(opts => Object.assign(opts, { store: `_${opts.name.toLowerCase()}s` })) + .flatMap(opts => [ + `// ========== ${opts.name} Tests ==========`, + ``, + `BitMaps.${opts.name}[2] private ${opts.store};`, + ``, + testSymbolicSetAndGet(opts), + opts.bits < 8 && testSymbolicTruncation(opts), + opts.bits >= 8 && testSymbolicAssemblyCorruption(opts), + testSymbolicIsolation(opts), + ]) + .filter(e => typeof e == 'string'), + footer, + ), + ).trimEnd(), + '}', ); diff --git a/test/utils/structs/BitMaps.t.sol b/test/utils/structs/BitMaps.t.sol index 254dcaceb84..95053661be5 100644 --- a/test/utils/structs/BitMaps.t.sol +++ b/test/utils/structs/BitMaps.t.sol @@ -40,74 +40,80 @@ contract BitMapsTest is Test { // ========== PairMap Tests ========== - BitMaps.PairMap[2] private _pairMaps; + BitMaps.PairMap[2] private _pairmaps; function testSymbolicPairMapSetAndGet(uint256 index, uint8 value) public { - vm.assume(value <= 3); // PairMap only supports 2-bit values (0-3) - assertEq(_pairMaps[0].get(index), 0); // initial state - _pairMaps[0].set(index, value); - assertEq(_pairMaps[0].get(index), value); // after set + value = uint8(bound(value, 0, 0x03)); // PairMap only supports 2-bit values (0-0x03) + + assertEq(_pairmaps[0].get(index), 0); // initial state + _pairmaps[0].set(index, value); + assertEq(_pairmaps[0].get(index), value); // after set } function testSymbolicPairMapTruncation(uint256 index, uint8 value) public { - vm.assume(value > 3); // Test values that need truncation - _pairMaps[0].set(index, value); - assertEq(_pairMaps[0].get(index), value & 3); // Should be truncated to 2 bits + value = uint8(bound(value, 4, type(uint8).max)); // Test values that need truncation + + _pairmaps[0].set(index, value); + assertEq(_pairmaps[0].get(index), value & 0x03); // Should be truncated to 2 bits } function testSymbolicPairMapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { vm.assume(index1 != index2); - vm.assume(value1 <= 3); - vm.assume(value2 <= 3); + value1 = uint8(bound(value1, 0, 0x03)); + value2 = uint8(bound(value2, 0, 0x03)); - _pairMaps[0].set(index1, value1); - _pairMaps[1].set(index2, value2); + _pairmaps[0].set(index1, value1); + _pairmaps[1].set(index2, value2); - assertEq(_pairMaps[0].get(index1), value1); - assertEq(_pairMaps[1].get(index2), value2); - assertEq(_pairMaps[0].get(index2), 0); - assertEq(_pairMaps[1].get(index1), 0); + assertEq(_pairmaps[0].get(index1), value1); + assertEq(_pairmaps[1].get(index2), value2); + assertEq(_pairmaps[0].get(index2), 0); + assertEq(_pairmaps[1].get(index1), 0); } // ========== NibbleMap Tests ========== - BitMaps.NibbleMap[2] private _nibbleMaps; + BitMaps.NibbleMap[2] private _nibblemaps; function testSymbolicNibbleMapSetAndGet(uint256 index, uint8 value) public { - vm.assume(value <= 15); // NibbleMap only supports 4-bit values (0-15) - assertEq(_nibbleMaps[0].get(index), 0); // initial state - _nibbleMaps[0].set(index, value); - assertEq(_nibbleMaps[0].get(index), value); // after set + value = uint8(bound(value, 0, 0x0f)); // NibbleMap only supports 4-bit values (0-0x0f) + + assertEq(_nibblemaps[0].get(index), 0); // initial state + _nibblemaps[0].set(index, value); + assertEq(_nibblemaps[0].get(index), value); // after set } function testSymbolicNibbleMapTruncation(uint256 index, uint8 value) public { - vm.assume(value > 15); // Test values that need truncation - _nibbleMaps[0].set(index, value); - assertEq(_nibbleMaps[0].get(index), value & 15); // Should be truncated to 4 bits + value = uint8(bound(value, 16, type(uint8).max)); // Test values that need truncation + + _nibblemaps[0].set(index, value); + assertEq(_nibblemaps[0].get(index), value & 0x0f); // Should be truncated to 4 bits } function testSymbolicNibbleMapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { vm.assume(index1 != index2); - vm.assume(value1 <= 15); - vm.assume(value2 <= 15); + value1 = uint8(bound(value1, 0, 0x0f)); + value2 = uint8(bound(value2, 0, 0x0f)); - _nibbleMaps[0].set(index1, value1); - _nibbleMaps[1].set(index2, value2); + _nibblemaps[0].set(index1, value1); + _nibblemaps[1].set(index2, value2); - assertEq(_nibbleMaps[0].get(index1), value1); - assertEq(_nibbleMaps[1].get(index2), value2); - assertEq(_nibbleMaps[0].get(index2), 0); - assertEq(_nibbleMaps[1].get(index1), 0); + assertEq(_nibblemaps[0].get(index1), value1); + assertEq(_nibblemaps[1].get(index2), value2); + assertEq(_nibblemaps[0].get(index2), 0); + assertEq(_nibblemaps[1].get(index1), 0); } // ========== Uint8Map Tests ========== - BitMaps.Uint8Map[2] private _uint8Maps; + BitMaps.Uint8Map[2] private _uint8maps; function testSymbolicUint8MapSetAndGet(uint256 index, uint8 value) public { - assertEq(_uint8Maps[0].get(index), 0); // initial state - _uint8Maps[0].set(index, value); - assertEq(_uint8Maps[0].get(index), value); // after set + value = uint8(bound(value, 0, 0xff)); // Uint8Map only supports 8-bit values (0-0xff) + + assertEq(_uint8maps[0].get(index), 0); // initial state + _uint8maps[0].set(index, value); + assertEq(_uint8maps[0].get(index), value); // after set } function testSymbolicUint8MapAssemblyCorruption(uint256 index, uint8 value) public { @@ -116,30 +122,35 @@ contract BitMapsTest is Test { assembly { corruptedValue := corrupted } - _uint8Maps[0].set(index, corruptedValue); - assertEq(_uint8Maps[0].get(index), uint8(corrupted)); // Should match truncated value + + _uint8maps[0].set(index, corruptedValue); + assertEq(_uint8maps[0].get(index), uint8(corrupted)); // Should match truncated value } function testSymbolicUint8MapIsolation(uint256 index1, uint256 index2, uint8 value1, uint8 value2) public { vm.assume(index1 != index2); + value1 = uint8(bound(value1, 0, 0xff)); + value2 = uint8(bound(value2, 0, 0xff)); - _uint8Maps[0].set(index1, value1); - _uint8Maps[1].set(index2, value2); + _uint8maps[0].set(index1, value1); + _uint8maps[1].set(index2, value2); - assertEq(_uint8Maps[0].get(index1), value1); - assertEq(_uint8Maps[1].get(index2), value2); - assertEq(_uint8Maps[0].get(index2), 0); - assertEq(_uint8Maps[1].get(index1), 0); + assertEq(_uint8maps[0].get(index1), value1); + assertEq(_uint8maps[1].get(index2), value2); + assertEq(_uint8maps[0].get(index2), 0); + assertEq(_uint8maps[1].get(index1), 0); } // ========== Uint16Map Tests ========== - BitMaps.Uint16Map[2] private _uint16Maps; + BitMaps.Uint16Map[2] private _uint16maps; function testSymbolicUint16MapSetAndGet(uint256 index, uint16 value) public { - assertEq(_uint16Maps[0].get(index), 0); // initial state - _uint16Maps[0].set(index, value); - assertEq(_uint16Maps[0].get(index), value); // after set + value = uint16(bound(value, 0, 0xffff)); // Uint16Map only supports 16-bit values (0-0xffff) + + assertEq(_uint16maps[0].get(index), 0); // initial state + _uint16maps[0].set(index, value); + assertEq(_uint16maps[0].get(index), value); // after set } function testSymbolicUint16MapAssemblyCorruption(uint256 index, uint16 value) public { @@ -148,30 +159,35 @@ contract BitMapsTest is Test { assembly { corruptedValue := corrupted } - _uint16Maps[0].set(index, corruptedValue); - assertEq(_uint16Maps[0].get(index), uint16(corrupted)); // Should match truncated value + + _uint16maps[0].set(index, corruptedValue); + assertEq(_uint16maps[0].get(index), uint16(corrupted)); // Should match truncated value } function testSymbolicUint16MapIsolation(uint256 index1, uint256 index2, uint16 value1, uint16 value2) public { vm.assume(index1 != index2); + value1 = uint16(bound(value1, 0, 0xffff)); + value2 = uint16(bound(value2, 0, 0xffff)); - _uint16Maps[0].set(index1, value1); - _uint16Maps[1].set(index2, value2); + _uint16maps[0].set(index1, value1); + _uint16maps[1].set(index2, value2); - assertEq(_uint16Maps[0].get(index1), value1); - assertEq(_uint16Maps[1].get(index2), value2); - assertEq(_uint16Maps[0].get(index2), 0); - assertEq(_uint16Maps[1].get(index1), 0); + assertEq(_uint16maps[0].get(index1), value1); + assertEq(_uint16maps[1].get(index2), value2); + assertEq(_uint16maps[0].get(index2), 0); + assertEq(_uint16maps[1].get(index1), 0); } // ========== Uint32Map Tests ========== - BitMaps.Uint32Map[2] private _uint32Maps; + BitMaps.Uint32Map[2] private _uint32maps; function testSymbolicUint32MapSetAndGet(uint256 index, uint32 value) public { - assertEq(_uint32Maps[0].get(index), 0); // initial state - _uint32Maps[0].set(index, value); - assertEq(_uint32Maps[0].get(index), value); // after set + value = uint32(bound(value, 0, 0xffffffff)); // Uint32Map only supports 32-bit values (0-0xffffffff) + + assertEq(_uint32maps[0].get(index), 0); // initial state + _uint32maps[0].set(index, value); + assertEq(_uint32maps[0].get(index), value); // after set } function testSymbolicUint32MapAssemblyCorruption(uint256 index, uint32 value) public { @@ -180,30 +196,35 @@ contract BitMapsTest is Test { assembly { corruptedValue := corrupted } - _uint32Maps[0].set(index, corruptedValue); - assertEq(_uint32Maps[0].get(index), uint32(corrupted)); // Should match truncated value + + _uint32maps[0].set(index, corruptedValue); + assertEq(_uint32maps[0].get(index), uint32(corrupted)); // Should match truncated value } function testSymbolicUint32MapIsolation(uint256 index1, uint256 index2, uint32 value1, uint32 value2) public { vm.assume(index1 != index2); + value1 = uint32(bound(value1, 0, 0xffffffff)); + value2 = uint32(bound(value2, 0, 0xffffffff)); - _uint32Maps[0].set(index1, value1); - _uint32Maps[1].set(index2, value2); + _uint32maps[0].set(index1, value1); + _uint32maps[1].set(index2, value2); - assertEq(_uint32Maps[0].get(index1), value1); - assertEq(_uint32Maps[1].get(index2), value2); - assertEq(_uint32Maps[0].get(index2), 0); - assertEq(_uint32Maps[1].get(index1), 0); + assertEq(_uint32maps[0].get(index1), value1); + assertEq(_uint32maps[1].get(index2), value2); + assertEq(_uint32maps[0].get(index2), 0); + assertEq(_uint32maps[1].get(index1), 0); } // ========== Uint64Map Tests ========== - BitMaps.Uint64Map[2] private _uint64Maps; + BitMaps.Uint64Map[2] private _uint64maps; function testSymbolicUint64MapSetAndGet(uint256 index, uint64 value) public { - assertEq(_uint64Maps[0].get(index), 0); // initial state - _uint64Maps[0].set(index, value); - assertEq(_uint64Maps[0].get(index), value); // after set + value = uint64(bound(value, 0, 0xffffffffffffffff)); // Uint64Map only supports 64-bit values (0-0xffffffffffffffff) + + assertEq(_uint64maps[0].get(index), 0); // initial state + _uint64maps[0].set(index, value); + assertEq(_uint64maps[0].get(index), value); // after set } function testSymbolicUint64MapAssemblyCorruption(uint256 index, uint64 value) public { @@ -212,30 +233,35 @@ contract BitMapsTest is Test { assembly { corruptedValue := corrupted } - _uint64Maps[0].set(index, corruptedValue); - assertEq(_uint64Maps[0].get(index), uint64(corrupted)); // Should match truncated value + + _uint64maps[0].set(index, corruptedValue); + assertEq(_uint64maps[0].get(index), uint64(corrupted)); // Should match truncated value } function testSymbolicUint64MapIsolation(uint256 index1, uint256 index2, uint64 value1, uint64 value2) public { vm.assume(index1 != index2); + value1 = uint64(bound(value1, 0, 0xffffffffffffffff)); + value2 = uint64(bound(value2, 0, 0xffffffffffffffff)); - _uint64Maps[0].set(index1, value1); - _uint64Maps[1].set(index2, value2); + _uint64maps[0].set(index1, value1); + _uint64maps[1].set(index2, value2); - assertEq(_uint64Maps[0].get(index1), value1); - assertEq(_uint64Maps[1].get(index2), value2); - assertEq(_uint64Maps[0].get(index2), 0); - assertEq(_uint64Maps[1].get(index1), 0); + assertEq(_uint64maps[0].get(index1), value1); + assertEq(_uint64maps[1].get(index2), value2); + assertEq(_uint64maps[0].get(index2), 0); + assertEq(_uint64maps[1].get(index1), 0); } // ========== Uint128Map Tests ========== - BitMaps.Uint128Map[2] private _uint128Maps; + BitMaps.Uint128Map[2] private _uint128maps; function testSymbolicUint128MapSetAndGet(uint256 index, uint128 value) public { - assertEq(_uint128Maps[0].get(index), 0); // initial state - _uint128Maps[0].set(index, value); - assertEq(_uint128Maps[0].get(index), value); // after set + value = uint128(bound(value, 0, 0xffffffffffffffffffffffffffffffff)); // Uint128Map only supports 128-bit values (0-0xffffffffffffffffffffffffffffffff) + + assertEq(_uint128maps[0].get(index), 0); // initial state + _uint128maps[0].set(index, value); + assertEq(_uint128maps[0].get(index), value); // after set } function testSymbolicUint128MapAssemblyCorruption(uint256 index, uint128 value) public { @@ -244,20 +270,23 @@ contract BitMapsTest is Test { assembly { corruptedValue := corrupted } - _uint128Maps[0].set(index, corruptedValue); - assertEq(_uint128Maps[0].get(index), uint128(corrupted)); // Should match truncated value + + _uint128maps[0].set(index, corruptedValue); + assertEq(_uint128maps[0].get(index), uint128(corrupted)); // Should match truncated value } function testSymbolicUint128MapIsolation(uint256 index1, uint256 index2, uint128 value1, uint128 value2) public { vm.assume(index1 != index2); + value1 = uint128(bound(value1, 0, 0xffffffffffffffffffffffffffffffff)); + value2 = uint128(bound(value2, 0, 0xffffffffffffffffffffffffffffffff)); - _uint128Maps[0].set(index1, value1); - _uint128Maps[1].set(index2, value2); + _uint128maps[0].set(index1, value1); + _uint128maps[1].set(index2, value2); - assertEq(_uint128Maps[0].get(index1), value1); - assertEq(_uint128Maps[1].get(index2), value2); - assertEq(_uint128Maps[0].get(index2), 0); - assertEq(_uint128Maps[1].get(index1), 0); + assertEq(_uint128maps[0].get(index1), value1); + assertEq(_uint128maps[1].get(index2), value2); + assertEq(_uint128maps[0].get(index2), 0); + assertEq(_uint128maps[1].get(index1), 0); } function _corruptValue(uint256 value) private pure returns (uint256 corrupted) {