From 1c97739c1fa97941a71b99eb6b9ed096eb085b51 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 2 May 2025 15:42:17 -0600 Subject: [PATCH 01/23] Test ethers 6.13.6-beta.1 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a82155c7608..a8cdbf4de21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "chai": "^4.2.0", "eslint": "^9.0.0", "eslint-config-prettier": "^10.0.0", - "ethers": "^6.13.4", + "ethers": "^6.13.6-beta.1", "glob": "^11.0.0", "globals": "^16.0.0", "graphlib": "^2.1.8", @@ -4483,9 +4483,9 @@ } }, "node_modules/ethers": { - "version": "6.13.7", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.7.tgz", - "integrity": "sha512-qbaJ0uIrjh+huP1Lad2f2QtzW5dcqSVjIzVH6yWB4dKoMuj2WqYz5aMeeQTCNpAKgTJBM5J9vcc2cYJ23UAimQ==", + "version": "6.13.6-beta.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.6-beta.1.tgz", + "integrity": "sha512-sJZklf+m7QrlzYnOFbR0qHPqgYHeevbY98VIhzvnSdzhJVN/nNV/skKc/4wjyxbWRhK9t7r6ENcwUwLPjfxTLw==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index d2f7dec6c6d..635290f6205 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "chai": "^4.2.0", "eslint": "^9.0.0", "eslint-config-prettier": "^10.0.0", - "ethers": "^6.13.4", + "ethers": "^6.13.6-beta.1", "glob": "^11.0.0", "globals": "^16.0.0", "graphlib": "^2.1.8", From 6b1bbd8d7bb6096597b478c443fde4d6b51e1c6a Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 2 May 2025 15:44:49 -0600 Subject: [PATCH 02/23] up --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a8cdbf4de21..1ac37124f2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4483,9 +4483,9 @@ } }, "node_modules/ethers": { - "version": "6.13.6-beta.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.6-beta.1.tgz", - "integrity": "sha512-sJZklf+m7QrlzYnOFbR0qHPqgYHeevbY98VIhzvnSdzhJVN/nNV/skKc/4wjyxbWRhK9t7r6ENcwUwLPjfxTLw==", + "version": "6.13.7", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.7.tgz", + "integrity": "sha512-qbaJ0uIrjh+huP1Lad2f2QtzW5dcqSVjIzVH6yWB4dKoMuj2WqYz5aMeeQTCNpAKgTJBM5J9vcc2cYJ23UAimQ==", "dev": true, "funding": [ { From 19fe4c525d96d9d8baa2b9bf48be25b3c0384b44 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 2 May 2025 15:52:18 -0600 Subject: [PATCH 03/23] up --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ac37124f2e..e1b7018388f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "chai": "^4.2.0", "eslint": "^9.0.0", "eslint-config-prettier": "^10.0.0", - "ethers": "^6.13.6-beta.1", + "ethers": "6.13.6-beta.1", "glob": "^11.0.0", "globals": "^16.0.0", "graphlib": "^2.1.8", @@ -4483,9 +4483,9 @@ } }, "node_modules/ethers": { - "version": "6.13.7", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.7.tgz", - "integrity": "sha512-qbaJ0uIrjh+huP1Lad2f2QtzW5dcqSVjIzVH6yWB4dKoMuj2WqYz5aMeeQTCNpAKgTJBM5J9vcc2cYJ23UAimQ==", + "version": "6.13.6-beta.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.6-beta.1.tgz", + "integrity": "sha512-sJZklf+m7QrlzYnOFbR0qHPqgYHeevbY98VIhzvnSdzhJVN/nNV/skKc/4wjyxbWRhK9t7r6ENcwUwLPjfxTLw==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 635290f6205..64ee144be16 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "chai": "^4.2.0", "eslint": "^9.0.0", "eslint-config-prettier": "^10.0.0", - "ethers": "^6.13.6-beta.1", + "ethers": "6.13.6-beta.1", "glob": "^11.0.0", "globals": "^16.0.0", "graphlib": "^2.1.8", From c39d5f5755c64b3c0ff7fc666c615e58f6135f2a Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 3 May 2025 00:43:58 -0600 Subject: [PATCH 04/23] Tweak workflows --- .github/actions/setup/action.yml | 3 ++- .github/workflows/checks.yml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 3c5fc602e13..fe8a85b3f55 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -13,7 +13,8 @@ runs: path: '**/node_modules' key: npm-v3-${{ hashFiles('**/package-lock.json') }} - name: Install dependencies - run: npm ci + ## TODO: Remove when EIP-7702 authorizations are enabled in latest non-beta ethers version + run: npm ci --legacy-peer-deps shell: bash if: steps.cache.outputs.cache-hit != 'true' - name: Install Foundry diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 6aca7f30cb4..cba9894b3b9 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -118,6 +118,8 @@ jobs: - uses: actions/checkout@v4 - name: Set up environment uses: ./.github/actions/setup + ## TODO: Remove when EIP-7702 authorizations are enabled in latest non-beta ethers version + - run: rm package-lock.json package.json # Dependencies already installed - uses: crytic/slither-action@v0.4.1 codespell: From 54f632a588bd9b306d42c51d3e7613625809b665 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 3 May 2025 00:46:34 -0600 Subject: [PATCH 05/23] Use Solidity 0.8.27 as default and set default EVM to prague --- foundry.toml | 2 +- hardhat.config.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/foundry.toml b/foundry.toml index 7a2e8a60942..ea8b1fadd88 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,5 @@ [profile.default] -solc_version = '0.8.24' +solc_version = '0.8.27' evm_version = 'prague' optimizer = true optimizer-runs = 200 diff --git a/hardhat.config.js b/hardhat.config.js index 30c19ca6d5b..17ebf45eeb7 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -18,7 +18,7 @@ const { argv } = require('yargs/yargs')() compiler: { alias: 'compileVersion', type: 'string', - default: '0.8.24', + default: '0.8.27', }, src: { alias: 'source', @@ -38,7 +38,7 @@ const { argv } = require('yargs/yargs')() evm: { alias: 'evmVersion', type: 'string', - default: 'cancun', + default: 'prague', }, // Extra modules coverage: { From 58c794e3f4caf91421cd4d643b5465dd513e282f Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 3 May 2025 01:04:29 -0600 Subject: [PATCH 06/23] Adjust ERC2771Forwarder gas to avoid GasFloorMoreThanGasLimit --- test/metatx/ERC2771Forwarder.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/metatx/ERC2771Forwarder.test.js b/test/metatx/ERC2771Forwarder.test.js index bf6cfd10c4b..07682c18747 100644 --- a/test/metatx/ERC2771Forwarder.test.js +++ b/test/metatx/ERC2771Forwarder.test.js @@ -174,7 +174,7 @@ describe('ERC2771Forwarder', function () { // Because the relayer call consumes gas until the `CALL` opcode, the gas left after failing // the subcall won't enough to finish the top level call (after testing), so we add a // moderated buffer. - const gasLimit = estimate + 2_000n; + const gasLimit = estimate + 10_000n; // The subcall out of gas should be caught by the contract and then bubbled up consuming // the available gas with an `invalid` opcode. From e412bd933bccbcbcc85ca1e020be9c3e5ac1d5d4 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 2 May 2025 01:42:11 -0600 Subject: [PATCH 07/23] Add EnumerableSetExtended and EnumerableMapExtended --- contracts/utils/README.adoc | 5 + .../utils/structs/EnumerableMapExtended.sol | 287 ++++++++++++ .../utils/structs/EnumerableSetExtended.sol | 422 ++++++++++++++++++ scripts/generate/run.js | 2 + scripts/generate/templates/Enumerable.opts.js | 64 +++ scripts/generate/templates/EnumerableMap.js | 4 +- .../generate/templates/EnumerableMap.opts.js | 19 - .../templates/EnumerableMapExtended.js | 179 ++++++++ scripts/generate/templates/EnumerableSet.js | 4 +- .../generate/templates/EnumerableSet.opts.js | 12 - .../templates/EnumerableSetExtended.js | 319 +++++++++++++ test/utils/structs/EnumerableMap.test.js | 8 +- .../structs/EnumerableMapExtended.test.js | 66 +++ test/utils/structs/EnumerableSet.test.js | 6 +- .../structs/EnumerableSetExtended.test.js | 62 +++ 15 files changed, 1417 insertions(+), 42 deletions(-) create mode 100644 contracts/utils/structs/EnumerableMapExtended.sol create mode 100644 contracts/utils/structs/EnumerableSetExtended.sol create mode 100644 scripts/generate/templates/Enumerable.opts.js delete mode 100644 scripts/generate/templates/EnumerableMap.opts.js create mode 100644 scripts/generate/templates/EnumerableMapExtended.js delete mode 100644 scripts/generate/templates/EnumerableSet.opts.js create mode 100644 scripts/generate/templates/EnumerableSetExtended.js create mode 100644 test/utils/structs/EnumerableMapExtended.test.js create mode 100644 test/utils/structs/EnumerableSetExtended.test.js diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 74b26b236cc..83912e5b0c2 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -23,6 +23,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {BitMaps}: A simple library to manage boolean value mapped to a numerical index in an efficient way. * {EnumerableMap}: A type like Solidity's https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`], but with key-value _enumeration_: this will let you know how many entries a mapping has, and iterate over them (which is not possible with `mapping`). * {EnumerableSet}: Like {EnumerableMap}, but for https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets]. Can be used to store privileged accounts, issued IDs, etc. + * {EnumerableSetExtended} and {EnumerableMapExtended}: Extensions of the `EnumerableSet` and `EnumerableMap` libraries with more types, including non-value types. * {DoubleEndedQueue}: An implementation of a https://en.wikipedia.org/wiki/Double-ended_queue[double ended queue] whose values can be added or removed from both sides. Useful for FIFO and LIFO structures. * {CircularBuffer}: A data structure to store the last N values pushed to it. * {Checkpoints}: A data structure to store values mapped to a strictly increasing key. Can be used for storing and accessing values over time. @@ -108,8 +109,12 @@ Ethereum contracts have no native concept of an interface, so applications must {{EnumerableMap}} +{{EnumerableMapExtended}} + {{EnumerableSet}} +{{EnumerableSetExtended}} + {{DoubleEndedQueue}} {{CircularBuffer}} diff --git a/contracts/utils/structs/EnumerableMapExtended.sol b/contracts/utils/structs/EnumerableMapExtended.sol new file mode 100644 index 00000000000..31dd512afd5 --- /dev/null +++ b/contracts/utils/structs/EnumerableMapExtended.sol @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/EnumerableMapExtended.js. + +pragma solidity ^0.8.20; + +import {EnumerableSet} from "./EnumerableSet.sol"; +import {EnumerableSetExtended} from "./EnumerableSetExtended.sol"; + +/** + * @dev Library for managing an enumerable variant of Solidity's + * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] + * type for non-value types as keys. + * + * Maps have the following properties: + * + * - Entries are added, removed, and checked for existence in constant time + * (O(1)). + * - Entries are enumerated in O(n). No guarantees are made on the ordering. + * - Map can be cleared (all entries removed) in O(n). + * + * ```solidity + * contract Example { + * // Add the library methods + * using EnumerableMapExtended for EnumerableMapExtended.BytesToUintMap; + * + * // Declare a set state variable + * EnumerableMapExtended.BytesToUintMap private myMap; + * } + * ``` + * + * The following map types are supported: + * + * - `bytes -> uint256` (`BytesToUintMap`) + * - `string -> string` (`StringToStringMap`) + * + * [WARNING] + * ==== + * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure + * unusable. + * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. + * + * In order to clean an EnumerableMap, you can either remove all elements one by one or create a fresh instance using an + * array of EnumerableMap. + * ==== + * + * NOTE: Extensions of {EnumerableMap} + */ +library EnumerableMapExtended { + using EnumerableSet for *; + using EnumerableSetExtended for *; + + /** + * @dev Query for a nonexistent map key. + */ + error EnumerableMapNonexistentBytesKey(bytes key); + + struct BytesToUintMap { + // Storage of keys + EnumerableSetExtended.BytesSet _keys; + mapping(bytes key => uint256) _values; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(BytesToUintMap storage map, bytes memory key, uint256 value) internal returns (bool) { + map._values[key] = value; + return map._keys.add(key); + } + + /** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(BytesToUintMap storage map, bytes memory key) internal returns (bool) { + delete map._values[key]; + return map._keys.remove(key); + } + + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(BytesToUintMap storage map) internal { + uint256 len = length(map); + for (uint256 i = 0; i < len; ++i) { + delete map._values[map._keys.at(i)]; + } + map._keys.clear(); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(BytesToUintMap storage map, bytes memory key) internal view returns (bool) { + return map._keys.contains(key); + } + + /** + * @dev Returns the number of key-value pairs in the map. O(1). + */ + function length(BytesToUintMap storage map) internal view returns (uint256) { + return map._keys.length(); + } + + /** + * @dev Returns the key-value pair stored at position `index` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at( + BytesToUintMap storage map, + uint256 index + ) internal view returns (bytes memory key, uint256 value) { + key = map._keys.at(index); + value = map._values[key]; + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet( + BytesToUintMap storage map, + bytes memory key + ) internal view returns (bool exists, uint256 value) { + value = map._values[key]; + exists = value != uint256(0) || contains(map, key); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(BytesToUintMap storage map, bytes memory key) internal view returns (uint256 value) { + bool exists; + (exists, value) = tryGet(map, key); + if (!exists) { + revert EnumerableMapNonexistentBytesKey(key); + } + } + + /** + * @dev Return the an array containing all the keys + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(BytesToUintMap storage map) internal view returns (bytes[] memory) { + return map._keys.values(); + } + + /** + * @dev Query for a nonexistent map key. + */ + error EnumerableMapNonexistentStringKey(string key); + + struct StringToStringMap { + // Storage of keys + EnumerableSetExtended.StringSet _keys; + mapping(string key => string) _values; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(StringToStringMap storage map, string memory key, string memory value) internal returns (bool) { + map._values[key] = value; + return map._keys.add(key); + } + + /** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(StringToStringMap storage map, string memory key) internal returns (bool) { + delete map._values[key]; + return map._keys.remove(key); + } + + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(StringToStringMap storage map) internal { + uint256 len = length(map); + for (uint256 i = 0; i < len; ++i) { + delete map._values[map._keys.at(i)]; + } + map._keys.clear(); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(StringToStringMap storage map, string memory key) internal view returns (bool) { + return map._keys.contains(key); + } + + /** + * @dev Returns the number of key-value pairs in the map. O(1). + */ + function length(StringToStringMap storage map) internal view returns (uint256) { + return map._keys.length(); + } + + /** + * @dev Returns the key-value pair stored at position `index` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at( + StringToStringMap storage map, + uint256 index + ) internal view returns (string memory key, string memory value) { + key = map._keys.at(index); + value = map._values[key]; + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet( + StringToStringMap storage map, + string memory key + ) internal view returns (bool exists, string memory value) { + value = map._values[key]; + exists = bytes(value).length != 0 || contains(map, key); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(StringToStringMap storage map, string memory key) internal view returns (string memory value) { + bool exists; + (exists, value) = tryGet(map, key); + if (!exists) { + revert EnumerableMapNonexistentStringKey(key); + } + } + + /** + * @dev Return the an array containing all the keys + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(StringToStringMap storage map) internal view returns (string[] memory) { + return map._keys.values(); + } +} diff --git a/contracts/utils/structs/EnumerableSetExtended.sol b/contracts/utils/structs/EnumerableSetExtended.sol new file mode 100644 index 00000000000..82f3ae9da76 --- /dev/null +++ b/contracts/utils/structs/EnumerableSetExtended.sol @@ -0,0 +1,422 @@ +// SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/EnumerableSetExtended.js. + +pragma solidity ^0.8.20; + +import {Hashes} from "../cryptography/Hashes.sol"; + +/** + * @dev Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of non-value + * types. + * + * Sets have the following properties: + * + * - Elements are added, removed, and checked for existence in constant time + * (O(1)). + * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * - Set can be cleared (all elements removed) in O(n). + * + * ```solidity + * contract Example { + * // Add the library methods + * using EnumerableSetExtended for EnumerableSetExtended.StringSet; + * + * // Declare a set state variable + * EnumerableSetExtended.StringSet private mySet; + * } + * ``` + * + * Sets of type `string` (`StringSet`), `bytes` (`BytesSet`) and + * `bytes32[2]` (`Bytes32x2Set`) are supported. + * + * [WARNING] + * ==== + * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure + * unusable. + * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. + * + * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an + * array of EnumerableSet. + * ==== + * + * NOTE: This is an extension of {EnumerableSet}. + */ +library EnumerableSetExtended { + struct StringSet { + // Storage of set values + string[] _values; + // Position is the index of the value in the `values` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(string value => uint256) _positions; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(StringSet storage self, string memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[value] = self._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(StringSet storage self, string memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + uint256 position = self._positions[value]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + string memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[lastValue] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(StringSet storage set) internal { + uint256 len = length(set); + for (uint256 i = 0; i < len; ++i) { + delete set._positions[set._values[i]]; + } + // Replace when these are available in Arrays.sol + string[] storage array = set._values; + assembly ("memory-safe") { + sstore(array.slot, 0) + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(StringSet storage self, string memory value) internal view returns (bool) { + return self._positions[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(StringSet storage self) internal view returns (uint256) { + return self._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(StringSet storage self, uint256 index) internal view returns (string memory) { + return self._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(StringSet storage self) internal view returns (string[] memory) { + return self._values; + } + + struct BytesSet { + // Storage of set values + bytes[] _values; + // Position is the index of the value in the `values` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(bytes value => uint256) _positions; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(BytesSet storage self, bytes memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[value] = self._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(BytesSet storage self, bytes memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + uint256 position = self._positions[value]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + bytes memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[lastValue] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(BytesSet storage set) internal { + uint256 len = length(set); + for (uint256 i = 0; i < len; ++i) { + delete set._positions[set._values[i]]; + } + // Replace when these are available in Arrays.sol + bytes[] storage array = set._values; + assembly ("memory-safe") { + sstore(array.slot, 0) + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(BytesSet storage self, bytes memory value) internal view returns (bool) { + return self._positions[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(BytesSet storage self) internal view returns (uint256) { + return self._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(BytesSet storage self, uint256 index) internal view returns (bytes memory) { + return self._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(BytesSet storage self) internal view returns (bytes[] memory) { + return self._values; + } + + struct Bytes32x2Set { + // Storage of set values + bytes32[2][] _values; + // Position is the index of the value in the `values` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(bytes32 valueHash => uint256) _positions; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(Bytes32x2Set storage self, bytes32[2] memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[_hash(value)] = self._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(Bytes32x2Set storage self, bytes32[2] memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + bytes32 valueHash = _hash(value); + uint256 position = self._positions[valueHash]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + bytes32[2] memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[_hash(lastValue)] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[valueHash]; + + return true; + } else { + return false; + } + } + + /** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(Bytes32x2Set storage self) internal { + bytes32[2][] storage v = self._values; + + uint256 len = length(self); + for (uint256 i = 0; i < len; ++i) { + delete self._positions[_hash(v[i])]; + } + assembly ("memory-safe") { + sstore(v.slot, 0) + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(Bytes32x2Set storage self, bytes32[2] memory value) internal view returns (bool) { + return self._positions[_hash(value)] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(Bytes32x2Set storage self) internal view returns (uint256) { + return self._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32x2Set storage self, uint256 index) internal view returns (bytes32[2] memory) { + return self._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(Bytes32x2Set storage self) internal view returns (bytes32[2][] memory) { + return self._values; + } + + function _hash(bytes32[2] memory value) private pure returns (bytes32) { + return Hashes.efficientKeccak256(value[0], value[1]); + } +} diff --git a/scripts/generate/run.js b/scripts/generate/run.js index 6779c93f44b..a2f89e16e54 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -44,6 +44,8 @@ for (const [file, template] of Object.entries({ 'utils/Packing.sol': './templates/Packing.js', 'mocks/StorageSlotMock.sol': './templates/StorageSlotMock.js', 'mocks/TransientSlotMock.sol': './templates/TransientSlotMock.js', + 'utils/structs/EnumerableSetExtended.sol': './templates/EnumerableSetExtended.js', + 'utils/structs/EnumerableMapExtended.sol': './templates/EnumerableMapExtended.js', })) { generateFromTemplate(file, template, './contracts/'); } diff --git a/scripts/generate/templates/Enumerable.opts.js b/scripts/generate/templates/Enumerable.opts.js new file mode 100644 index 00000000000..cad0f4d7908 --- /dev/null +++ b/scripts/generate/templates/Enumerable.opts.js @@ -0,0 +1,64 @@ +const { capitalize, mapValues } = require('../../helpers'); + +const mapType = str => (str == 'uint256' ? 'Uint' : capitalize(str)); + +const formatSetType = type => ({ name: `${mapType(type)}Set`, type }); + +const SET_TYPES = ['bytes32', 'address', 'uint256'].map(formatSetType); + +const formatMapType = (keyType, valueType) => ({ + name: `${mapType(keyType)}To${mapType(valueType)}Map`, + keyType, + valueType, +}); + +const MAP_TYPES = ['uint256', 'address', 'bytes32'] + .flatMap((key, _, array) => array.map(value => [key, value])) + .slice(0, -1) // remove bytes32 → byte32 (last one) that is already defined + .map(args => formatMapType(...args)); + +const extendedTypeDescr = ({ type, size = 0, memory = false }) => { + memory |= size > 0; + + const name = [type == 'uint256' ? 'Uint' : capitalize(type), size].filter(Boolean).join('x'); + const base = size ? type : undefined; + const typeFull = size ? `${type}[${size}]` : type; + const typeLoc = memory ? `${typeFull} memory` : typeFull; + return { name, type: typeFull, typeLoc, base, size, memory }; +}; + +const toExtendedSetTypeDescr = value => ({ name: value.name + 'Set', value }); + +const toExtendedMapTypeDescr = ({ key, value }) => ({ + name: `${key.name}To${value.name}Map`, + keySet: toExtendedSetTypeDescr(key), + key, + value, +}); + +const EXTENDED_SET_TYPES = [ + { type: 'bytes32', size: 2 }, + { type: 'string', memory: true }, + { type: 'bytes', memory: true }, +] + .map(extendedTypeDescr) + .map(toExtendedSetTypeDescr); + +const EXTENDED_MAP_TYPES = [ + { key: { type: 'bytes', memory: true }, value: { type: 'uint256' } }, + { key: { type: 'string', memory: true }, value: { type: 'string', memory: true } }, +] + .map(entry => mapValues(entry, extendedTypeDescr)) + .map(toExtendedMapTypeDescr); + +module.exports = { + SET_TYPES, + MAP_TYPES, + EXTENDED_SET_TYPES, + EXTENDED_MAP_TYPES, + formatSetType, + formatMapType, + extendedTypeDescr, + toExtendedSetTypeDescr, + toExtendedMapTypeDescr, +}; diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index 284e5ac0281..8879c7a4b11 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -1,6 +1,6 @@ const format = require('../format-lines'); const { fromBytes32, toBytes32 } = require('./conversion'); -const { TYPES } = require('./EnumerableMap.opts'); +const { MAP_TYPES } = require('./Enumerable.opts'); const header = `\ pragma solidity ^0.8.20; @@ -290,7 +290,7 @@ module.exports = format( 'using EnumerableSet for EnumerableSet.Bytes32Set;', '', defaultMap, - TYPES.map(details => customMap(details)), + MAP_TYPES.map(details => customMap(details)), ), ).trimEnd(), '}', diff --git a/scripts/generate/templates/EnumerableMap.opts.js b/scripts/generate/templates/EnumerableMap.opts.js deleted file mode 100644 index d26ab05b2ac..00000000000 --- a/scripts/generate/templates/EnumerableMap.opts.js +++ /dev/null @@ -1,19 +0,0 @@ -const { capitalize } = require('../../helpers'); - -const mapType = str => (str == 'uint256' ? 'Uint' : capitalize(str)); - -const formatType = (keyType, valueType) => ({ - name: `${mapType(keyType)}To${mapType(valueType)}Map`, - keyType, - valueType, -}); - -const TYPES = ['uint256', 'address', 'bytes32'] - .flatMap((key, _, array) => array.map(value => [key, value])) - .slice(0, -1) // remove bytes32 → byte32 (last one) that is already defined - .map(args => formatType(...args)); - -module.exports = { - TYPES, - formatType, -}; diff --git a/scripts/generate/templates/EnumerableMapExtended.js b/scripts/generate/templates/EnumerableMapExtended.js new file mode 100644 index 00000000000..b1b55278974 --- /dev/null +++ b/scripts/generate/templates/EnumerableMapExtended.js @@ -0,0 +1,179 @@ +const format = require('../format-lines'); +const { EXTENDED_SET_TYPES, EXTENDED_MAP_TYPES } = require('./Enumerable.opts'); + +const header = `\ +pragma solidity ^0.8.20; + +import {EnumerableSet} from "./EnumerableSet.sol"; +import {EnumerableSetExtended} from "./EnumerableSetExtended.sol"; + +/** + * @dev Library for managing an enumerable variant of Solidity's + * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[\`mapping\`] + * type for non-value types as keys. + * + * Maps have the following properties: + * + * - Entries are added, removed, and checked for existence in constant time + * (O(1)). + * - Entries are enumerated in O(n). No guarantees are made on the ordering. + * - Map can be cleared (all entries removed) in O(n). + * + * \`\`\`solidity + * contract Example { + * // Add the library methods + * using EnumerableMapExtended for EnumerableMapExtended.BytesToUintMap; + * + * // Declare a set state variable + * EnumerableMapExtended.BytesToUintMap private myMap; + * } + * \`\`\` + * + * The following map types are supported: + * + * - \`bytes -> uint256\` (\`BytesToUintMap\`) + * - \`string -> string\` (\`StringToStringMap\`) + * + * [WARNING] + * ==== + * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure + * unusable. + * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. + * + * In order to clean an EnumerableMap, you can either remove all elements one by one or create a fresh instance using an + * array of EnumerableMap. + * ==== + * + * NOTE: Extensions of {EnumerableMap} + */ +`; + +const map = ({ name, keySet, key, value }) => `\ +/** + * @dev Query for a nonexistent map key. + */ +error EnumerableMapNonexistent${key.name}Key(${key.type} key); + +struct ${name} { + // Storage of keys + ${EXTENDED_SET_TYPES.some(el => el.name == keySet.name) ? 'EnumerableSetExtended' : 'EnumerableSet'}.${keySet.name} _keys; + mapping(${key.type} key => ${value.type}) _values; +} + +/** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ +function set(${name} storage map, ${key.typeLoc} key, ${value.typeLoc} value) internal returns (bool) { + map._values[key] = value; + return map._keys.add(key); +} + +/** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ +function remove(${name} storage map, ${key.typeLoc} key) internal returns (bool) { + delete map._values[key]; + return map._keys.remove(key); +} + +/** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ +function clear(${name} storage map) internal { + uint256 len = length(map); + for (uint256 i = 0; i < len; ++i) { + delete map._values[map._keys.at(i)]; + } + map._keys.clear(); +} + +/** + * @dev Returns true if the key is in the map. O(1). + */ +function contains(${name} storage map, ${key.typeLoc} key) internal view returns (bool) { + return map._keys.contains(key); +} + +/** + * @dev Returns the number of key-value pairs in the map. O(1). + */ +function length(${name} storage map) internal view returns (uint256) { + return map._keys.length(); +} + +/** + * @dev Returns the key-value pair stored at position \`index\` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - \`index\` must be strictly less than {length}. + */ +function at( + ${name} storage map, + uint256 index +) internal view returns (${key.typeLoc} key, ${value.typeLoc} value) { + key = map._keys.at(index); + value = map._values[key]; +} + +/** + * @dev Tries to returns the value associated with \`key\`. O(1). + * Does not revert if \`key\` is not in the map. + */ +function tryGet( + ${name} storage map, + ${key.typeLoc} key +) internal view returns (bool exists, ${value.typeLoc} value) { + value = map._values[key]; + exists = ${value.memory ? 'bytes(value).length != 0' : `value != ${value.type}(0)`} || contains(map, key); +} + +/** + * @dev Returns the value associated with \`key\`. O(1). + * + * Requirements: + * + * - \`key\` must be in the map. + */ +function get(${name} storage map, ${key.typeLoc} key) internal view returns (${value.typeLoc} value) { + bool exists; + (exists, value) = tryGet(map, key); + if (!exists) { + revert EnumerableMapNonexistent${key.name}Key(key); + } +} + +/** + * @dev Return the an array containing all the keys + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. + */ +function keys(${name} storage map) internal view returns (${key.type}[] memory) { + return map._keys.values(); +} +`; + +// GENERATE +module.exports = format( + header.trimEnd(), + 'library EnumerableMapExtended {', + format( + [].concat('using EnumerableSet for *;', 'using EnumerableSetExtended for *;', '', EXTENDED_MAP_TYPES.map(map)), + ).trimEnd(), + '}', +); diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index 3169d6a46f5..26263ba1889 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -1,6 +1,6 @@ const format = require('../format-lines'); const { fromBytes32, toBytes32 } = require('./conversion'); -const { TYPES } = require('./EnumerableSet.opts'); +const { SET_TYPES } = require('./Enumerable.opts'); const header = `\ pragma solidity ^0.8.20; @@ -267,7 +267,7 @@ module.exports = format( format( [].concat( defaultSet, - TYPES.map(details => customSet(details)), + SET_TYPES.map(details => customSet(details)), ), ).trimEnd(), '}', diff --git a/scripts/generate/templates/EnumerableSet.opts.js b/scripts/generate/templates/EnumerableSet.opts.js deleted file mode 100644 index 739f0acdfe4..00000000000 --- a/scripts/generate/templates/EnumerableSet.opts.js +++ /dev/null @@ -1,12 +0,0 @@ -const { capitalize } = require('../../helpers'); - -const mapType = str => (str == 'uint256' ? 'Uint' : capitalize(str)); - -const formatType = type => ({ - name: `${mapType(type)}Set`, - type, -}); - -const TYPES = ['bytes32', 'address', 'uint256'].map(formatType); - -module.exports = { TYPES, formatType }; diff --git a/scripts/generate/templates/EnumerableSetExtended.js b/scripts/generate/templates/EnumerableSetExtended.js new file mode 100644 index 00000000000..73c4b446160 --- /dev/null +++ b/scripts/generate/templates/EnumerableSetExtended.js @@ -0,0 +1,319 @@ +const format = require('../format-lines'); +const { EXTENDED_SET_TYPES } = require('./Enumerable.opts'); + +const header = `\ +pragma solidity ^0.8.20; + +import {Hashes} from "../cryptography/Hashes.sol"; + +/** + * @dev Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of non-value + * types. + * + * Sets have the following properties: + * + * - Elements are added, removed, and checked for existence in constant time + * (O(1)). + * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * - Set can be cleared (all elements removed) in O(n). + * + * \`\`\`solidity + * contract Example { + * // Add the library methods + * using EnumerableSetExtended for EnumerableSetExtended.StringSet; + * + * // Declare a set state variable + * EnumerableSetExtended.StringSet private mySet; + * } + * \`\`\` + * + * Sets of type \`string\` (\`StringSet\`), \`bytes\` (\`BytesSet\`) and + * \`bytes32[2]\` (\`Bytes32x2Set\`) are supported. + * + * [WARNING] + * ==== + * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure + * unusable. + * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. + * + * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an + * array of EnumerableSet. + * ==== + * + * NOTE: This is an extension of {EnumerableSet}. + */ +`; + +const set = ({ name, value }) => `\ +struct ${name} { + // Storage of set values + ${value.type}[] _values; + // Position is the index of the value in the \`values\` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(${value.type} value => uint256) _positions; +} + +/** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ +function add(${name} storage self, ${value.type} memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[value] = self._values.length; + return true; + } else { + return false; + } +} + +/** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ +function remove(${name} storage self, ${value.type} memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + uint256 position = self._positions[value]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + ${value.type} memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[lastValue] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[value]; + + return true; + } else { + return false; + } +} + +/** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ +function clear(${name} storage set) internal { + uint256 len = length(set); + for (uint256 i = 0; i < len; ++i) { + delete set._positions[set._values[i]]; + } + // Replace when these are available in Arrays.sol + ${value.type}[] storage array = set._values; + assembly ("memory-safe") { + sstore(array.slot, 0) + } +} + +/** + * @dev Returns true if the value is in the set. O(1). + */ +function contains(${name} storage self, ${value.type} memory value) internal view returns (bool) { + return self._positions[value] != 0; +} + +/** + * @dev Returns the number of values on the set. O(1). + */ +function length(${name} storage self) internal view returns (uint256) { + return self._values.length; +} + +/** + * @dev Returns the value stored at position \`index\` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - \`index\` must be strictly less than {length}. + */ +function at(${name} storage self, uint256 index) internal view returns (${value.type} memory) { + return self._values[index]; +} + +/** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ +function values(${name} storage self) internal view returns (${value.type}[] memory) { + return self._values; +} +`; + +const arraySet = ({ name, value }) => `\ +struct ${name} { + // Storage of set values + ${value.type}[] _values; + // Position is the index of the value in the \`values\` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(bytes32 valueHash => uint256) _positions; +} + +/** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ +function add(${name} storage self, ${value.type} memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[_hash(value)] = self._values.length; + return true; + } else { + return false; + } +} + +/** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ +function remove(${name} storage self, ${value.type} memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + bytes32 valueHash = _hash(value); + uint256 position = self._positions[valueHash]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + ${value.type} memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[_hash(lastValue)] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[valueHash]; + + return true; + } else { + return false; + } +} + +/** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ +function clear(${name} storage self) internal { + ${value.type}[] storage v = self._values; + + uint256 len = length(self); + for (uint256 i = 0; i < len; ++i) { + delete self._positions[_hash(v[i])]; + } + assembly ("memory-safe") { + sstore(v.slot, 0) + } +} + +/** + * @dev Returns true if the value is in the set. O(1). + */ +function contains(${name} storage self, ${value.type} memory value) internal view returns (bool) { + return self._positions[_hash(value)] != 0; +} + +/** + * @dev Returns the number of values on the set. O(1). + */ +function length(${name} storage self) internal view returns (uint256) { + return self._values.length; +} + +/** + * @dev Returns the value stored at position \`index\` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - \`index\` must be strictly less than {length}. + */ +function at(${name} storage self, uint256 index) internal view returns (${value.type} memory) { + return self._values[index]; +} + +/** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ +function values(${name} storage self) internal view returns (${value.type}[] memory) { + return self._values; +} +`; + +const hashes = `\ +function _hash(bytes32[2] memory value) private pure returns (bytes32) { + return Hashes.efficientKeccak256(value[0], value[1]); +} +`; + +// GENERATE +module.exports = format( + header.trimEnd(), + 'library EnumerableSetExtended {', + format( + [].concat( + EXTENDED_SET_TYPES.filter(({ value }) => value.size == 0).map(set), + EXTENDED_SET_TYPES.filter(({ value }) => value.size > 0).map(arraySet), + hashes, + ), + ).trimEnd(), + '}', +); diff --git a/test/utils/structs/EnumerableMap.test.js b/test/utils/structs/EnumerableMap.test.js index cb4b77a651f..d512fb32d18 100644 --- a/test/utils/structs/EnumerableMap.test.js +++ b/test/utils/structs/EnumerableMap.test.js @@ -3,17 +3,17 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { mapValues } = require('../../helpers/iterate'); const { generators } = require('../../helpers/random'); -const { TYPES, formatType } = require('../../../scripts/generate/templates/EnumerableMap.opts'); +const { MAP_TYPES, formatMapType } = require('../../../scripts/generate/templates/Enumerable.opts'); const { shouldBehaveLikeMap } = require('./EnumerableMap.behavior'); // Add Bytes32ToBytes32Map that must be tested but is not part of the generated types. -TYPES.unshift(formatType('bytes32', 'bytes32')); +MAP_TYPES.unshift(formatMapType('bytes32', 'bytes32')); async function fixture() { const mock = await ethers.deployContract('$EnumerableMap'); const env = Object.fromEntries( - TYPES.map(({ name, keyType, valueType }) => [ + MAP_TYPES.map(({ name, keyType, valueType }) => [ name, { keyType, @@ -52,7 +52,7 @@ describe('EnumerableMap', function () { Object.assign(this, await loadFixture(fixture)); }); - for (const { name } of TYPES) { + for (const { name } of MAP_TYPES) { describe(name, function () { beforeEach(async function () { Object.assign(this, this.env[name]); diff --git a/test/utils/structs/EnumerableMapExtended.test.js b/test/utils/structs/EnumerableMapExtended.test.js new file mode 100644 index 00000000000..a40b83dd12d --- /dev/null +++ b/test/utils/structs/EnumerableMapExtended.test.js @@ -0,0 +1,66 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { mapValues } = require('../../helpers/iterate'); +const { generators } = require('../../helpers/random'); +const { EXTENDED_MAP_TYPES } = require('../../../scripts/generate/templates/Enumerable.opts'); + +const { shouldBehaveLikeMap } = require('./EnumerableMap.behavior'); + +async function fixture() { + const mock = await ethers.deployContract('$EnumerableMapExtended'); + + const env = Object.fromEntries( + EXTENDED_MAP_TYPES.map(({ name, key, value }) => [ + name, + { + key, + value, + keys: Array.from({ length: 3 }, generators[key.type]), + values: Array.from({ length: 3 }, generators[value.type]), + zeroValue: generators[value.type].zero, + methods: mapValues( + { + set: `$set(uint256,${key.type},${value.type})`, + get: `$get(uint256,${key.type})`, + tryGet: `$tryGet(uint256,${key.type})`, + remove: `$remove(uint256,${key.type})`, + clear: `$clear_EnumerableMapExtended_${name}(uint256)`, + length: `$length_EnumerableMapExtended_${name}(uint256)`, + at: `$at_EnumerableMapExtended_${name}(uint256,uint256)`, + contains: `$contains(uint256,${key.type})`, + keys: `$keys_EnumerableMapExtended_${name}(uint256)`, + }, + fnSig => + (...args) => + mock.getFunction(fnSig)(0, ...args), + ), + events: { + setReturn: `return$set_EnumerableMapExtended_${name}_${key.type}_${value.type}`, + removeReturn: `return$remove_EnumerableMapExtended_${name}_${key.type}`, + }, + error: key.memory || value.memory ? `EnumerableMapNonexistent${key.name}Key` : `EnumerableMapNonexistentKey`, + }, + ]), + ); + + return { mock, env }; +} + +describe('EnumerableMapExtended', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + for (const { name, key, value } of EXTENDED_MAP_TYPES) { + describe(`${name} (enumerable map from ${key.type} to ${value.type})`, function () { + beforeEach(async function () { + Object.assign(this, this.env[name]); + [this.keyA, this.keyB, this.keyC] = this.keys; + [this.valueA, this.valueB, this.valueC] = this.values; + }); + + shouldBehaveLikeMap(); + }); + } +}); diff --git a/test/utils/structs/EnumerableSet.test.js b/test/utils/structs/EnumerableSet.test.js index 1f92727a4c4..f60adc103a5 100644 --- a/test/utils/structs/EnumerableSet.test.js +++ b/test/utils/structs/EnumerableSet.test.js @@ -3,7 +3,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { mapValues } = require('../../helpers/iterate'); const { generators } = require('../../helpers/random'); -const { TYPES } = require('../../../scripts/generate/templates/EnumerableSet.opts'); +const { SET_TYPES } = require('../../../scripts/generate/templates/Enumerable.opts'); const { shouldBehaveLikeSet } = require('./EnumerableSet.behavior'); @@ -20,7 +20,7 @@ async function fixture() { const mock = await ethers.deployContract('$EnumerableSet'); const env = Object.fromEntries( - TYPES.map(({ name, type }) => [ + SET_TYPES.map(({ name, type }) => [ type, { values: Array.from({ length: 3 }, generators[type]), @@ -49,7 +49,7 @@ describe('EnumerableSet', function () { Object.assign(this, await loadFixture(fixture)); }); - for (const { type } of TYPES) { + for (const { type } of SET_TYPES) { describe(type, function () { beforeEach(function () { Object.assign(this, this.env[type]); diff --git a/test/utils/structs/EnumerableSetExtended.test.js b/test/utils/structs/EnumerableSetExtended.test.js new file mode 100644 index 00000000000..3b9d5ad746d --- /dev/null +++ b/test/utils/structs/EnumerableSetExtended.test.js @@ -0,0 +1,62 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { mapValues } = require('../../helpers/iterate'); +const { generators } = require('../../helpers/random'); +const { EXTENDED_SET_TYPES } = require('../../../scripts/generate/templates/Enumerable.opts'); + +const { shouldBehaveLikeSet } = require('./EnumerableSet.behavior'); + +async function fixture() { + const mock = await ethers.deployContract('$EnumerableSetExtended'); + + const env = Object.fromEntries( + EXTENDED_SET_TYPES.map(({ name, value }) => [ + name, + { + value, + values: Array.from( + { length: 3 }, + value.size ? () => Array.from({ length: value.size }, generators[value.base]) : generators[value.type], + ), + methods: mapValues( + { + add: `$add(uint256,${value.type})`, + remove: `$remove(uint256,${value.type})`, + contains: `$contains(uint256,${value.type})`, + clear: `$clear_EnumerableSetExtended_${name}(uint256)`, + length: `$length_EnumerableSetExtended_${name}(uint256)`, + at: `$at_EnumerableSetExtended_${name}(uint256,uint256)`, + values: `$values_EnumerableSetExtended_${name}(uint256)`, + }, + fnSig => + (...args) => + mock.getFunction(fnSig)(0, ...args), + ), + events: { + addReturn: `return$add_EnumerableSetExtended_${name}_${value.type.replace(/[[\]]/g, '_')}`, + removeReturn: `return$remove_EnumerableSetExtended_${name}_${value.type.replace(/[[\]]/g, '_')}`, + }, + }, + ]), + ); + + return { mock, env }; +} + +describe('EnumerableSetExtended', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + for (const { name, value } of EXTENDED_SET_TYPES) { + describe(`${name} (enumerable set of ${value.type})`, function () { + beforeEach(function () { + Object.assign(this, this.env[name]); + [this.valueA, this.valueB, this.valueC] = this.values; + }); + + shouldBehaveLikeSet(); + }); + } +}); From f7f64eec43c040decc3e08a1c7af855cb03a336d Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 3 May 2025 10:35:51 -0600 Subject: [PATCH 08/23] Add changeset and fix linting --- .changeset/pink-dolls-shop.md | 5 +++++ contracts/utils/structs/EnumerableMapExtended.sol | 10 ++-------- contracts/utils/structs/EnumerableSetExtended.sol | 2 +- scripts/generate/run.js | 4 ++-- 4 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 .changeset/pink-dolls-shop.md diff --git a/.changeset/pink-dolls-shop.md b/.changeset/pink-dolls-shop.md new file mode 100644 index 00000000000..7718a738fcd --- /dev/null +++ b/.changeset/pink-dolls-shop.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`EnumerableSetExtended` and `EnumerableMapExtended`: Extensions of the `EnumerableSet` and `EnumerableMap` libraries with more types, including non-value types. diff --git a/contracts/utils/structs/EnumerableMapExtended.sol b/contracts/utils/structs/EnumerableMapExtended.sol index 31dd512afd5..91b912e6d72 100644 --- a/contracts/utils/structs/EnumerableMapExtended.sol +++ b/contracts/utils/structs/EnumerableMapExtended.sol @@ -120,10 +120,7 @@ library EnumerableMapExtended { * * - `index` must be strictly less than {length}. */ - function at( - BytesToUintMap storage map, - uint256 index - ) internal view returns (bytes memory key, uint256 value) { + function at(BytesToUintMap storage map, uint256 index) internal view returns (bytes memory key, uint256 value) { key = map._keys.at(index); value = map._values[key]; } @@ -132,10 +129,7 @@ library EnumerableMapExtended { * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ - function tryGet( - BytesToUintMap storage map, - bytes memory key - ) internal view returns (bool exists, uint256 value) { + function tryGet(BytesToUintMap storage map, bytes memory key) internal view returns (bool exists, uint256 value) { value = map._values[key]; exists = value != uint256(0) || contains(map, key); } diff --git a/contracts/utils/structs/EnumerableSetExtended.sol b/contracts/utils/structs/EnumerableSetExtended.sol index 82f3ae9da76..a5ba388a74f 100644 --- a/contracts/utils/structs/EnumerableSetExtended.sol +++ b/contracts/utils/structs/EnumerableSetExtended.sol @@ -27,7 +27,7 @@ import {Hashes} from "../cryptography/Hashes.sol"; * } * ``` * - * Sets of type `string` (`StringSet`), `bytes` (`BytesSet`) and + * Sets of type `string` (`StringSet`), `bytes` (`BytesSet`) and * `bytes32[2]` (`Bytes32x2Set`) are supported. * * [WARNING] diff --git a/scripts/generate/run.js b/scripts/generate/run.js index a2f89e16e54..68516fa135b 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -// const cp = require('child_process'); +const cp = require('child_process'); const fs = require('fs'); const path = require('path'); const format = require('./format-lines'); @@ -27,7 +27,7 @@ function generateFromTemplate(file, template, outputPrefix = '') { ); fs.writeFileSync(output, content); - // cp.execFileSync('prettier', ['--write', output]); + cp.execFileSync('prettier', ['--write', output]); } // Contracts From b695659abcbfe833f7f2596e9c24b0790de11a3e Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 21:45:03 -0600 Subject: [PATCH 09/23] Remove TODOs --- .github/actions/setup/action.yml | 3 +-- .github/workflows/checks.yml | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index fe8a85b3f55..3c5fc602e13 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -13,8 +13,7 @@ runs: path: '**/node_modules' key: npm-v3-${{ hashFiles('**/package-lock.json') }} - name: Install dependencies - ## TODO: Remove when EIP-7702 authorizations are enabled in latest non-beta ethers version - run: npm ci --legacy-peer-deps + run: npm ci shell: bash if: steps.cache.outputs.cache-hit != 'true' - name: Install Foundry diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index cba9894b3b9..6aca7f30cb4 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -118,8 +118,6 @@ jobs: - uses: actions/checkout@v4 - name: Set up environment uses: ./.github/actions/setup - ## TODO: Remove when EIP-7702 authorizations are enabled in latest non-beta ethers version - - run: rm package-lock.json package.json # Dependencies already installed - uses: crytic/slither-action@v0.4.1 codespell: From 5c4fb8824760226964f0d859619fe43e1cffa303 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 21:46:32 -0600 Subject: [PATCH 10/23] Reset package-lock --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a2fecf97526..30fbd0b1c12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4483,9 +4483,9 @@ } }, "node_modules/ethers": { - "version": "6.14.3", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.3.tgz", - "integrity": "sha512-qq7ft/oCJohoTcsNPFaXSQUm457MA5iWqkf1Mb11ujONdg7jBI6sAOrHaTi3j0CBqIGFSCeR/RMc+qwRRub7IA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.0.tgz", + "integrity": "sha512-KgHwltNSMdbrGWEyKkM0Rt2s+u1nDH/5BVDQakLinzGEJi4bWindBzZSCC4gKsbZjwDTI6ex/8suR9Ihbmz4IQ==", "dev": true, "funding": [ { @@ -4497,6 +4497,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", From 8f32638bd24d53a2bf5249c820a62c538c7d3bb2 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 1 Jun 2025 21:48:34 -0600 Subject: [PATCH 11/23] Revert run.js --- scripts/generate/run.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/generate/run.js b/scripts/generate/run.js index 68516fa135b..6779c93f44b 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -const cp = require('child_process'); +// const cp = require('child_process'); const fs = require('fs'); const path = require('path'); const format = require('./format-lines'); @@ -27,7 +27,7 @@ function generateFromTemplate(file, template, outputPrefix = '') { ); fs.writeFileSync(output, content); - cp.execFileSync('prettier', ['--write', output]); + // cp.execFileSync('prettier', ['--write', output]); } // Contracts @@ -44,8 +44,6 @@ for (const [file, template] of Object.entries({ 'utils/Packing.sol': './templates/Packing.js', 'mocks/StorageSlotMock.sol': './templates/StorageSlotMock.js', 'mocks/TransientSlotMock.sol': './templates/TransientSlotMock.js', - 'utils/structs/EnumerableSetExtended.sol': './templates/EnumerableSetExtended.js', - 'utils/structs/EnumerableMapExtended.sol': './templates/EnumerableMapExtended.js', })) { generateFromTemplate(file, template, './contracts/'); } From b12ca61a09acabae77d2747c21cb2d2b3d2b553d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 2 Jun 2025 10:42:23 +0200 Subject: [PATCH 12/23] Merge Enumerable{Set,Map}Extended into Enumerable{Set,Map} --- contracts/utils/README.adoc | 5 - contracts/utils/structs/EnumerableMap.sol | 234 +++++++++- .../utils/structs/EnumerableMapExtended.sol | 281 ------------ contracts/utils/structs/EnumerableSet.sol | 378 ++++++++++++++++ .../utils/structs/EnumerableSetExtended.sol | 422 ------------------ scripts/generate/run.js | 4 +- scripts/generate/templates/Enumerable.opts.js | 67 ++- scripts/generate/templates/EnumerableMap.js | 159 ++++++- .../templates/EnumerableMapExtended.js | 179 -------- scripts/generate/templates/EnumerableSet.js | 270 ++++++++++- .../templates/EnumerableSetExtended.js | 319 ------------- test/utils/structs/EnumerableMap.behavior.js | 2 +- test/utils/structs/EnumerableMap.test.js | 59 ++- .../structs/EnumerableMapExtended.test.js | 66 --- test/utils/structs/EnumerableSet.test.js | 31 +- .../structs/EnumerableSetExtended.test.js | 62 --- 16 files changed, 1106 insertions(+), 1432 deletions(-) delete mode 100644 contracts/utils/structs/EnumerableMapExtended.sol delete mode 100644 contracts/utils/structs/EnumerableSetExtended.sol delete mode 100644 scripts/generate/templates/EnumerableMapExtended.js delete mode 100644 scripts/generate/templates/EnumerableSetExtended.js delete mode 100644 test/utils/structs/EnumerableMapExtended.test.js delete mode 100644 test/utils/structs/EnumerableSetExtended.test.js diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 6a265a9fa6a..10b8e17aafe 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -23,7 +23,6 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {BitMaps}: A simple library to manage boolean value mapped to a numerical index in an efficient way. * {EnumerableMap}: A type like Solidity's https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`], but with key-value _enumeration_: this will let you know how many entries a mapping has, and iterate over them (which is not possible with `mapping`). * {EnumerableSet}: Like {EnumerableMap}, but for https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets]. Can be used to store privileged accounts, issued IDs, etc. - * {EnumerableSetExtended} and {EnumerableMapExtended}: Extensions of the `EnumerableSet` and `EnumerableMap` libraries with more types, including non-value types. * {DoubleEndedQueue}: An implementation of a https://en.wikipedia.org/wiki/Double-ended_queue[double ended queue] whose values can be added or removed from both sides. Useful for FIFO and LIFO structures. * {CircularBuffer}: A data structure to store the last N values pushed to it. * {Checkpoints}: A data structure to store values mapped to a strictly increasing key. Can be used for storing and accessing values over time. @@ -121,12 +120,8 @@ Ethereum contracts have no native concept of an interface, so applications must {{EnumerableMap}} -{{EnumerableMapExtended}} - {{EnumerableSet}} -{{EnumerableSetExtended}} - {{DoubleEndedQueue}} {{CircularBuffer}} diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index 09fa498fcb4..a53ac05ec60 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -39,6 +39,8 @@ import {EnumerableSet} from "./EnumerableSet.sol"; * - `address -> address` (`AddressToAddressMap`) since v5.1.0 * - `address -> bytes32` (`AddressToBytes32Map`) since v5.1.0 * - `bytes32 -> address` (`Bytes32ToAddressMap`) since v5.1.0 + * - `bytes -> uint256` (`BytesToUintMap`) since v5.4.0 + * - `string -> string` (`StringToStringMap`) since v5.4.0 * * [WARNING] * ==== @@ -51,7 +53,7 @@ import {EnumerableSet} from "./EnumerableSet.sol"; * ==== */ library EnumerableMap { - using EnumerableSet for EnumerableSet.Bytes32Set; + using EnumerableSet for *; // To implement this library for multiple types with as little code repetition as possible, we write it in // terms of a generic Map type with bytes32 keys and values. The Map implementation uses private functions, @@ -997,4 +999,234 @@ library EnumerableMap { return result; } + + /** + * @dev Query for a nonexistent map key. + */ + error EnumerableMapNonexistentBytesKey(bytes key); + + struct BytesToUintMap { + // Storage of keys + EnumerableSet.BytesSet _keys; + mapping(bytes key => uint256) _values; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(BytesToUintMap storage map, bytes memory key, uint256 value) internal returns (bool) { + map._values[key] = value; + return map._keys.add(key); + } + + /** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(BytesToUintMap storage map, bytes memory key) internal returns (bool) { + delete map._values[key]; + return map._keys.remove(key); + } + + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(BytesToUintMap storage map) internal { + uint256 len = length(map); + for (uint256 i = 0; i < len; ++i) { + delete map._values[map._keys.at(i)]; + } + map._keys.clear(); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(BytesToUintMap storage map, bytes memory key) internal view returns (bool) { + return map._keys.contains(key); + } + + /** + * @dev Returns the number of key-value pairs in the map. O(1). + */ + function length(BytesToUintMap storage map) internal view returns (uint256) { + return map._keys.length(); + } + + /** + * @dev Returns the key-value pair stored at position `index` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(BytesToUintMap storage map, uint256 index) internal view returns (bytes memory key, uint256 value) { + key = map._keys.at(index); + value = map._values[key]; + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet(BytesToUintMap storage map, bytes memory key) internal view returns (bool exists, uint256 value) { + value = map._values[key]; + exists = value != uint256(0) || contains(map, key); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(BytesToUintMap storage map, bytes memory key) internal view returns (uint256 value) { + bool exists; + (exists, value) = tryGet(map, key); + if (!exists) { + revert EnumerableMapNonexistentBytesKey(key); + } + } + + /** + * @dev Return the an array containing all the keys + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(BytesToUintMap storage map) internal view returns (bytes[] memory) { + return map._keys.values(); + } + + /** + * @dev Query for a nonexistent map key. + */ + error EnumerableMapNonexistentStringKey(string key); + + struct StringToStringMap { + // Storage of keys + EnumerableSet.StringSet _keys; + mapping(string key => string) _values; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(StringToStringMap storage map, string memory key, string memory value) internal returns (bool) { + map._values[key] = value; + return map._keys.add(key); + } + + /** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(StringToStringMap storage map, string memory key) internal returns (bool) { + delete map._values[key]; + return map._keys.remove(key); + } + + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(StringToStringMap storage map) internal { + uint256 len = length(map); + for (uint256 i = 0; i < len; ++i) { + delete map._values[map._keys.at(i)]; + } + map._keys.clear(); + } + + /** + * @dev Returns true if the key is in the map. O(1). + */ + function contains(StringToStringMap storage map, string memory key) internal view returns (bool) { + return map._keys.contains(key); + } + + /** + * @dev Returns the number of key-value pairs in the map. O(1). + */ + function length(StringToStringMap storage map) internal view returns (uint256) { + return map._keys.length(); + } + + /** + * @dev Returns the key-value pair stored at position `index` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at( + StringToStringMap storage map, + uint256 index + ) internal view returns (string memory key, string memory value) { + key = map._keys.at(index); + value = map._values[key]; + } + + /** + * @dev Tries to returns the value associated with `key`. O(1). + * Does not revert if `key` is not in the map. + */ + function tryGet( + StringToStringMap storage map, + string memory key + ) internal view returns (bool exists, string memory value) { + value = map._values[key]; + exists = bytes(value).length != 0 || contains(map, key); + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(StringToStringMap storage map, string memory key) internal view returns (string memory value) { + bool exists; + (exists, value) = tryGet(map, key); + if (!exists) { + revert EnumerableMapNonexistentStringKey(key); + } + } + + /** + * @dev Return the an array containing all the keys + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function keys(StringToStringMap storage map) internal view returns (string[] memory) { + return map._keys.values(); + } } diff --git a/contracts/utils/structs/EnumerableMapExtended.sol b/contracts/utils/structs/EnumerableMapExtended.sol deleted file mode 100644 index 91b912e6d72..00000000000 --- a/contracts/utils/structs/EnumerableMapExtended.sol +++ /dev/null @@ -1,281 +0,0 @@ -// SPDX-License-Identifier: MIT -// This file was procedurally generated from scripts/generate/templates/EnumerableMapExtended.js. - -pragma solidity ^0.8.20; - -import {EnumerableSet} from "./EnumerableSet.sol"; -import {EnumerableSetExtended} from "./EnumerableSetExtended.sol"; - -/** - * @dev Library for managing an enumerable variant of Solidity's - * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] - * type for non-value types as keys. - * - * Maps have the following properties: - * - * - Entries are added, removed, and checked for existence in constant time - * (O(1)). - * - Entries are enumerated in O(n). No guarantees are made on the ordering. - * - Map can be cleared (all entries removed) in O(n). - * - * ```solidity - * contract Example { - * // Add the library methods - * using EnumerableMapExtended for EnumerableMapExtended.BytesToUintMap; - * - * // Declare a set state variable - * EnumerableMapExtended.BytesToUintMap private myMap; - * } - * ``` - * - * The following map types are supported: - * - * - `bytes -> uint256` (`BytesToUintMap`) - * - `string -> string` (`StringToStringMap`) - * - * [WARNING] - * ==== - * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure - * unusable. - * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. - * - * In order to clean an EnumerableMap, you can either remove all elements one by one or create a fresh instance using an - * array of EnumerableMap. - * ==== - * - * NOTE: Extensions of {EnumerableMap} - */ -library EnumerableMapExtended { - using EnumerableSet for *; - using EnumerableSetExtended for *; - - /** - * @dev Query for a nonexistent map key. - */ - error EnumerableMapNonexistentBytesKey(bytes key); - - struct BytesToUintMap { - // Storage of keys - EnumerableSetExtended.BytesSet _keys; - mapping(bytes key => uint256) _values; - } - - /** - * @dev Adds a key-value pair to a map, or updates the value for an existing - * key. O(1). - * - * Returns true if the key was added to the map, that is if it was not - * already present. - */ - function set(BytesToUintMap storage map, bytes memory key, uint256 value) internal returns (bool) { - map._values[key] = value; - return map._keys.add(key); - } - - /** - * @dev Removes a key-value pair from a map. O(1). - * - * Returns true if the key was removed from the map, that is if it was present. - */ - function remove(BytesToUintMap storage map, bytes memory key) internal returns (bool) { - delete map._values[key]; - return map._keys.remove(key); - } - - /** - * @dev Removes all the entries from a map. O(n). - * - * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the - * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. - */ - function clear(BytesToUintMap storage map) internal { - uint256 len = length(map); - for (uint256 i = 0; i < len; ++i) { - delete map._values[map._keys.at(i)]; - } - map._keys.clear(); - } - - /** - * @dev Returns true if the key is in the map. O(1). - */ - function contains(BytesToUintMap storage map, bytes memory key) internal view returns (bool) { - return map._keys.contains(key); - } - - /** - * @dev Returns the number of key-value pairs in the map. O(1). - */ - function length(BytesToUintMap storage map) internal view returns (uint256) { - return map._keys.length(); - } - - /** - * @dev Returns the key-value pair stored at position `index` in the map. O(1). - * - * Note that there are no guarantees on the ordering of entries inside the - * array, and it may change when more entries are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function at(BytesToUintMap storage map, uint256 index) internal view returns (bytes memory key, uint256 value) { - key = map._keys.at(index); - value = map._values[key]; - } - - /** - * @dev Tries to returns the value associated with `key`. O(1). - * Does not revert if `key` is not in the map. - */ - function tryGet(BytesToUintMap storage map, bytes memory key) internal view returns (bool exists, uint256 value) { - value = map._values[key]; - exists = value != uint256(0) || contains(map, key); - } - - /** - * @dev Returns the value associated with `key`. O(1). - * - * Requirements: - * - * - `key` must be in the map. - */ - function get(BytesToUintMap storage map, bytes memory key) internal view returns (uint256 value) { - bool exists; - (exists, value) = tryGet(map, key); - if (!exists) { - revert EnumerableMapNonexistentBytesKey(key); - } - } - - /** - * @dev Return the an array containing all the keys - * - * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed - * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that - * this function has an unbounded cost, and using it as part of a state-changing function may render the function - * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. - */ - function keys(BytesToUintMap storage map) internal view returns (bytes[] memory) { - return map._keys.values(); - } - - /** - * @dev Query for a nonexistent map key. - */ - error EnumerableMapNonexistentStringKey(string key); - - struct StringToStringMap { - // Storage of keys - EnumerableSetExtended.StringSet _keys; - mapping(string key => string) _values; - } - - /** - * @dev Adds a key-value pair to a map, or updates the value for an existing - * key. O(1). - * - * Returns true if the key was added to the map, that is if it was not - * already present. - */ - function set(StringToStringMap storage map, string memory key, string memory value) internal returns (bool) { - map._values[key] = value; - return map._keys.add(key); - } - - /** - * @dev Removes a key-value pair from a map. O(1). - * - * Returns true if the key was removed from the map, that is if it was present. - */ - function remove(StringToStringMap storage map, string memory key) internal returns (bool) { - delete map._values[key]; - return map._keys.remove(key); - } - - /** - * @dev Removes all the entries from a map. O(n). - * - * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the - * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. - */ - function clear(StringToStringMap storage map) internal { - uint256 len = length(map); - for (uint256 i = 0; i < len; ++i) { - delete map._values[map._keys.at(i)]; - } - map._keys.clear(); - } - - /** - * @dev Returns true if the key is in the map. O(1). - */ - function contains(StringToStringMap storage map, string memory key) internal view returns (bool) { - return map._keys.contains(key); - } - - /** - * @dev Returns the number of key-value pairs in the map. O(1). - */ - function length(StringToStringMap storage map) internal view returns (uint256) { - return map._keys.length(); - } - - /** - * @dev Returns the key-value pair stored at position `index` in the map. O(1). - * - * Note that there are no guarantees on the ordering of entries inside the - * array, and it may change when more entries are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function at( - StringToStringMap storage map, - uint256 index - ) internal view returns (string memory key, string memory value) { - key = map._keys.at(index); - value = map._values[key]; - } - - /** - * @dev Tries to returns the value associated with `key`. O(1). - * Does not revert if `key` is not in the map. - */ - function tryGet( - StringToStringMap storage map, - string memory key - ) internal view returns (bool exists, string memory value) { - value = map._values[key]; - exists = bytes(value).length != 0 || contains(map, key); - } - - /** - * @dev Returns the value associated with `key`. O(1). - * - * Requirements: - * - * - `key` must be in the map. - */ - function get(StringToStringMap storage map, string memory key) internal view returns (string memory value) { - bool exists; - (exists, value) = tryGet(map, key); - if (!exists) { - revert EnumerableMapNonexistentStringKey(key); - } - } - - /** - * @dev Return the an array containing all the keys - * - * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed - * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that - * this function has an unbounded cost, and using it as part of a state-changing function may render the function - * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. - */ - function keys(StringToStringMap storage map) internal view returns (string[] memory) { - return map._keys.values(); - } -} diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index ec8bb377965..f70a4d19334 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.20; import {Arrays} from "../Arrays.sol"; +import {Hashes} from "../cryptography/Hashes.sol"; /** * @dev Library for managing @@ -419,4 +420,381 @@ library EnumerableSet { return result; } + + struct StringSet { + // Storage of set values + string[] _values; + // Position is the index of the value in the `values` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(string value => uint256) _positions; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(StringSet storage self, string memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[value] = self._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(StringSet storage self, string memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + uint256 position = self._positions[value]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + string memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[lastValue] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(StringSet storage set) internal { + uint256 len = length(set); + for (uint256 i = 0; i < len; ++i) { + delete set._positions[set._values[i]]; + } + // Replace when these are available in Arrays.sol + string[] storage array = set._values; + assembly ("memory-safe") { + sstore(array.slot, 0) + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(StringSet storage self, string memory value) internal view returns (bool) { + return self._positions[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(StringSet storage self) internal view returns (uint256) { + return self._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(StringSet storage self, uint256 index) internal view returns (string memory) { + return self._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(StringSet storage self) internal view returns (string[] memory) { + return self._values; + } + + struct BytesSet { + // Storage of set values + bytes[] _values; + // Position is the index of the value in the `values` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(bytes value => uint256) _positions; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(BytesSet storage self, bytes memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[value] = self._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(BytesSet storage self, bytes memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + uint256 position = self._positions[value]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + bytes memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[lastValue] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[value]; + + return true; + } else { + return false; + } + } + + /** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(BytesSet storage set) internal { + uint256 len = length(set); + for (uint256 i = 0; i < len; ++i) { + delete set._positions[set._values[i]]; + } + // Replace when these are available in Arrays.sol + bytes[] storage array = set._values; + assembly ("memory-safe") { + sstore(array.slot, 0) + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(BytesSet storage self, bytes memory value) internal view returns (bool) { + return self._positions[value] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(BytesSet storage self) internal view returns (uint256) { + return self._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(BytesSet storage self, uint256 index) internal view returns (bytes memory) { + return self._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(BytesSet storage self) internal view returns (bytes[] memory) { + return self._values; + } + + struct Bytes32x2Set { + // Storage of set values + bytes32[2][] _values; + // Position is the index of the value in the `values` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(bytes32 valueHash => uint256) _positions; + } + + /** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(Bytes32x2Set storage self, bytes32[2] memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[_hash(value)] = self._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(Bytes32x2Set storage self, bytes32[2] memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + bytes32 valueHash = _hash(value); + uint256 position = self._positions[valueHash]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + bytes32[2] memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[_hash(lastValue)] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[valueHash]; + + return true; + } else { + return false; + } + } + + /** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(Bytes32x2Set storage self) internal { + bytes32[2][] storage v = self._values; + + uint256 len = length(self); + for (uint256 i = 0; i < len; ++i) { + delete self._positions[_hash(v[i])]; + } + assembly ("memory-safe") { + sstore(v.slot, 0) + } + } + + /** + * @dev Returns true if the value is in the set. O(1). + */ + function contains(Bytes32x2Set storage self, bytes32[2] memory value) internal view returns (bool) { + return self._positions[_hash(value)] != 0; + } + + /** + * @dev Returns the number of values on the set. O(1). + */ + function length(Bytes32x2Set storage self) internal view returns (uint256) { + return self._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32x2Set storage self, uint256 index) internal view returns (bytes32[2] memory) { + return self._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(Bytes32x2Set storage self) internal view returns (bytes32[2][] memory) { + return self._values; + } + + function _hash(bytes32[2] memory value) private pure returns (bytes32) { + return Hashes.efficientKeccak256(value[0], value[1]); + } } diff --git a/contracts/utils/structs/EnumerableSetExtended.sol b/contracts/utils/structs/EnumerableSetExtended.sol deleted file mode 100644 index a5ba388a74f..00000000000 --- a/contracts/utils/structs/EnumerableSetExtended.sol +++ /dev/null @@ -1,422 +0,0 @@ -// SPDX-License-Identifier: MIT -// This file was procedurally generated from scripts/generate/templates/EnumerableSetExtended.js. - -pragma solidity ^0.8.20; - -import {Hashes} from "../cryptography/Hashes.sol"; - -/** - * @dev Library for managing - * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of non-value - * types. - * - * Sets have the following properties: - * - * - Elements are added, removed, and checked for existence in constant time - * (O(1)). - * - Elements are enumerated in O(n). No guarantees are made on the ordering. - * - Set can be cleared (all elements removed) in O(n). - * - * ```solidity - * contract Example { - * // Add the library methods - * using EnumerableSetExtended for EnumerableSetExtended.StringSet; - * - * // Declare a set state variable - * EnumerableSetExtended.StringSet private mySet; - * } - * ``` - * - * Sets of type `string` (`StringSet`), `bytes` (`BytesSet`) and - * `bytes32[2]` (`Bytes32x2Set`) are supported. - * - * [WARNING] - * ==== - * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure - * unusable. - * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. - * - * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an - * array of EnumerableSet. - * ==== - * - * NOTE: This is an extension of {EnumerableSet}. - */ -library EnumerableSetExtended { - struct StringSet { - // Storage of set values - string[] _values; - // Position is the index of the value in the `values` array plus 1. - // Position 0 is used to mean a value is not in the set. - mapping(string value => uint256) _positions; - } - - /** - * @dev Add a value to a set. O(1). - * - * Returns true if the value was added to the set, that is if it was not - * already present. - */ - function add(StringSet storage self, string memory value) internal returns (bool) { - if (!contains(self, value)) { - self._values.push(value); - // The value is stored at length-1, but we add 1 to all indexes - // and use 0 as a sentinel value - self._positions[value] = self._values.length; - return true; - } else { - return false; - } - } - - /** - * @dev Removes a value from a set. O(1). - * - * Returns true if the value was removed from the set, that is if it was - * present. - */ - function remove(StringSet storage self, string memory value) internal returns (bool) { - // We cache the value's position to prevent multiple reads from the same storage slot - uint256 position = self._positions[value]; - - if (position != 0) { - // Equivalent to contains(self, value) - // To delete an element from the _values array in O(1), we swap the element to delete with the last one in - // the array, and then remove the last element (sometimes called as 'swap and pop'). - // This modifies the order of the array, as noted in {at}. - - uint256 valueIndex = position - 1; - uint256 lastIndex = self._values.length - 1; - - if (valueIndex != lastIndex) { - string memory lastValue = self._values[lastIndex]; - - // Move the lastValue to the index where the value to delete is - self._values[valueIndex] = lastValue; - // Update the tracked position of the lastValue (that was just moved) - self._positions[lastValue] = position; - } - - // Delete the slot where the moved value was stored - self._values.pop(); - - // Delete the tracked position for the deleted slot - delete self._positions[value]; - - return true; - } else { - return false; - } - } - - /** - * @dev Removes all the values from a set. O(n). - * - * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the - * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. - */ - function clear(StringSet storage set) internal { - uint256 len = length(set); - for (uint256 i = 0; i < len; ++i) { - delete set._positions[set._values[i]]; - } - // Replace when these are available in Arrays.sol - string[] storage array = set._values; - assembly ("memory-safe") { - sstore(array.slot, 0) - } - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function contains(StringSet storage self, string memory value) internal view returns (bool) { - return self._positions[value] != 0; - } - - /** - * @dev Returns the number of values on the set. O(1). - */ - function length(StringSet storage self) internal view returns (uint256) { - return self._values.length; - } - - /** - * @dev Returns the value stored at position `index` in the set. O(1). - * - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function at(StringSet storage self, uint256 index) internal view returns (string memory) { - return self._values[index]; - } - - /** - * @dev Return the entire set in an array - * - * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed - * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that - * this function has an unbounded cost, and using it as part of a state-changing function may render the function - * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. - */ - function values(StringSet storage self) internal view returns (string[] memory) { - return self._values; - } - - struct BytesSet { - // Storage of set values - bytes[] _values; - // Position is the index of the value in the `values` array plus 1. - // Position 0 is used to mean a value is not in the set. - mapping(bytes value => uint256) _positions; - } - - /** - * @dev Add a value to a set. O(1). - * - * Returns true if the value was added to the set, that is if it was not - * already present. - */ - function add(BytesSet storage self, bytes memory value) internal returns (bool) { - if (!contains(self, value)) { - self._values.push(value); - // The value is stored at length-1, but we add 1 to all indexes - // and use 0 as a sentinel value - self._positions[value] = self._values.length; - return true; - } else { - return false; - } - } - - /** - * @dev Removes a value from a set. O(1). - * - * Returns true if the value was removed from the set, that is if it was - * present. - */ - function remove(BytesSet storage self, bytes memory value) internal returns (bool) { - // We cache the value's position to prevent multiple reads from the same storage slot - uint256 position = self._positions[value]; - - if (position != 0) { - // Equivalent to contains(self, value) - // To delete an element from the _values array in O(1), we swap the element to delete with the last one in - // the array, and then remove the last element (sometimes called as 'swap and pop'). - // This modifies the order of the array, as noted in {at}. - - uint256 valueIndex = position - 1; - uint256 lastIndex = self._values.length - 1; - - if (valueIndex != lastIndex) { - bytes memory lastValue = self._values[lastIndex]; - - // Move the lastValue to the index where the value to delete is - self._values[valueIndex] = lastValue; - // Update the tracked position of the lastValue (that was just moved) - self._positions[lastValue] = position; - } - - // Delete the slot where the moved value was stored - self._values.pop(); - - // Delete the tracked position for the deleted slot - delete self._positions[value]; - - return true; - } else { - return false; - } - } - - /** - * @dev Removes all the values from a set. O(n). - * - * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the - * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. - */ - function clear(BytesSet storage set) internal { - uint256 len = length(set); - for (uint256 i = 0; i < len; ++i) { - delete set._positions[set._values[i]]; - } - // Replace when these are available in Arrays.sol - bytes[] storage array = set._values; - assembly ("memory-safe") { - sstore(array.slot, 0) - } - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function contains(BytesSet storage self, bytes memory value) internal view returns (bool) { - return self._positions[value] != 0; - } - - /** - * @dev Returns the number of values on the set. O(1). - */ - function length(BytesSet storage self) internal view returns (uint256) { - return self._values.length; - } - - /** - * @dev Returns the value stored at position `index` in the set. O(1). - * - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function at(BytesSet storage self, uint256 index) internal view returns (bytes memory) { - return self._values[index]; - } - - /** - * @dev Return the entire set in an array - * - * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed - * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that - * this function has an unbounded cost, and using it as part of a state-changing function may render the function - * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. - */ - function values(BytesSet storage self) internal view returns (bytes[] memory) { - return self._values; - } - - struct Bytes32x2Set { - // Storage of set values - bytes32[2][] _values; - // Position is the index of the value in the `values` array plus 1. - // Position 0 is used to mean a value is not in the set. - mapping(bytes32 valueHash => uint256) _positions; - } - - /** - * @dev Add a value to a set. O(1). - * - * Returns true if the value was added to the set, that is if it was not - * already present. - */ - function add(Bytes32x2Set storage self, bytes32[2] memory value) internal returns (bool) { - if (!contains(self, value)) { - self._values.push(value); - // The value is stored at length-1, but we add 1 to all indexes - // and use 0 as a sentinel value - self._positions[_hash(value)] = self._values.length; - return true; - } else { - return false; - } - } - - /** - * @dev Removes a value from a set. O(1). - * - * Returns true if the value was removed from the set, that is if it was - * present. - */ - function remove(Bytes32x2Set storage self, bytes32[2] memory value) internal returns (bool) { - // We cache the value's position to prevent multiple reads from the same storage slot - bytes32 valueHash = _hash(value); - uint256 position = self._positions[valueHash]; - - if (position != 0) { - // Equivalent to contains(self, value) - // To delete an element from the _values array in O(1), we swap the element to delete with the last one in - // the array, and then remove the last element (sometimes called as 'swap and pop'). - // This modifies the order of the array, as noted in {at}. - - uint256 valueIndex = position - 1; - uint256 lastIndex = self._values.length - 1; - - if (valueIndex != lastIndex) { - bytes32[2] memory lastValue = self._values[lastIndex]; - - // Move the lastValue to the index where the value to delete is - self._values[valueIndex] = lastValue; - // Update the tracked position of the lastValue (that was just moved) - self._positions[_hash(lastValue)] = position; - } - - // Delete the slot where the moved value was stored - self._values.pop(); - - // Delete the tracked position for the deleted slot - delete self._positions[valueHash]; - - return true; - } else { - return false; - } - } - - /** - * @dev Removes all the values from a set. O(n). - * - * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the - * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. - */ - function clear(Bytes32x2Set storage self) internal { - bytes32[2][] storage v = self._values; - - uint256 len = length(self); - for (uint256 i = 0; i < len; ++i) { - delete self._positions[_hash(v[i])]; - } - assembly ("memory-safe") { - sstore(v.slot, 0) - } - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function contains(Bytes32x2Set storage self, bytes32[2] memory value) internal view returns (bool) { - return self._positions[_hash(value)] != 0; - } - - /** - * @dev Returns the number of values on the set. O(1). - */ - function length(Bytes32x2Set storage self) internal view returns (uint256) { - return self._values.length; - } - - /** - * @dev Returns the value stored at position `index` in the set. O(1). - * - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function at(Bytes32x2Set storage self, uint256 index) internal view returns (bytes32[2] memory) { - return self._values[index]; - } - - /** - * @dev Return the entire set in an array - * - * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed - * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that - * this function has an unbounded cost, and using it as part of a state-changing function may render the function - * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. - */ - function values(Bytes32x2Set storage self) internal view returns (bytes32[2][] memory) { - return self._values; - } - - function _hash(bytes32[2] memory value) private pure returns (bytes32) { - return Hashes.efficientKeccak256(value[0], value[1]); - } -} diff --git a/scripts/generate/run.js b/scripts/generate/run.js index 6779c93f44b..b06685adee7 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -// const cp = require('child_process'); +const cp = require('child_process'); const fs = require('fs'); const path = require('path'); const format = require('./format-lines'); @@ -27,7 +27,7 @@ function generateFromTemplate(file, template, outputPrefix = '') { ); fs.writeFileSync(output, content); - // cp.execFileSync('prettier', ['--write', output]); + cp.execFileSync('prettier', ['--write', output]); } // Contracts diff --git a/scripts/generate/templates/Enumerable.opts.js b/scripts/generate/templates/Enumerable.opts.js index cad0f4d7908..611e969d1d6 100644 --- a/scripts/generate/templates/Enumerable.opts.js +++ b/scripts/generate/templates/Enumerable.opts.js @@ -1,23 +1,6 @@ const { capitalize, mapValues } = require('../../helpers'); -const mapType = str => (str == 'uint256' ? 'Uint' : capitalize(str)); - -const formatSetType = type => ({ name: `${mapType(type)}Set`, type }); - -const SET_TYPES = ['bytes32', 'address', 'uint256'].map(formatSetType); - -const formatMapType = (keyType, valueType) => ({ - name: `${mapType(keyType)}To${mapType(valueType)}Map`, - keyType, - valueType, -}); - -const MAP_TYPES = ['uint256', 'address', 'bytes32'] - .flatMap((key, _, array) => array.map(value => [key, value])) - .slice(0, -1) // remove bytes32 → byte32 (last one) that is already defined - .map(args => formatMapType(...args)); - -const extendedTypeDescr = ({ type, size = 0, memory = false }) => { +const typeDescr = ({ type, size = 0, memory = false }) => { memory |= size > 0; const name = [type == 'uint256' ? 'Uint' : capitalize(type), size].filter(Boolean).join('x'); @@ -27,38 +10,46 @@ const extendedTypeDescr = ({ type, size = 0, memory = false }) => { return { name, type: typeFull, typeLoc, base, size, memory }; }; -const toExtendedSetTypeDescr = value => ({ name: value.name + 'Set', value }); +const toSetTypeDescr = value => ({ + name: value.name + 'Set', + value, +}); -const toExtendedMapTypeDescr = ({ key, value }) => ({ +const toMapTypeDescr = ({ key, value }) => ({ name: `${key.name}To${value.name}Map`, - keySet: toExtendedSetTypeDescr(key), + keySet: toSetTypeDescr(key), key, value, }); -const EXTENDED_SET_TYPES = [ +const SET_TYPES = [ + { type: 'bytes32' }, + { type: 'address' }, + { type: 'uint256' }, { type: 'bytes32', size: 2 }, { type: 'string', memory: true }, { type: 'bytes', memory: true }, ] - .map(extendedTypeDescr) - .map(toExtendedSetTypeDescr); - -const EXTENDED_MAP_TYPES = [ - { key: { type: 'bytes', memory: true }, value: { type: 'uint256' } }, - { key: { type: 'string', memory: true }, value: { type: 'string', memory: true } }, -] - .map(entry => mapValues(entry, extendedTypeDescr)) - .map(toExtendedMapTypeDescr); + .map(typeDescr) + .map(toSetTypeDescr); + +const MAP_TYPES = [] + .concat( + // value type maps + ['uint256', 'address', 'bytes32'] + .flatMap((keyType, _, array) => array.map(valueType => ({ key: { type: keyType }, value: { type: valueType } }))) + .slice(0, -1), // remove bytes32 → byte32 (last one) that is already defined + // non-value type maps + { key: { type: 'bytes', memory: true }, value: { type: 'uint256' } }, + { key: { type: 'string', memory: true }, value: { type: 'string', memory: true } }, + ) + .map(entry => mapValues(entry, typeDescr)) + .map(toMapTypeDescr); module.exports = { SET_TYPES, MAP_TYPES, - EXTENDED_SET_TYPES, - EXTENDED_MAP_TYPES, - formatSetType, - formatMapType, - extendedTypeDescr, - toExtendedSetTypeDescr, - toExtendedMapTypeDescr, + typeDescr, + toSetTypeDescr, + toMapTypeDescr, }; diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index 8879c7a4b11..7e52b2eb3c6 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -40,6 +40,8 @@ import {EnumerableSet} from "./EnumerableSet.sol"; * - \`address -> address\` (\`AddressToAddressMap\`) since v5.1.0 * - \`address -> bytes32\` (\`AddressToBytes32Map\`) since v5.1.0 * - \`bytes32 -> address\` (\`Bytes32ToAddressMap\`) since v5.1.0 + * - \`bytes -> uint256\` (\`BytesToUintMap\`) since v5.4.0 + * - \`string -> string\` (\`StringToStringMap\`) since v5.4.0 * * [WARNING] * ==== @@ -176,7 +178,7 @@ function keys(Bytes32ToBytes32Map storage map) internal view returns (bytes32[] } `; -const customMap = ({ name, keyType, valueType }) => `\ +const customMap = ({ name, key, value }) => `\ // ${name} struct ${name} { @@ -190,8 +192,8 @@ struct ${name} { * Returns true if the key was added to the map, that is if it was not * already present. */ -function set(${name} storage map, ${keyType} key, ${valueType} value) internal returns (bool) { - return set(map._inner, ${toBytes32(keyType, 'key')}, ${toBytes32(valueType, 'value')}); +function set(${name} storage map, ${key.type} key, ${value.type} value) internal returns (bool) { + return set(map._inner, ${toBytes32(key.type, 'key')}, ${toBytes32(value.type, 'value')}); } /** @@ -199,8 +201,8 @@ function set(${name} storage map, ${keyType} key, ${valueType} value) internal r * * Returns true if the key was removed from the map, that is if it was present. */ -function remove(${name} storage map, ${keyType} key) internal returns (bool) { - return remove(map._inner, ${toBytes32(keyType, 'key')}); +function remove(${name} storage map, ${key.type} key) internal returns (bool) { + return remove(map._inner, ${toBytes32(key.type, 'key')}); } /** @@ -216,8 +218,8 @@ function clear(${name} storage map) internal { /** * @dev Returns true if the key is in the map. O(1). */ -function contains(${name} storage map, ${keyType} key) internal view returns (bool) { - return contains(map._inner, ${toBytes32(keyType, 'key')}); +function contains(${name} storage map, ${key.type} key) internal view returns (bool) { + return contains(map._inner, ${toBytes32(key.type, 'key')}); } /** @@ -236,18 +238,18 @@ function length(${name} storage map) internal view returns (uint256) { * * - \`index\` must be strictly less than {length}. */ -function at(${name} storage map, uint256 index) internal view returns (${keyType} key, ${valueType} value) { +function at(${name} storage map, uint256 index) internal view returns (${key.type} key, ${value.type} value) { (bytes32 atKey, bytes32 val) = at(map._inner, index); - return (${fromBytes32(keyType, 'atKey')}, ${fromBytes32(valueType, 'val')}); + return (${fromBytes32(key.type, 'atKey')}, ${fromBytes32(value.type, 'val')}); } /** * @dev Tries to returns the value associated with \`key\`. O(1). * Does not revert if \`key\` is not in the map. */ -function tryGet(${name} storage map, ${keyType} key) internal view returns (bool exists, ${valueType} value) { - (bool success, bytes32 val) = tryGet(map._inner, ${toBytes32(keyType, 'key')}); - return (success, ${fromBytes32(valueType, 'val')}); +function tryGet(${name} storage map, ${key.type} key) internal view returns (bool exists, ${value.type} value) { + (bool success, bytes32 val) = tryGet(map._inner, ${toBytes32(key.type, 'key')}); + return (success, ${fromBytes32(value.type, 'val')}); } /** @@ -257,8 +259,8 @@ function tryGet(${name} storage map, ${keyType} key) internal view returns (bool * * - \`key\` must be in the map. */ -function get(${name} storage map, ${keyType} key) internal view returns (${valueType}) { - return ${fromBytes32(valueType, `get(map._inner, ${toBytes32(keyType, 'key')})`)}; +function get(${name} storage map, ${key.type} key) internal view returns (${value.type}) { + return ${fromBytes32(value.type, `get(map._inner, ${toBytes32(key.type, 'key')})`)}; } /** @@ -269,9 +271,9 @@ function get(${name} storage map, ${keyType} key) internal view returns (${value * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. */ -function keys(${name} storage map) internal view returns (${keyType}[] memory) { +function keys(${name} storage map) internal view returns (${key.type}[] memory) { bytes32[] memory store = keys(map._inner); - ${keyType}[] memory result; + ${key.type}[] memory result; assembly ("memory-safe") { result := store @@ -281,16 +283,137 @@ function keys(${name} storage map) internal view returns (${keyType}[] memory) { } `; +const memoryMap = ({ name, keySet, key, value }) => `\ +/** + * @dev Query for a nonexistent map key. + */ +error EnumerableMapNonexistent${key.name}Key(${key.type} key); + +struct ${name} { + // Storage of keys + EnumerableSet.${keySet.name} _keys; + mapping(${key.type} key => ${value.type}) _values; +} + +/** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ +function set(${name} storage map, ${key.typeLoc} key, ${value.typeLoc} value) internal returns (bool) { + map._values[key] = value; + return map._keys.add(key); +} + +/** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ +function remove(${name} storage map, ${key.typeLoc} key) internal returns (bool) { + delete map._values[key]; + return map._keys.remove(key); +} + +/** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ +function clear(${name} storage map) internal { + uint256 len = length(map); + for (uint256 i = 0; i < len; ++i) { + delete map._values[map._keys.at(i)]; + } + map._keys.clear(); +} + +/** + * @dev Returns true if the key is in the map. O(1). + */ +function contains(${name} storage map, ${key.typeLoc} key) internal view returns (bool) { + return map._keys.contains(key); +} + +/** + * @dev Returns the number of key-value pairs in the map. O(1). + */ +function length(${name} storage map) internal view returns (uint256) { + return map._keys.length(); +} + +/** + * @dev Returns the key-value pair stored at position \`index\` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - \`index\` must be strictly less than {length}. + */ +function at( + ${name} storage map, + uint256 index +) internal view returns (${key.typeLoc} key, ${value.typeLoc} value) { + key = map._keys.at(index); + value = map._values[key]; +} + +/** + * @dev Tries to returns the value associated with \`key\`. O(1). + * Does not revert if \`key\` is not in the map. + */ +function tryGet( + ${name} storage map, + ${key.typeLoc} key +) internal view returns (bool exists, ${value.typeLoc} value) { + value = map._values[key]; + exists = ${value.memory ? 'bytes(value).length != 0' : `value != ${value.type}(0)`} || contains(map, key); +} + +/** + * @dev Returns the value associated with \`key\`. O(1). + * + * Requirements: + * + * - \`key\` must be in the map. + */ +function get(${name} storage map, ${key.typeLoc} key) internal view returns (${value.typeLoc} value) { + bool exists; + (exists, value) = tryGet(map, key); + if (!exists) { + revert EnumerableMapNonexistent${key.name}Key(key); + } +} + +/** + * @dev Return the an array containing all the keys + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. + */ +function keys(${name} storage map) internal view returns (${key.type}[] memory) { + return map._keys.values(); +} +`; + // GENERATE module.exports = format( header.trimEnd(), 'library EnumerableMap {', format( [].concat( - 'using EnumerableSet for EnumerableSet.Bytes32Set;', + 'using EnumerableSet for *;', '', defaultMap, - MAP_TYPES.map(details => customMap(details)), + MAP_TYPES.filter(({ key, value }) => !(key.memory || value.memory)).map(customMap), + MAP_TYPES.filter(({ key, value }) => key.memory || value.memory).map(memoryMap), ), ).trimEnd(), '}', diff --git a/scripts/generate/templates/EnumerableMapExtended.js b/scripts/generate/templates/EnumerableMapExtended.js deleted file mode 100644 index b1b55278974..00000000000 --- a/scripts/generate/templates/EnumerableMapExtended.js +++ /dev/null @@ -1,179 +0,0 @@ -const format = require('../format-lines'); -const { EXTENDED_SET_TYPES, EXTENDED_MAP_TYPES } = require('./Enumerable.opts'); - -const header = `\ -pragma solidity ^0.8.20; - -import {EnumerableSet} from "./EnumerableSet.sol"; -import {EnumerableSetExtended} from "./EnumerableSetExtended.sol"; - -/** - * @dev Library for managing an enumerable variant of Solidity's - * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[\`mapping\`] - * type for non-value types as keys. - * - * Maps have the following properties: - * - * - Entries are added, removed, and checked for existence in constant time - * (O(1)). - * - Entries are enumerated in O(n). No guarantees are made on the ordering. - * - Map can be cleared (all entries removed) in O(n). - * - * \`\`\`solidity - * contract Example { - * // Add the library methods - * using EnumerableMapExtended for EnumerableMapExtended.BytesToUintMap; - * - * // Declare a set state variable - * EnumerableMapExtended.BytesToUintMap private myMap; - * } - * \`\`\` - * - * The following map types are supported: - * - * - \`bytes -> uint256\` (\`BytesToUintMap\`) - * - \`string -> string\` (\`StringToStringMap\`) - * - * [WARNING] - * ==== - * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure - * unusable. - * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. - * - * In order to clean an EnumerableMap, you can either remove all elements one by one or create a fresh instance using an - * array of EnumerableMap. - * ==== - * - * NOTE: Extensions of {EnumerableMap} - */ -`; - -const map = ({ name, keySet, key, value }) => `\ -/** - * @dev Query for a nonexistent map key. - */ -error EnumerableMapNonexistent${key.name}Key(${key.type} key); - -struct ${name} { - // Storage of keys - ${EXTENDED_SET_TYPES.some(el => el.name == keySet.name) ? 'EnumerableSetExtended' : 'EnumerableSet'}.${keySet.name} _keys; - mapping(${key.type} key => ${value.type}) _values; -} - -/** - * @dev Adds a key-value pair to a map, or updates the value for an existing - * key. O(1). - * - * Returns true if the key was added to the map, that is if it was not - * already present. - */ -function set(${name} storage map, ${key.typeLoc} key, ${value.typeLoc} value) internal returns (bool) { - map._values[key] = value; - return map._keys.add(key); -} - -/** - * @dev Removes a key-value pair from a map. O(1). - * - * Returns true if the key was removed from the map, that is if it was present. - */ -function remove(${name} storage map, ${key.typeLoc} key) internal returns (bool) { - delete map._values[key]; - return map._keys.remove(key); -} - -/** - * @dev Removes all the entries from a map. O(n). - * - * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the - * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. - */ -function clear(${name} storage map) internal { - uint256 len = length(map); - for (uint256 i = 0; i < len; ++i) { - delete map._values[map._keys.at(i)]; - } - map._keys.clear(); -} - -/** - * @dev Returns true if the key is in the map. O(1). - */ -function contains(${name} storage map, ${key.typeLoc} key) internal view returns (bool) { - return map._keys.contains(key); -} - -/** - * @dev Returns the number of key-value pairs in the map. O(1). - */ -function length(${name} storage map) internal view returns (uint256) { - return map._keys.length(); -} - -/** - * @dev Returns the key-value pair stored at position \`index\` in the map. O(1). - * - * Note that there are no guarantees on the ordering of entries inside the - * array, and it may change when more entries are added or removed. - * - * Requirements: - * - * - \`index\` must be strictly less than {length}. - */ -function at( - ${name} storage map, - uint256 index -) internal view returns (${key.typeLoc} key, ${value.typeLoc} value) { - key = map._keys.at(index); - value = map._values[key]; -} - -/** - * @dev Tries to returns the value associated with \`key\`. O(1). - * Does not revert if \`key\` is not in the map. - */ -function tryGet( - ${name} storage map, - ${key.typeLoc} key -) internal view returns (bool exists, ${value.typeLoc} value) { - value = map._values[key]; - exists = ${value.memory ? 'bytes(value).length != 0' : `value != ${value.type}(0)`} || contains(map, key); -} - -/** - * @dev Returns the value associated with \`key\`. O(1). - * - * Requirements: - * - * - \`key\` must be in the map. - */ -function get(${name} storage map, ${key.typeLoc} key) internal view returns (${value.typeLoc} value) { - bool exists; - (exists, value) = tryGet(map, key); - if (!exists) { - revert EnumerableMapNonexistent${key.name}Key(key); - } -} - -/** - * @dev Return the an array containing all the keys - * - * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed - * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that - * this function has an unbounded cost, and using it as part of a state-changing function may render the function - * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. - */ -function keys(${name} storage map) internal view returns (${key.type}[] memory) { - return map._keys.values(); -} -`; - -// GENERATE -module.exports = format( - header.trimEnd(), - 'library EnumerableMapExtended {', - format( - [].concat('using EnumerableSet for *;', 'using EnumerableSetExtended for *;', '', EXTENDED_MAP_TYPES.map(map)), - ).trimEnd(), - '}', -); diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index 26263ba1889..8f04474a58d 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -6,6 +6,7 @@ const header = `\ pragma solidity ^0.8.20; import {Arrays} from "../Arrays.sol"; +import {Hashes} from "../cryptography/Hashes.sol"; /** * @dev Library for managing @@ -44,6 +45,7 @@ import {Arrays} from "../Arrays.sol"; */ `; +// NOTE: this should be deprecated in favor of a more native construction in v6.0 const defaultSet = `\ // To implement this library for multiple types with as little code // repetition as possible, we write it in terms of a generic Set type with @@ -175,7 +177,8 @@ function _values(Set storage set) private view returns (bytes32[] memory) { } `; -const customSet = ({ name, type }) => `\ +// NOTE: this should be deprecated in favor of a more native construction in v6.0 +const customSet = ({ name, value: { type } }) => `\ // ${name} struct ${name} { @@ -260,6 +263,266 @@ function values(${name} storage set) internal view returns (${type}[] memory) { } `; +// NOTE: this should be used for all value-type sets in v6.0 +const set = ({ name, value }) => `\ +struct ${name} { + // Storage of set values + ${value.type}[] _values; + // Position is the index of the value in the \`values\` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(${value.type} value => uint256) _positions; +} + +/** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ +function add(${name} storage self, ${value.type} memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[value] = self._values.length; + return true; + } else { + return false; + } +} + +/** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ +function remove(${name} storage self, ${value.type} memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + uint256 position = self._positions[value]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + ${value.type} memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[lastValue] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[value]; + + return true; + } else { + return false; + } +} + +/** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ +function clear(${name} storage set) internal { + uint256 len = length(set); + for (uint256 i = 0; i < len; ++i) { + delete set._positions[set._values[i]]; + } + // Replace when these are available in Arrays.sol + ${value.type}[] storage array = set._values; + assembly ("memory-safe") { + sstore(array.slot, 0) + } +} + +/** + * @dev Returns true if the value is in the set. O(1). + */ +function contains(${name} storage self, ${value.type} memory value) internal view returns (bool) { + return self._positions[value] != 0; +} + +/** + * @dev Returns the number of values on the set. O(1). + */ +function length(${name} storage self) internal view returns (uint256) { + return self._values.length; +} + +/** + * @dev Returns the value stored at position \`index\` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - \`index\` must be strictly less than {length}. + */ +function at(${name} storage self, uint256 index) internal view returns (${value.type} memory) { + return self._values[index]; +} + +/** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ +function values(${name} storage self) internal view returns (${value.type}[] memory) { + return self._values; +} +`; + +const arraySet = ({ name, value }) => `\ +struct ${name} { + // Storage of set values + ${value.type}[] _values; + // Position is the index of the value in the \`values\` array plus 1. + // Position 0 is used to mean a value is not in the set. + mapping(bytes32 valueHash => uint256) _positions; +} + +/** + * @dev Add a value to a set. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ +function add(${name} storage self, ${value.type} memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[_hash(value)] = self._values.length; + return true; + } else { + return false; + } +} + +/** + * @dev Removes a value from a set. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ +function remove(${name} storage self, ${value.type} memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + bytes32 valueHash = _hash(value); + uint256 position = self._positions[valueHash]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + ${value.type} memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[_hash(lastValue)] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[valueHash]; + + return true; + } else { + return false; + } +} + +/** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ +function clear(${name} storage self) internal { + ${value.type}[] storage v = self._values; + + uint256 len = length(self); + for (uint256 i = 0; i < len; ++i) { + delete self._positions[_hash(v[i])]; + } + assembly ("memory-safe") { + sstore(v.slot, 0) + } +} + +/** + * @dev Returns true if the value is in the set. O(1). + */ +function contains(${name} storage self, ${value.type} memory value) internal view returns (bool) { + return self._positions[_hash(value)] != 0; +} + +/** + * @dev Returns the number of values on the set. O(1). + */ +function length(${name} storage self) internal view returns (uint256) { + return self._values.length; +} + +/** + * @dev Returns the value stored at position \`index\` in the set. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - \`index\` must be strictly less than {length}. + */ +function at(${name} storage self, uint256 index) internal view returns (${value.type} memory) { + return self._values[index]; +} + +/** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ +function values(${name} storage self) internal view returns (${value.type}[] memory) { + return self._values; +} +`; + +const hashes = `\ +function _hash(bytes32[2] memory value) private pure returns (bytes32) { + return Hashes.efficientKeccak256(value[0], value[1]); +} +`; + // GENERATE module.exports = format( header.trimEnd(), @@ -267,7 +530,10 @@ module.exports = format( format( [].concat( defaultSet, - SET_TYPES.map(details => customSet(details)), + SET_TYPES.filter(({ value }) => !value.memory).map(customSet), + SET_TYPES.filter(({ value }) => value.memory && value.size == 0).map(set), + SET_TYPES.filter(({ value }) => value.memory && value.size > 0).map(arraySet), + hashes, ), ).trimEnd(), '}', diff --git a/scripts/generate/templates/EnumerableSetExtended.js b/scripts/generate/templates/EnumerableSetExtended.js deleted file mode 100644 index 73c4b446160..00000000000 --- a/scripts/generate/templates/EnumerableSetExtended.js +++ /dev/null @@ -1,319 +0,0 @@ -const format = require('../format-lines'); -const { EXTENDED_SET_TYPES } = require('./Enumerable.opts'); - -const header = `\ -pragma solidity ^0.8.20; - -import {Hashes} from "../cryptography/Hashes.sol"; - -/** - * @dev Library for managing - * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of non-value - * types. - * - * Sets have the following properties: - * - * - Elements are added, removed, and checked for existence in constant time - * (O(1)). - * - Elements are enumerated in O(n). No guarantees are made on the ordering. - * - Set can be cleared (all elements removed) in O(n). - * - * \`\`\`solidity - * contract Example { - * // Add the library methods - * using EnumerableSetExtended for EnumerableSetExtended.StringSet; - * - * // Declare a set state variable - * EnumerableSetExtended.StringSet private mySet; - * } - * \`\`\` - * - * Sets of type \`string\` (\`StringSet\`), \`bytes\` (\`BytesSet\`) and - * \`bytes32[2]\` (\`Bytes32x2Set\`) are supported. - * - * [WARNING] - * ==== - * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure - * unusable. - * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info. - * - * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an - * array of EnumerableSet. - * ==== - * - * NOTE: This is an extension of {EnumerableSet}. - */ -`; - -const set = ({ name, value }) => `\ -struct ${name} { - // Storage of set values - ${value.type}[] _values; - // Position is the index of the value in the \`values\` array plus 1. - // Position 0 is used to mean a value is not in the set. - mapping(${value.type} value => uint256) _positions; -} - -/** - * @dev Add a value to a set. O(1). - * - * Returns true if the value was added to the set, that is if it was not - * already present. - */ -function add(${name} storage self, ${value.type} memory value) internal returns (bool) { - if (!contains(self, value)) { - self._values.push(value); - // The value is stored at length-1, but we add 1 to all indexes - // and use 0 as a sentinel value - self._positions[value] = self._values.length; - return true; - } else { - return false; - } -} - -/** - * @dev Removes a value from a set. O(1). - * - * Returns true if the value was removed from the set, that is if it was - * present. - */ -function remove(${name} storage self, ${value.type} memory value) internal returns (bool) { - // We cache the value's position to prevent multiple reads from the same storage slot - uint256 position = self._positions[value]; - - if (position != 0) { - // Equivalent to contains(self, value) - // To delete an element from the _values array in O(1), we swap the element to delete with the last one in - // the array, and then remove the last element (sometimes called as 'swap and pop'). - // This modifies the order of the array, as noted in {at}. - - uint256 valueIndex = position - 1; - uint256 lastIndex = self._values.length - 1; - - if (valueIndex != lastIndex) { - ${value.type} memory lastValue = self._values[lastIndex]; - - // Move the lastValue to the index where the value to delete is - self._values[valueIndex] = lastValue; - // Update the tracked position of the lastValue (that was just moved) - self._positions[lastValue] = position; - } - - // Delete the slot where the moved value was stored - self._values.pop(); - - // Delete the tracked position for the deleted slot - delete self._positions[value]; - - return true; - } else { - return false; - } -} - -/** - * @dev Removes all the values from a set. O(n). - * - * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the - * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. - */ -function clear(${name} storage set) internal { - uint256 len = length(set); - for (uint256 i = 0; i < len; ++i) { - delete set._positions[set._values[i]]; - } - // Replace when these are available in Arrays.sol - ${value.type}[] storage array = set._values; - assembly ("memory-safe") { - sstore(array.slot, 0) - } -} - -/** - * @dev Returns true if the value is in the set. O(1). - */ -function contains(${name} storage self, ${value.type} memory value) internal view returns (bool) { - return self._positions[value] != 0; -} - -/** - * @dev Returns the number of values on the set. O(1). - */ -function length(${name} storage self) internal view returns (uint256) { - return self._values.length; -} - -/** - * @dev Returns the value stored at position \`index\` in the set. O(1). - * - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - \`index\` must be strictly less than {length}. - */ -function at(${name} storage self, uint256 index) internal view returns (${value.type} memory) { - return self._values[index]; -} - -/** - * @dev Return the entire set in an array - * - * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed - * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that - * this function has an unbounded cost, and using it as part of a state-changing function may render the function - * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. - */ -function values(${name} storage self) internal view returns (${value.type}[] memory) { - return self._values; -} -`; - -const arraySet = ({ name, value }) => `\ -struct ${name} { - // Storage of set values - ${value.type}[] _values; - // Position is the index of the value in the \`values\` array plus 1. - // Position 0 is used to mean a value is not in the set. - mapping(bytes32 valueHash => uint256) _positions; -} - -/** - * @dev Add a value to a set. O(1). - * - * Returns true if the value was added to the set, that is if it was not - * already present. - */ -function add(${name} storage self, ${value.type} memory value) internal returns (bool) { - if (!contains(self, value)) { - self._values.push(value); - // The value is stored at length-1, but we add 1 to all indexes - // and use 0 as a sentinel value - self._positions[_hash(value)] = self._values.length; - return true; - } else { - return false; - } -} - -/** - * @dev Removes a value from a set. O(1). - * - * Returns true if the value was removed from the set, that is if it was - * present. - */ -function remove(${name} storage self, ${value.type} memory value) internal returns (bool) { - // We cache the value's position to prevent multiple reads from the same storage slot - bytes32 valueHash = _hash(value); - uint256 position = self._positions[valueHash]; - - if (position != 0) { - // Equivalent to contains(self, value) - // To delete an element from the _values array in O(1), we swap the element to delete with the last one in - // the array, and then remove the last element (sometimes called as 'swap and pop'). - // This modifies the order of the array, as noted in {at}. - - uint256 valueIndex = position - 1; - uint256 lastIndex = self._values.length - 1; - - if (valueIndex != lastIndex) { - ${value.type} memory lastValue = self._values[lastIndex]; - - // Move the lastValue to the index where the value to delete is - self._values[valueIndex] = lastValue; - // Update the tracked position of the lastValue (that was just moved) - self._positions[_hash(lastValue)] = position; - } - - // Delete the slot where the moved value was stored - self._values.pop(); - - // Delete the tracked position for the deleted slot - delete self._positions[valueHash]; - - return true; - } else { - return false; - } -} - -/** - * @dev Removes all the values from a set. O(n). - * - * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the - * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. - */ -function clear(${name} storage self) internal { - ${value.type}[] storage v = self._values; - - uint256 len = length(self); - for (uint256 i = 0; i < len; ++i) { - delete self._positions[_hash(v[i])]; - } - assembly ("memory-safe") { - sstore(v.slot, 0) - } -} - -/** - * @dev Returns true if the value is in the set. O(1). - */ -function contains(${name} storage self, ${value.type} memory value) internal view returns (bool) { - return self._positions[_hash(value)] != 0; -} - -/** - * @dev Returns the number of values on the set. O(1). - */ -function length(${name} storage self) internal view returns (uint256) { - return self._values.length; -} - -/** - * @dev Returns the value stored at position \`index\` in the set. O(1). - * - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - \`index\` must be strictly less than {length}. - */ -function at(${name} storage self, uint256 index) internal view returns (${value.type} memory) { - return self._values[index]; -} - -/** - * @dev Return the entire set in an array - * - * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed - * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that - * this function has an unbounded cost, and using it as part of a state-changing function may render the function - * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. - */ -function values(${name} storage self) internal view returns (${value.type}[] memory) { - return self._values; -} -`; - -const hashes = `\ -function _hash(bytes32[2] memory value) private pure returns (bytes32) { - return Hashes.efficientKeccak256(value[0], value[1]); -} -`; - -// GENERATE -module.exports = format( - header.trimEnd(), - 'library EnumerableSetExtended {', - format( - [].concat( - EXTENDED_SET_TYPES.filter(({ value }) => value.size == 0).map(set), - EXTENDED_SET_TYPES.filter(({ value }) => value.size > 0).map(arraySet), - hashes, - ), - ).trimEnd(), - '}', -); diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index cf8de2e9df5..11806dae675 100644 --- a/test/utils/structs/EnumerableMap.behavior.js +++ b/test/utils/structs/EnumerableMap.behavior.js @@ -176,7 +176,7 @@ function shouldBehaveLikeMap() { .withArgs( this.key?.memory || this.value?.memory ? this.keyB - : ethers.AbiCoder.defaultAbiCoder().encode([this.keyType], [this.keyB]), + : ethers.AbiCoder.defaultAbiCoder().encode([this.key.type], [this.keyB]), ); }); }); diff --git a/test/utils/structs/EnumerableMap.test.js b/test/utils/structs/EnumerableMap.test.js index d512fb32d18..7319ba38d41 100644 --- a/test/utils/structs/EnumerableMap.test.js +++ b/test/utils/structs/EnumerableMap.test.js @@ -3,43 +3,58 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { mapValues } = require('../../helpers/iterate'); const { generators } = require('../../helpers/random'); -const { MAP_TYPES, formatMapType } = require('../../../scripts/generate/templates/Enumerable.opts'); +const { MAP_TYPES, typeDescr, toMapTypeDescr } = require('../../../scripts/generate/templates/Enumerable.opts'); const { shouldBehaveLikeMap } = require('./EnumerableMap.behavior'); // Add Bytes32ToBytes32Map that must be tested but is not part of the generated types. -MAP_TYPES.unshift(formatMapType('bytes32', 'bytes32')); +MAP_TYPES.unshift(toMapTypeDescr({ key: typeDescr({ type: 'bytes32' }), value: typeDescr({ type: 'bytes32' }) })); async function fixture() { const mock = await ethers.deployContract('$EnumerableMap'); + const env = Object.fromEntries( - MAP_TYPES.map(({ name, keyType, valueType }) => [ + MAP_TYPES.map(({ name, key, value }) => [ name, { - keyType, - keys: Array.from({ length: 3 }, generators[keyType]), - values: Array.from({ length: 3 }, generators[valueType]), - zeroValue: generators[valueType].zero, + key, + value, + keys: Array.from({ length: 3 }, generators[key.type]), + values: Array.from({ length: 3 }, generators[value.type]), + zeroValue: generators[value.type].zero, methods: mapValues( - { - set: `$set(uint256,${keyType},${valueType})`, - get: `$get_EnumerableMap_${name}(uint256,${keyType})`, - tryGet: `$tryGet_EnumerableMap_${name}(uint256,${keyType})`, - remove: `$remove_EnumerableMap_${name}(uint256,${keyType})`, - clear: `$clear_EnumerableMap_${name}(uint256)`, - length: `$length_EnumerableMap_${name}(uint256)`, - at: `$at_EnumerableMap_${name}(uint256,uint256)`, - contains: `$contains_EnumerableMap_${name}(uint256,${keyType})`, - keys: `$keys_EnumerableMap_${name}(uint256)`, - }, + MAP_TYPES.filter(map => map.key.name == key.name).length == 1 + ? { + set: `$set(uint256,${key.type},${value.type})`, + get: `$get(uint256,${key.type})`, + tryGet: `$tryGet(uint256,${key.type})`, + remove: `$remove(uint256,${key.type})`, + contains: `$contains(uint256,${key.type})`, + clear: `$clear_EnumerableMap_${name}(uint256)`, + length: `$length_EnumerableMap_${name}(uint256)`, + at: `$at_EnumerableMap_${name}(uint256,uint256)`, + keys: `$keys_EnumerableMap_${name}(uint256)`, + } + : { + set: `$set(uint256,${key.type},${value.type})`, + get: `$get_EnumerableMap_${name}(uint256,${key.type})`, + tryGet: `$tryGet_EnumerableMap_${name}(uint256,${key.type})`, + remove: `$remove_EnumerableMap_${name}(uint256,${key.type})`, + contains: `$contains_EnumerableMap_${name}(uint256,${key.type})`, + clear: `$clear_EnumerableMap_${name}(uint256)`, + length: `$length_EnumerableMap_${name}(uint256)`, + at: `$at_EnumerableMap_${name}(uint256,uint256)`, + keys: `$keys_EnumerableMap_${name}(uint256)`, + }, fnSig => (...args) => mock.getFunction(fnSig)(0, ...args), ), events: { - setReturn: `return$set_EnumerableMap_${name}_${keyType}_${valueType}`, - removeReturn: `return$remove_EnumerableMap_${name}_${keyType}`, + setReturn: `return$set_EnumerableMap_${name}_${key.type}_${value.type}`, + removeReturn: `return$remove_EnumerableMap_${name}_${key.type}`, }, + error: key.memory || value.memory ? `EnumerableMapNonexistent${key.name}Key` : `EnumerableMapNonexistentKey`, }, ]), ); @@ -52,8 +67,8 @@ describe('EnumerableMap', function () { Object.assign(this, await loadFixture(fixture)); }); - for (const { name } of MAP_TYPES) { - describe(name, function () { + for (const { name, key, value } of MAP_TYPES) { + describe(`${name} (enumerable map from ${key.type} to ${value.type})`, function () { beforeEach(async function () { Object.assign(this, this.env[name]); [this.keyA, this.keyB, this.keyC] = this.keys; diff --git a/test/utils/structs/EnumerableMapExtended.test.js b/test/utils/structs/EnumerableMapExtended.test.js deleted file mode 100644 index a40b83dd12d..00000000000 --- a/test/utils/structs/EnumerableMapExtended.test.js +++ /dev/null @@ -1,66 +0,0 @@ -const { ethers } = require('hardhat'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); - -const { mapValues } = require('../../helpers/iterate'); -const { generators } = require('../../helpers/random'); -const { EXTENDED_MAP_TYPES } = require('../../../scripts/generate/templates/Enumerable.opts'); - -const { shouldBehaveLikeMap } = require('./EnumerableMap.behavior'); - -async function fixture() { - const mock = await ethers.deployContract('$EnumerableMapExtended'); - - const env = Object.fromEntries( - EXTENDED_MAP_TYPES.map(({ name, key, value }) => [ - name, - { - key, - value, - keys: Array.from({ length: 3 }, generators[key.type]), - values: Array.from({ length: 3 }, generators[value.type]), - zeroValue: generators[value.type].zero, - methods: mapValues( - { - set: `$set(uint256,${key.type},${value.type})`, - get: `$get(uint256,${key.type})`, - tryGet: `$tryGet(uint256,${key.type})`, - remove: `$remove(uint256,${key.type})`, - clear: `$clear_EnumerableMapExtended_${name}(uint256)`, - length: `$length_EnumerableMapExtended_${name}(uint256)`, - at: `$at_EnumerableMapExtended_${name}(uint256,uint256)`, - contains: `$contains(uint256,${key.type})`, - keys: `$keys_EnumerableMapExtended_${name}(uint256)`, - }, - fnSig => - (...args) => - mock.getFunction(fnSig)(0, ...args), - ), - events: { - setReturn: `return$set_EnumerableMapExtended_${name}_${key.type}_${value.type}`, - removeReturn: `return$remove_EnumerableMapExtended_${name}_${key.type}`, - }, - error: key.memory || value.memory ? `EnumerableMapNonexistent${key.name}Key` : `EnumerableMapNonexistentKey`, - }, - ]), - ); - - return { mock, env }; -} - -describe('EnumerableMapExtended', function () { - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - for (const { name, key, value } of EXTENDED_MAP_TYPES) { - describe(`${name} (enumerable map from ${key.type} to ${value.type})`, function () { - beforeEach(async function () { - Object.assign(this, this.env[name]); - [this.keyA, this.keyB, this.keyC] = this.keys; - [this.valueA, this.valueB, this.valueC] = this.values; - }); - - shouldBehaveLikeMap(); - }); - } -}); diff --git a/test/utils/structs/EnumerableSet.test.js b/test/utils/structs/EnumerableSet.test.js index f60adc103a5..e111d21975e 100644 --- a/test/utils/structs/EnumerableSet.test.js +++ b/test/utils/structs/EnumerableSet.test.js @@ -7,35 +7,38 @@ const { SET_TYPES } = require('../../../scripts/generate/templates/Enumerable.op const { shouldBehaveLikeSet } = require('./EnumerableSet.behavior'); -const getMethods = (mock, fnSigs) => { - return mapValues( +const getMethods = (mock, fnSigs) => + mapValues( fnSigs, fnSig => (...args) => mock.getFunction(fnSig)(0, ...args), ); -}; async function fixture() { const mock = await ethers.deployContract('$EnumerableSet'); const env = Object.fromEntries( - SET_TYPES.map(({ name, type }) => [ - type, + SET_TYPES.map(({ name, value }) => [ + name, { - values: Array.from({ length: 3 }, generators[type]), + value, + values: Array.from( + { length: 3 }, + value.size ? () => Array.from({ length: value.size }, generators[value.base]) : generators[value.type], + ), methods: getMethods(mock, { - add: `$add(uint256,${type})`, - remove: `$remove(uint256,${type})`, + add: `$add(uint256,${value.type})`, + remove: `$remove(uint256,${value.type})`, + contains: `$contains(uint256,${value.type})`, clear: `$clear_EnumerableSet_${name}(uint256)`, - contains: `$contains(uint256,${type})`, length: `$length_EnumerableSet_${name}(uint256)`, at: `$at_EnumerableSet_${name}(uint256,uint256)`, values: `$values_EnumerableSet_${name}(uint256)`, }), events: { - addReturn: `return$add_EnumerableSet_${name}_${type}`, - removeReturn: `return$remove_EnumerableSet_${name}_${type}`, + addReturn: `return$add_EnumerableSet_${name}_${value.type.replace(/[[\]]/g, '_')}`, + removeReturn: `return$remove_EnumerableSet_${name}_${value.type.replace(/[[\]]/g, '_')}`, }, }, ]), @@ -49,10 +52,10 @@ describe('EnumerableSet', function () { Object.assign(this, await loadFixture(fixture)); }); - for (const { type } of SET_TYPES) { - describe(type, function () { + for (const { name, value } of SET_TYPES) { + describe(`${name} (enumerable set of ${value.type})`, function () { beforeEach(function () { - Object.assign(this, this.env[type]); + Object.assign(this, this.env[name]); [this.valueA, this.valueB, this.valueC] = this.values; }); diff --git a/test/utils/structs/EnumerableSetExtended.test.js b/test/utils/structs/EnumerableSetExtended.test.js deleted file mode 100644 index 3b9d5ad746d..00000000000 --- a/test/utils/structs/EnumerableSetExtended.test.js +++ /dev/null @@ -1,62 +0,0 @@ -const { ethers } = require('hardhat'); -const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); - -const { mapValues } = require('../../helpers/iterate'); -const { generators } = require('../../helpers/random'); -const { EXTENDED_SET_TYPES } = require('../../../scripts/generate/templates/Enumerable.opts'); - -const { shouldBehaveLikeSet } = require('./EnumerableSet.behavior'); - -async function fixture() { - const mock = await ethers.deployContract('$EnumerableSetExtended'); - - const env = Object.fromEntries( - EXTENDED_SET_TYPES.map(({ name, value }) => [ - name, - { - value, - values: Array.from( - { length: 3 }, - value.size ? () => Array.from({ length: value.size }, generators[value.base]) : generators[value.type], - ), - methods: mapValues( - { - add: `$add(uint256,${value.type})`, - remove: `$remove(uint256,${value.type})`, - contains: `$contains(uint256,${value.type})`, - clear: `$clear_EnumerableSetExtended_${name}(uint256)`, - length: `$length_EnumerableSetExtended_${name}(uint256)`, - at: `$at_EnumerableSetExtended_${name}(uint256,uint256)`, - values: `$values_EnumerableSetExtended_${name}(uint256)`, - }, - fnSig => - (...args) => - mock.getFunction(fnSig)(0, ...args), - ), - events: { - addReturn: `return$add_EnumerableSetExtended_${name}_${value.type.replace(/[[\]]/g, '_')}`, - removeReturn: `return$remove_EnumerableSetExtended_${name}_${value.type.replace(/[[\]]/g, '_')}`, - }, - }, - ]), - ); - - return { mock, env }; -} - -describe('EnumerableSetExtended', function () { - beforeEach(async function () { - Object.assign(this, await loadFixture(fixture)); - }); - - for (const { name, value } of EXTENDED_SET_TYPES) { - describe(`${name} (enumerable set of ${value.type})`, function () { - beforeEach(function () { - Object.assign(this, this.env[name]); - [this.valueA, this.valueB, this.valueC] = this.values; - }); - - shouldBehaveLikeSet(); - }); - } -}); From 45fa35f55727647680bafb103e0fdbc676ce1bde Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 2 Jun 2025 11:11:22 +0200 Subject: [PATCH 13/23] Update scripts/generate/templates/Enumerable.opts.js --- scripts/generate/templates/Enumerable.opts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generate/templates/Enumerable.opts.js b/scripts/generate/templates/Enumerable.opts.js index 611e969d1d6..20e11475729 100644 --- a/scripts/generate/templates/Enumerable.opts.js +++ b/scripts/generate/templates/Enumerable.opts.js @@ -38,7 +38,7 @@ const MAP_TYPES = [] // value type maps ['uint256', 'address', 'bytes32'] .flatMap((keyType, _, array) => array.map(valueType => ({ key: { type: keyType }, value: { type: valueType } }))) - .slice(0, -1), // remove bytes32 → byte32 (last one) that is already defined + .slice(0, -1), // remove bytes32 → bytes32 (last one) that is already defined // non-value type maps { key: { type: 'bytes', memory: true }, value: { type: 'uint256' } }, { key: { type: 'string', memory: true }, value: { type: 'string', memory: true } }, From bec8059bf0611d37857e0e86fde7cf6a04c5b76d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 2 Jun 2025 16:57:36 +0200 Subject: [PATCH 14/23] clarification --- scripts/generate/templates/EnumerableSet.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index 8f04474a58d..981b52db2b4 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -263,8 +263,7 @@ function values(${name} storage set) internal view returns (${type}[] memory) { } `; -// NOTE: this should be used for all value-type sets in v6.0 -const set = ({ name, value }) => `\ +const memorySet = ({ name, value }) => `\ struct ${name} { // Storage of set values ${value.type}[] _values; @@ -531,7 +530,7 @@ module.exports = format( [].concat( defaultSet, SET_TYPES.filter(({ value }) => !value.memory).map(customSet), - SET_TYPES.filter(({ value }) => value.memory && value.size == 0).map(set), + SET_TYPES.filter(({ value }) => value.memory && value.size == 0).map(memorySet), SET_TYPES.filter(({ value }) => value.memory && value.size > 0).map(arraySet), hashes, ), From 0d7f9d0311efd2b738350800f17bcfaeabc062b3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 2 Jun 2025 17:03:20 +0200 Subject: [PATCH 15/23] speedup generation with selective linter --- scripts/generate/run.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/generate/run.js b/scripts/generate/run.js index b06685adee7..394bb39525e 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -13,7 +13,7 @@ function getVersion(path) { } } -function generateFromTemplate(file, template, outputPrefix = '') { +function generateFromTemplate(file, template, outputPrefix = '', lint = false) { const script = path.relative(path.join(__dirname, '../..'), __filename); const input = path.join(path.dirname(script), template); const output = path.join(outputPrefix, file); @@ -27,9 +27,12 @@ function generateFromTemplate(file, template, outputPrefix = '') { ); fs.writeFileSync(output, content); - cp.execFileSync('prettier', ['--write', output]); + lint && cp.execFileSync('prettier', ['--write', output]); } +// Some templates needs to go through the linter after generation +const needsLinter = ['utils/structs/EnumerableMap.sol']; + // Contracts for (const [file, template] of Object.entries({ 'utils/cryptography/MerkleProof.sol': './templates/MerkleProof.js', @@ -45,7 +48,7 @@ for (const [file, template] of Object.entries({ 'mocks/StorageSlotMock.sol': './templates/StorageSlotMock.js', 'mocks/TransientSlotMock.sol': './templates/TransientSlotMock.js', })) { - generateFromTemplate(file, template, './contracts/'); + generateFromTemplate(file, template, './contracts/', needsLinter.includes(file)); } // Tests @@ -54,5 +57,5 @@ for (const [file, template] of Object.entries({ 'utils/Packing.t.sol': './templates/Packing.t.js', 'utils/SlotDerivation.t.sol': './templates/SlotDerivation.t.js', })) { - generateFromTemplate(file, template, './test/'); + generateFromTemplate(file, template, './test/', needsLinter.includes(file)); } From 2376a457a8a723a0901d7c6121794cc4c39474ee Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 2 Jun 2025 18:06:41 +0200 Subject: [PATCH 16/23] update documentation --- contracts/utils/structs/EnumerableSet.sol | 10 ++++++++-- scripts/generate/templates/EnumerableSet.js | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index f70a4d19334..94a7fb0eeb4 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -29,8 +29,14 @@ import {Hashes} from "../cryptography/Hashes.sol"; * } * ``` * - * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`) - * and `uint256` (`UintSet`) are supported. + * The following types are supported: + * + * - `bytes32` (`Bytes32Set`) since v3.3.0 + * - `address` (`AddressSet`) since v3.3.0 + * - `uint256` (`UintSet`) since v3.3.0 + * - `string` (`StringSet`) since v5.4.0 + * - `bytes` (`BytesSet`) since v5.4.0 + * - `bytes32[2]` (`Bytes32x2Set`) since v5.4.0 * * [WARNING] * ==== diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index 981b52db2b4..668ee903b09 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -30,8 +30,14 @@ import {Hashes} from "../cryptography/Hashes.sol"; * } * \`\`\` * - * As of v3.3.0, sets of type \`bytes32\` (\`Bytes32Set\`), \`address\` (\`AddressSet\`) - * and \`uint256\` (\`UintSet\`) are supported. + * The following types are supported: + * + * - \`bytes32\` (\`Bytes32Set\`) since v3.3.0 + * - \`address\` (\`AddressSet\`) since v3.3.0 + * - \`uint256\` (\`UintSet\`) since v3.3.0 + * - \`string\` (\`StringSet\`) since v5.4.0 + * - \`bytes\` (\`BytesSet\`) since v5.4.0 + * - \`bytes32[2]\` (\`Bytes32x2Set\`) since v5.4.0 * * [WARNING] * ==== From 5975d79687e7862765c008b1539f5ed514ca4da3 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 2 Jun 2025 11:14:19 -0600 Subject: [PATCH 17/23] Remove Bytes32x2 --- contracts/utils/structs/EnumerableSet.sol | 126 ----------------- scripts/generate/templates/Enumerable.opts.js | 1 - scripts/generate/templates/EnumerableSet.js | 131 +----------------- 3 files changed, 1 insertion(+), 257 deletions(-) diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 94a7fb0eeb4..cc9a851c101 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -36,7 +36,6 @@ import {Hashes} from "../cryptography/Hashes.sol"; * - `uint256` (`UintSet`) since v3.3.0 * - `string` (`StringSet`) since v5.4.0 * - `bytes` (`BytesSet`) since v5.4.0 - * - `bytes32[2]` (`Bytes32x2Set`) since v5.4.0 * * [WARNING] * ==== @@ -675,131 +674,6 @@ library EnumerableSet { return self._values; } - struct Bytes32x2Set { - // Storage of set values - bytes32[2][] _values; - // Position is the index of the value in the `values` array plus 1. - // Position 0 is used to mean a value is not in the set. - mapping(bytes32 valueHash => uint256) _positions; - } - - /** - * @dev Add a value to a set. O(1). - * - * Returns true if the value was added to the set, that is if it was not - * already present. - */ - function add(Bytes32x2Set storage self, bytes32[2] memory value) internal returns (bool) { - if (!contains(self, value)) { - self._values.push(value); - // The value is stored at length-1, but we add 1 to all indexes - // and use 0 as a sentinel value - self._positions[_hash(value)] = self._values.length; - return true; - } else { - return false; - } - } - - /** - * @dev Removes a value from a set. O(1). - * - * Returns true if the value was removed from the set, that is if it was - * present. - */ - function remove(Bytes32x2Set storage self, bytes32[2] memory value) internal returns (bool) { - // We cache the value's position to prevent multiple reads from the same storage slot - bytes32 valueHash = _hash(value); - uint256 position = self._positions[valueHash]; - - if (position != 0) { - // Equivalent to contains(self, value) - // To delete an element from the _values array in O(1), we swap the element to delete with the last one in - // the array, and then remove the last element (sometimes called as 'swap and pop'). - // This modifies the order of the array, as noted in {at}. - - uint256 valueIndex = position - 1; - uint256 lastIndex = self._values.length - 1; - - if (valueIndex != lastIndex) { - bytes32[2] memory lastValue = self._values[lastIndex]; - - // Move the lastValue to the index where the value to delete is - self._values[valueIndex] = lastValue; - // Update the tracked position of the lastValue (that was just moved) - self._positions[_hash(lastValue)] = position; - } - - // Delete the slot where the moved value was stored - self._values.pop(); - - // Delete the tracked position for the deleted slot - delete self._positions[valueHash]; - - return true; - } else { - return false; - } - } - - /** - * @dev Removes all the values from a set. O(n). - * - * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the - * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. - */ - function clear(Bytes32x2Set storage self) internal { - bytes32[2][] storage v = self._values; - - uint256 len = length(self); - for (uint256 i = 0; i < len; ++i) { - delete self._positions[_hash(v[i])]; - } - assembly ("memory-safe") { - sstore(v.slot, 0) - } - } - - /** - * @dev Returns true if the value is in the set. O(1). - */ - function contains(Bytes32x2Set storage self, bytes32[2] memory value) internal view returns (bool) { - return self._positions[_hash(value)] != 0; - } - - /** - * @dev Returns the number of values on the set. O(1). - */ - function length(Bytes32x2Set storage self) internal view returns (uint256) { - return self._values.length; - } - - /** - * @dev Returns the value stored at position `index` in the set. O(1). - * - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function at(Bytes32x2Set storage self, uint256 index) internal view returns (bytes32[2] memory) { - return self._values[index]; - } - - /** - * @dev Return the entire set in an array - * - * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed - * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that - * this function has an unbounded cost, and using it as part of a state-changing function may render the function - * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. - */ - function values(Bytes32x2Set storage self) internal view returns (bytes32[2][] memory) { - return self._values; - } - function _hash(bytes32[2] memory value) private pure returns (bytes32) { return Hashes.efficientKeccak256(value[0], value[1]); } diff --git a/scripts/generate/templates/Enumerable.opts.js b/scripts/generate/templates/Enumerable.opts.js index 20e11475729..95b403b408a 100644 --- a/scripts/generate/templates/Enumerable.opts.js +++ b/scripts/generate/templates/Enumerable.opts.js @@ -26,7 +26,6 @@ const SET_TYPES = [ { type: 'bytes32' }, { type: 'address' }, { type: 'uint256' }, - { type: 'bytes32', size: 2 }, { type: 'string', memory: true }, { type: 'bytes', memory: true }, ] diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index 668ee903b09..0586fcff3d2 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -37,7 +37,6 @@ import {Hashes} from "../cryptography/Hashes.sol"; * - \`uint256\` (\`UintSet\`) since v3.3.0 * - \`string\` (\`StringSet\`) since v5.4.0 * - \`bytes\` (\`BytesSet\`) since v5.4.0 - * - \`bytes32[2]\` (\`Bytes32x2Set\`) since v5.4.0 * * [WARNING] * ==== @@ -395,133 +394,6 @@ function values(${name} storage self) internal view returns (${value.type}[] mem } `; -const arraySet = ({ name, value }) => `\ -struct ${name} { - // Storage of set values - ${value.type}[] _values; - // Position is the index of the value in the \`values\` array plus 1. - // Position 0 is used to mean a value is not in the set. - mapping(bytes32 valueHash => uint256) _positions; -} - -/** - * @dev Add a value to a set. O(1). - * - * Returns true if the value was added to the set, that is if it was not - * already present. - */ -function add(${name} storage self, ${value.type} memory value) internal returns (bool) { - if (!contains(self, value)) { - self._values.push(value); - // The value is stored at length-1, but we add 1 to all indexes - // and use 0 as a sentinel value - self._positions[_hash(value)] = self._values.length; - return true; - } else { - return false; - } -} - -/** - * @dev Removes a value from a set. O(1). - * - * Returns true if the value was removed from the set, that is if it was - * present. - */ -function remove(${name} storage self, ${value.type} memory value) internal returns (bool) { - // We cache the value's position to prevent multiple reads from the same storage slot - bytes32 valueHash = _hash(value); - uint256 position = self._positions[valueHash]; - - if (position != 0) { - // Equivalent to contains(self, value) - // To delete an element from the _values array in O(1), we swap the element to delete with the last one in - // the array, and then remove the last element (sometimes called as 'swap and pop'). - // This modifies the order of the array, as noted in {at}. - - uint256 valueIndex = position - 1; - uint256 lastIndex = self._values.length - 1; - - if (valueIndex != lastIndex) { - ${value.type} memory lastValue = self._values[lastIndex]; - - // Move the lastValue to the index where the value to delete is - self._values[valueIndex] = lastValue; - // Update the tracked position of the lastValue (that was just moved) - self._positions[_hash(lastValue)] = position; - } - - // Delete the slot where the moved value was stored - self._values.pop(); - - // Delete the tracked position for the deleted slot - delete self._positions[valueHash]; - - return true; - } else { - return false; - } -} - -/** - * @dev Removes all the values from a set. O(n). - * - * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the - * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. - */ -function clear(${name} storage self) internal { - ${value.type}[] storage v = self._values; - - uint256 len = length(self); - for (uint256 i = 0; i < len; ++i) { - delete self._positions[_hash(v[i])]; - } - assembly ("memory-safe") { - sstore(v.slot, 0) - } -} - -/** - * @dev Returns true if the value is in the set. O(1). - */ -function contains(${name} storage self, ${value.type} memory value) internal view returns (bool) { - return self._positions[_hash(value)] != 0; -} - -/** - * @dev Returns the number of values on the set. O(1). - */ -function length(${name} storage self) internal view returns (uint256) { - return self._values.length; -} - -/** - * @dev Returns the value stored at position \`index\` in the set. O(1). - * - * Note that there are no guarantees on the ordering of values inside the - * array, and it may change when more values are added or removed. - * - * Requirements: - * - * - \`index\` must be strictly less than {length}. - */ -function at(${name} storage self, uint256 index) internal view returns (${value.type} memory) { - return self._values[index]; -} - -/** - * @dev Return the entire set in an array - * - * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed - * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that - * this function has an unbounded cost, and using it as part of a state-changing function may render the function - * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. - */ -function values(${name} storage self) internal view returns (${value.type}[] memory) { - return self._values; -} -`; - const hashes = `\ function _hash(bytes32[2] memory value) private pure returns (bytes32) { return Hashes.efficientKeccak256(value[0], value[1]); @@ -536,8 +408,7 @@ module.exports = format( [].concat( defaultSet, SET_TYPES.filter(({ value }) => !value.memory).map(customSet), - SET_TYPES.filter(({ value }) => value.memory && value.size == 0).map(memorySet), - SET_TYPES.filter(({ value }) => value.memory && value.size > 0).map(arraySet), + SET_TYPES.filter(({ value }) => value.memory).map(memorySet), hashes, ), ).trimEnd(), From 935659952d667c596990a836168268abff0e72e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Mon, 2 Jun 2025 11:15:15 -0600 Subject: [PATCH 18/23] Update .changeset/pink-dolls-shop.md Co-authored-by: Hadrien Croubois --- .changeset/pink-dolls-shop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pink-dolls-shop.md b/.changeset/pink-dolls-shop.md index 7718a738fcd..9aeab0f77ac 100644 --- a/.changeset/pink-dolls-shop.md +++ b/.changeset/pink-dolls-shop.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`EnumerableSetExtended` and `EnumerableMapExtended`: Extensions of the `EnumerableSet` and `EnumerableMap` libraries with more types, including non-value types. +`EnumerableSet` and `EnumerableMap`: Add support for more types, including non-value types. From 86c2cb8b655fe9ec4dc17d8596f9f59eff329e82 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 2 Jun 2025 11:27:07 -0600 Subject: [PATCH 19/23] Remove unnecessary _hashes --- contracts/utils/structs/EnumerableSet.sol | 4 ---- scripts/generate/templates/EnumerableSet.js | 7 ------- 2 files changed, 11 deletions(-) diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index cc9a851c101..461106ea3c2 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -673,8 +673,4 @@ library EnumerableSet { function values(BytesSet storage self) internal view returns (bytes[] memory) { return self._values; } - - function _hash(bytes32[2] memory value) private pure returns (bytes32) { - return Hashes.efficientKeccak256(value[0], value[1]); - } } diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index 0586fcff3d2..c0d19d1c3ee 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -394,12 +394,6 @@ function values(${name} storage self) internal view returns (${value.type}[] mem } `; -const hashes = `\ -function _hash(bytes32[2] memory value) private pure returns (bytes32) { - return Hashes.efficientKeccak256(value[0], value[1]); -} -`; - // GENERATE module.exports = format( header.trimEnd(), @@ -409,7 +403,6 @@ module.exports = format( defaultSet, SET_TYPES.filter(({ value }) => !value.memory).map(customSet), SET_TYPES.filter(({ value }) => value.memory).map(memorySet), - hashes, ), ).trimEnd(), '}', From 68982306d38a9216b0896c3453a00f1d3fd2d2df Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 2 Jun 2025 11:53:30 -0600 Subject: [PATCH 20/23] Simplify --- contracts/utils/structs/EnumerableMap.sol | 145 ++---------------- scripts/generate/templates/Enumerable.opts.js | 3 +- scripts/generate/templates/EnumerableMap.js | 3 +- 3 files changed, 18 insertions(+), 133 deletions(-) diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index a53ac05ec60..1c67aacafdc 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -39,8 +39,7 @@ import {EnumerableSet} from "./EnumerableSet.sol"; * - `address -> address` (`AddressToAddressMap`) since v5.1.0 * - `address -> bytes32` (`AddressToBytes32Map`) since v5.1.0 * - `bytes32 -> address` (`Bytes32ToAddressMap`) since v5.1.0 - * - `bytes -> uint256` (`BytesToUintMap`) since v5.4.0 - * - `string -> string` (`StringToStringMap`) since v5.4.0 + * - `bytes -> bytes` (`BytesToBytesMap`) since v5.4.0 * * [WARNING] * ==== @@ -1005,10 +1004,10 @@ library EnumerableMap { */ error EnumerableMapNonexistentBytesKey(bytes key); - struct BytesToUintMap { + struct BytesToBytesMap { // Storage of keys EnumerableSet.BytesSet _keys; - mapping(bytes key => uint256) _values; + mapping(bytes key => bytes) _values; } /** @@ -1018,7 +1017,7 @@ library EnumerableMap { * Returns true if the key was added to the map, that is if it was not * already present. */ - function set(BytesToUintMap storage map, bytes memory key, uint256 value) internal returns (bool) { + function set(BytesToBytesMap storage map, bytes memory key, bytes memory value) internal returns (bool) { map._values[key] = value; return map._keys.add(key); } @@ -1028,7 +1027,7 @@ library EnumerableMap { * * Returns true if the key was removed from the map, that is if it was present. */ - function remove(BytesToUintMap storage map, bytes memory key) internal returns (bool) { + function remove(BytesToBytesMap storage map, bytes memory key) internal returns (bool) { delete map._values[key]; return map._keys.remove(key); } @@ -1039,7 +1038,7 @@ library EnumerableMap { * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. */ - function clear(BytesToUintMap storage map) internal { + function clear(BytesToBytesMap storage map) internal { uint256 len = length(map); for (uint256 i = 0; i < len; ++i) { delete map._values[map._keys.at(i)]; @@ -1050,126 +1049,14 @@ library EnumerableMap { /** * @dev Returns true if the key is in the map. O(1). */ - function contains(BytesToUintMap storage map, bytes memory key) internal view returns (bool) { + function contains(BytesToBytesMap storage map, bytes memory key) internal view returns (bool) { return map._keys.contains(key); } /** * @dev Returns the number of key-value pairs in the map. O(1). */ - function length(BytesToUintMap storage map) internal view returns (uint256) { - return map._keys.length(); - } - - /** - * @dev Returns the key-value pair stored at position `index` in the map. O(1). - * - * Note that there are no guarantees on the ordering of entries inside the - * array, and it may change when more entries are added or removed. - * - * Requirements: - * - * - `index` must be strictly less than {length}. - */ - function at(BytesToUintMap storage map, uint256 index) internal view returns (bytes memory key, uint256 value) { - key = map._keys.at(index); - value = map._values[key]; - } - - /** - * @dev Tries to returns the value associated with `key`. O(1). - * Does not revert if `key` is not in the map. - */ - function tryGet(BytesToUintMap storage map, bytes memory key) internal view returns (bool exists, uint256 value) { - value = map._values[key]; - exists = value != uint256(0) || contains(map, key); - } - - /** - * @dev Returns the value associated with `key`. O(1). - * - * Requirements: - * - * - `key` must be in the map. - */ - function get(BytesToUintMap storage map, bytes memory key) internal view returns (uint256 value) { - bool exists; - (exists, value) = tryGet(map, key); - if (!exists) { - revert EnumerableMapNonexistentBytesKey(key); - } - } - - /** - * @dev Return the an array containing all the keys - * - * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed - * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that - * this function has an unbounded cost, and using it as part of a state-changing function may render the function - * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. - */ - function keys(BytesToUintMap storage map) internal view returns (bytes[] memory) { - return map._keys.values(); - } - - /** - * @dev Query for a nonexistent map key. - */ - error EnumerableMapNonexistentStringKey(string key); - - struct StringToStringMap { - // Storage of keys - EnumerableSet.StringSet _keys; - mapping(string key => string) _values; - } - - /** - * @dev Adds a key-value pair to a map, or updates the value for an existing - * key. O(1). - * - * Returns true if the key was added to the map, that is if it was not - * already present. - */ - function set(StringToStringMap storage map, string memory key, string memory value) internal returns (bool) { - map._values[key] = value; - return map._keys.add(key); - } - - /** - * @dev Removes a key-value pair from a map. O(1). - * - * Returns true if the key was removed from the map, that is if it was present. - */ - function remove(StringToStringMap storage map, string memory key) internal returns (bool) { - delete map._values[key]; - return map._keys.remove(key); - } - - /** - * @dev Removes all the entries from a map. O(n). - * - * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the - * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. - */ - function clear(StringToStringMap storage map) internal { - uint256 len = length(map); - for (uint256 i = 0; i < len; ++i) { - delete map._values[map._keys.at(i)]; - } - map._keys.clear(); - } - - /** - * @dev Returns true if the key is in the map. O(1). - */ - function contains(StringToStringMap storage map, string memory key) internal view returns (bool) { - return map._keys.contains(key); - } - - /** - * @dev Returns the number of key-value pairs in the map. O(1). - */ - function length(StringToStringMap storage map) internal view returns (uint256) { + function length(BytesToBytesMap storage map) internal view returns (uint256) { return map._keys.length(); } @@ -1184,9 +1071,9 @@ library EnumerableMap { * - `index` must be strictly less than {length}. */ function at( - StringToStringMap storage map, + BytesToBytesMap storage map, uint256 index - ) internal view returns (string memory key, string memory value) { + ) internal view returns (bytes memory key, bytes memory value) { key = map._keys.at(index); value = map._values[key]; } @@ -1196,9 +1083,9 @@ library EnumerableMap { * Does not revert if `key` is not in the map. */ function tryGet( - StringToStringMap storage map, - string memory key - ) internal view returns (bool exists, string memory value) { + BytesToBytesMap storage map, + bytes memory key + ) internal view returns (bool exists, bytes memory value) { value = map._values[key]; exists = bytes(value).length != 0 || contains(map, key); } @@ -1210,11 +1097,11 @@ library EnumerableMap { * * - `key` must be in the map. */ - function get(StringToStringMap storage map, string memory key) internal view returns (string memory value) { + function get(BytesToBytesMap storage map, bytes memory key) internal view returns (bytes memory value) { bool exists; (exists, value) = tryGet(map, key); if (!exists) { - revert EnumerableMapNonexistentStringKey(key); + revert EnumerableMapNonexistentBytesKey(key); } } @@ -1226,7 +1113,7 @@ library EnumerableMap { * this function has an unbounded cost, and using it as part of a state-changing function may render the function * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block. */ - function keys(StringToStringMap storage map) internal view returns (string[] memory) { + function keys(BytesToBytesMap storage map) internal view returns (bytes[] memory) { return map._keys.values(); } } diff --git a/scripts/generate/templates/Enumerable.opts.js b/scripts/generate/templates/Enumerable.opts.js index 95b403b408a..50c7349b352 100644 --- a/scripts/generate/templates/Enumerable.opts.js +++ b/scripts/generate/templates/Enumerable.opts.js @@ -39,8 +39,7 @@ const MAP_TYPES = [] .flatMap((keyType, _, array) => array.map(valueType => ({ key: { type: keyType }, value: { type: valueType } }))) .slice(0, -1), // remove bytes32 → bytes32 (last one) that is already defined // non-value type maps - { key: { type: 'bytes', memory: true }, value: { type: 'uint256' } }, - { key: { type: 'string', memory: true }, value: { type: 'string', memory: true } }, + { key: { type: 'bytes', memory: true }, value: { type: 'bytes', memory: true } }, ) .map(entry => mapValues(entry, typeDescr)) .map(toMapTypeDescr); diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index 7e52b2eb3c6..55742188861 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -40,8 +40,7 @@ import {EnumerableSet} from "./EnumerableSet.sol"; * - \`address -> address\` (\`AddressToAddressMap\`) since v5.1.0 * - \`address -> bytes32\` (\`AddressToBytes32Map\`) since v5.1.0 * - \`bytes32 -> address\` (\`Bytes32ToAddressMap\`) since v5.1.0 - * - \`bytes -> uint256\` (\`BytesToUintMap\`) since v5.4.0 - * - \`string -> string\` (\`StringToStringMap\`) since v5.4.0 + * - \`bytes -> bytes\` (\`BytesToBytesMap\`) since v5.4.0 * * [WARNING] * ==== From 4e2bc70d4b8b47ae663572e47aa4fd967f791cce Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 2 Jun 2025 11:59:49 -0600 Subject: [PATCH 21/23] Improve changesets --- .changeset/long-hornets-mate.md | 5 +++++ .changeset/pink-dolls-shop.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/long-hornets-mate.md diff --git a/.changeset/long-hornets-mate.md b/.changeset/long-hornets-mate.md new file mode 100644 index 00000000000..29f8f82a398 --- /dev/null +++ b/.changeset/long-hornets-mate.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`EnumerableMap`: Add support for `BytesToBytesMap` type. diff --git a/.changeset/pink-dolls-shop.md b/.changeset/pink-dolls-shop.md index 9aeab0f77ac..2b9fbf6c8a1 100644 --- a/.changeset/pink-dolls-shop.md +++ b/.changeset/pink-dolls-shop.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`EnumerableSet` and `EnumerableMap`: Add support for more types, including non-value types. +`EnumerableSet`: Add support for `StringSet` and `BytesSet` types. From 2a1f50378fc6b12ae00c6e106faba7800c4b63c7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 2 Jun 2025 21:52:35 +0200 Subject: [PATCH 22/23] remove unecessary import --- contracts/utils/structs/EnumerableSet.sol | 1 - scripts/generate/templates/EnumerableSet.js | 1 - 2 files changed, 2 deletions(-) diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 461106ea3c2..fcc738e6da4 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.20; import {Arrays} from "../Arrays.sol"; -import {Hashes} from "../cryptography/Hashes.sol"; /** * @dev Library for managing diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index c0d19d1c3ee..6719e38af2a 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -6,7 +6,6 @@ const header = `\ pragma solidity ^0.8.20; import {Arrays} from "../Arrays.sol"; -import {Hashes} from "../cryptography/Hashes.sol"; /** * @dev Library for managing From 326c466e63c1d69c93fe28e0c77d109003525bc9 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 2 Jun 2025 14:29:41 -0600 Subject: [PATCH 23/23] Use Arrays.sol --- contracts/utils/structs/EnumerableSet.sol | 12 ++---------- scripts/generate/templates/EnumerableSet.js | 6 +----- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index fcc738e6da4..1c037241b19 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -502,11 +502,7 @@ library EnumerableSet { for (uint256 i = 0; i < len; ++i) { delete set._positions[set._values[i]]; } - // Replace when these are available in Arrays.sol - string[] storage array = set._values; - assembly ("memory-safe") { - sstore(array.slot, 0) - } + Arrays.unsafeSetLength(set._values, 0); } /** @@ -626,11 +622,7 @@ library EnumerableSet { for (uint256 i = 0; i < len; ++i) { delete set._positions[set._values[i]]; } - // Replace when these are available in Arrays.sol - bytes[] storage array = set._values; - assembly ("memory-safe") { - sstore(array.slot, 0) - } + Arrays.unsafeSetLength(set._values, 0); } /** diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index 6719e38af2a..ac620b88ae5 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -345,11 +345,7 @@ function clear(${name} storage set) internal { for (uint256 i = 0; i < len; ++i) { delete set._positions[set._values[i]]; } - // Replace when these are available in Arrays.sol - ${value.type}[] storage array = set._values; - assembly ("memory-safe") { - sstore(array.slot, 0) - } + Arrays.unsafeSetLength(set._values, 0); } /**