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/.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: 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..91b912e6d72 --- /dev/null +++ b/contracts/utils/structs/EnumerableMapExtended.sol @@ -0,0 +1,281 @@ +// 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..a5ba388a74f --- /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/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: { diff --git a/package-lock.json b/package-lock.json index a82155c7608..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.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..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.4", + "ethers": "6.13.6-beta.1", "glob": "^11.0.0", "globals": "^16.0.0", "graphlib": "^2.1.8", diff --git a/scripts/generate/run.js b/scripts/generate/run.js index 6779c93f44b..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 @@ -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/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. 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(); + }); + } +});