From 17bc2dabe08289470ce18fb0a16ebb5b58123a22 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 19 Aug 2022 14:12:18 +0200 Subject: [PATCH] Procedurally generate EnumerableSet and EnumerableMap (#3429) --- contracts/mocks/EnumerableMapMock.sol | 1 + contracts/utils/structs/EnumerableMap.sol | 24 +- contracts/utils/structs/EnumerableSet.sol | 12 +- scripts/generate/run.js | 7 + scripts/generate/templates/EnumerableMap.js | 276 ++++++++++++++++++ .../generate/templates/EnumerableMapMock.js | 66 +++++ scripts/generate/templates/EnumerableSet.js | 251 ++++++++++++++++ .../generate/templates/EnumerableSetMock.js | 56 ++++ scripts/generate/templates/conversion.js | 30 ++ 9 files changed, 708 insertions(+), 15 deletions(-) create mode 100644 scripts/generate/templates/EnumerableMap.js create mode 100755 scripts/generate/templates/EnumerableMapMock.js create mode 100644 scripts/generate/templates/EnumerableSet.js create mode 100755 scripts/generate/templates/EnumerableSetMock.js create mode 100644 scripts/generate/templates/conversion.js diff --git a/contracts/mocks/EnumerableMapMock.sol b/contracts/mocks/EnumerableMapMock.sol index dbdf2b24976..96013957938 100644 --- a/contracts/mocks/EnumerableMapMock.sol +++ b/contracts/mocks/EnumerableMapMock.sol @@ -90,6 +90,7 @@ contract AddressToUintMapMock { } } +// Bytes32ToBytes32Map contract Bytes32ToBytes32MapMock { using EnumerableMap for EnumerableMap.Bytes32ToBytes32Map; diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index ba8020155e6..5db9df44ef1 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -30,7 +30,7 @@ import "./EnumerableSet.sol"; * * - `uint256 -> address` (`UintToAddressMap`) since v3.0.0 * - `address -> uint256` (`AddressToUintMap`) since v4.6.0 - * - `bytes32 -> bytes32` (`Bytes32ToBytes32`) since v4.6.0 + * - `bytes32 -> bytes32` (`Bytes32ToBytes32Map`) since v4.6.0 * - `uint256 -> uint256` (`UintToUintMap`) since v4.7.0 * - `bytes32 -> uint256` (`Bytes32ToUintMap`) since v4.7.0 * @@ -116,7 +116,7 @@ library EnumerableMap { } /** - * @dev Tries to returns the value associated with `key`. O(1). + * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool, bytes32) { @@ -129,7 +129,7 @@ library EnumerableMap { } /** - * @dev Returns the value associated with `key`. O(1). + * @dev Returns the value associated with `key`. O(1). * * Requirements: * @@ -216,7 +216,7 @@ library EnumerableMap { } /** - * @dev Tries to returns the value associated with `key`. O(1). + * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ function tryGet(UintToUintMap storage map, uint256 key) internal view returns (bool, uint256) { @@ -225,7 +225,7 @@ library EnumerableMap { } /** - * @dev Returns the value associated with `key`. O(1). + * @dev Returns the value associated with `key`. O(1). * * Requirements: * @@ -308,10 +308,8 @@ library EnumerableMap { } /** - * @dev Tries to returns the value associated with `key`. O(1). + * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. - * - * _Available since v3.4._ */ function tryGet(UintToAddressMap storage map, uint256 key) internal view returns (bool, address) { (bool success, bytes32 value) = tryGet(map._inner, bytes32(key)); @@ -319,7 +317,7 @@ library EnumerableMap { } /** - * @dev Returns the value associated with `key`. O(1). + * @dev Returns the value associated with `key`. O(1). * * Requirements: * @@ -402,7 +400,7 @@ library EnumerableMap { } /** - * @dev Tries to returns the value associated with `key`. O(1). + * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ function tryGet(AddressToUintMap storage map, address key) internal view returns (bool, uint256) { @@ -411,7 +409,7 @@ library EnumerableMap { } /** - * @dev Returns the value associated with `key`. O(1). + * @dev Returns the value associated with `key`. O(1). * * Requirements: * @@ -494,7 +492,7 @@ library EnumerableMap { } /** - * @dev Tries to returns the value associated with `key`. O(1). + * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ function tryGet(Bytes32ToUintMap storage map, bytes32 key) internal view returns (bool, uint256) { @@ -503,7 +501,7 @@ library EnumerableMap { } /** - * @dev Returns the value associated with `key`. O(1). + * @dev Returns the value associated with `key`. O(1). * * Requirements: * diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index b6c647f0700..1aff30c3907 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -214,7 +214,15 @@ library EnumerableSet { * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. */ function values(Bytes32Set storage set) internal view returns (bytes32[] memory) { - return _values(set._inner); + bytes32[] memory store = _values(set._inner); + bytes32[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; } // AddressSet @@ -325,7 +333,7 @@ library EnumerableSet { } /** - * @dev Returns the number of values on the set. O(1). + * @dev Returns the number of values in the set. O(1). */ function length(UintSet storage set) internal view returns (uint256) { return _length(set._inner); diff --git a/scripts/generate/run.js b/scripts/generate/run.js index 0072653d0c8..1967520f499 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -14,8 +14,15 @@ function getVersion (path) { } for (const [ file, template ] of Object.entries({ + // SafeCast 'utils/math/SafeCast.sol': './templates/SafeCast', 'mocks/SafeCastMock.sol': './templates/SafeCastMock', + // EnumerableSet + 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet', + 'mocks/EnumerableSetMock.sol': './templates/EnumerableSetMock', + // EnumerableMap + 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap', + 'mocks/EnumerableMapMock.sol': './templates/EnumerableMapMock', })) { const path = `./contracts/${file}`; const version = getVersion(path); diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js new file mode 100644 index 00000000000..2f729e56aa4 --- /dev/null +++ b/scripts/generate/templates/EnumerableMap.js @@ -0,0 +1,276 @@ +const format = require('../format-lines'); +const { fromBytes32, toBytes32 } = require('./conversion'); + +const TYPES = [ + { name: 'UintToUintMap', keyType: 'uint256', valueType: 'uint256' }, + { name: 'UintToAddressMap', keyType: 'uint256', valueType: 'address' }, + { name: 'AddressToUintMap', keyType: 'address', valueType: 'uint256' }, + { name: 'Bytes32ToUintMap', keyType: 'bytes32', valueType: 'uint256' }, +]; + +/* eslint-disable max-len */ +const header = `\ +pragma solidity ^0.8.0; + +import "./EnumerableSet.sol"; + +/** + * @dev Library for managing an enumerable variant of Solidity's + * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[\`mapping\`] + * type. + * + * 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. + * + * \`\`\` + * contract Example { + * // Add the library methods + * using EnumerableMap for EnumerableMap.UintToAddressMap; + * + * // Declare a set state variable + * EnumerableMap.UintToAddressMap private myMap; + * } + * \`\`\` + * + * The following map types are supported: + * + * - \`uint256 -> address\` (\`UintToAddressMap\`) since v3.0.0 + * - \`address -> uint256\` (\`AddressToUintMap\`) since v4.6.0 + * - \`bytes32 -> bytes32\` (\`Bytes32ToBytes32Map\`) since v4.6.0 + * - \`uint256 -> uint256\` (\`UintToUintMap\`) since v4.7.0 + * - \`bytes32 -> uint256\` (\`Bytes32ToUintMap\`) since v4.7.0 + * + * [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. + * ==== + */ +`; +/* eslint-enable max-len */ + +const defaultMap = () => `\ +// 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, and user-facing +// implementations (such as Uint256ToAddressMap) are just wrappers around +// the underlying Map. +// This means that we can only create new EnumerableMaps for types that fit +// in bytes32. + +struct Bytes32ToBytes32Map { + // Storage of keys + EnumerableSet.Bytes32Set _keys; + mapping(bytes32 => bytes32) _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( + Bytes32ToBytes32Map storage map, + bytes32 key, + bytes32 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(Bytes32ToBytes32Map storage map, bytes32 key) internal returns (bool) { + delete map._values[key]; + return map._keys.remove(key); +} + +/** + * @dev Returns true if the key is in the map. O(1). + */ +function contains(Bytes32ToBytes32Map storage map, bytes32 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(Bytes32ToBytes32Map 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(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32, bytes32) { + bytes32 key = map._keys.at(index); + return (key, 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(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool, bytes32) { + bytes32 value = map._values[key]; + if (value == bytes32(0)) { + return (contains(map, key), bytes32(0)); + } else { + return (true, value); + } +} + +/** + * @dev Returns the value associated with \`key\`. O(1). + * + * Requirements: + * + * - \`key\` must be in the map. + */ +function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { + bytes32 value = map._values[key]; + require(value != 0 || contains(map, key), "EnumerableMap: nonexistent key"); + return value; +} + +/** + * @dev Same as {_get}, with a custom error message when \`key\` is not in the map. + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {_tryGet}. + */ +function get( + Bytes32ToBytes32Map storage map, + bytes32 key, + string memory errorMessage +) internal view returns (bytes32) { + bytes32 value = map._values[key]; + require(value != 0 || contains(map, key), errorMessage); + return value; +} +`; + +const customMap = ({ name, keyType, valueType }) => `\ +// ${name} + +struct ${name} { + Bytes32ToBytes32Map _inner; +} + +/** + * @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, + ${keyType} key, + ${valueType} value +) internal returns (bool) { + return set(map._inner, ${toBytes32(keyType, 'key')}, ${toBytes32(valueType, 'value')}); +} + +/** + * @dev Removes a value from a set. O(1). + * + * 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')}); +} + +/** + * @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')}); +} + +/** + * @dev Returns the number of elements in the map. O(1). + */ +function length(${name} storage map) internal view returns (uint256) { + return length(map._inner); +} + +/** + * @dev Returns the element 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 map, uint256 index) internal view returns (${keyType}, ${valueType}) { + (bytes32 key, bytes32 value) = at(map._inner, index); + return (${fromBytes32(keyType, 'key')}, ${fromBytes32(valueType, 'value')}); +} + +/** + * @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, ${valueType}) { + (bool success, bytes32 value) = tryGet(map._inner, ${toBytes32(keyType, 'key')}); + return (success, ${fromBytes32(valueType, 'value')}); +} + +/** + * @dev Returns the value associated with \`key\`. O(1). + * + * Requirements: + * + * - \`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')})`)}; +} + +/** + * @dev Same as {get}, with a custom error message when \`key\` is not in the map. + * + * CAUTION: This function is deprecated because it requires allocating memory for the error + * message unnecessarily. For custom revert reasons use {tryGet}. + */ +function get( + ${name} storage map, + ${keyType} key, + string memory errorMessage +) internal view returns (${valueType}) { + return ${fromBytes32(valueType, `get(map._inner, ${toBytes32(keyType, 'key')}, errorMessage)`)}; +} +`; + +// GENERATE +module.exports = format( + header.trimEnd(), + 'library EnumerableMap {', + [ + 'using EnumerableSet for EnumerableSet.Bytes32Set;', + '', + defaultMap(), + TYPES.map(details => customMap(details).trimEnd()).join('\n\n'), + ], + '}', +); diff --git a/scripts/generate/templates/EnumerableMapMock.js b/scripts/generate/templates/EnumerableMapMock.js new file mode 100755 index 00000000000..ff26a6aed77 --- /dev/null +++ b/scripts/generate/templates/EnumerableMapMock.js @@ -0,0 +1,66 @@ +const format = require('../format-lines'); + +const TYPES = [ + { name: 'UintToAddressMap', keyType: 'uint256', valueType: 'address' }, + { name: 'AddressToUintMap', keyType: 'address', valueType: 'uint256' }, + { name: 'Bytes32ToBytes32Map', keyType: 'bytes32', valueType: 'bytes32' }, + { name: 'UintToUintMap', keyType: 'uint256', valueType: 'uint256' }, + { name: 'Bytes32ToUintMap', keyType: 'bytes32', valueType: 'uint256' }, +]; + +const header = `\ +pragma solidity ^0.8.0; + +import "../utils/structs/EnumerableMap.sol"; +`; + +const customSetMock = ({ name, keyType, valueType }) => `\ +// ${name} +contract ${name}Mock { + using EnumerableMap for EnumerableMap.${name}; + + event OperationResult(bool result); + + EnumerableMap.${name} private _map; + + function contains(${keyType} key) public view returns (bool) { + return _map.contains(key); + } + + function set(${keyType} key, ${valueType} value) public { + bool result = _map.set(key, value); + emit OperationResult(result); + } + + function remove(${keyType} key) public { + bool result = _map.remove(key); + emit OperationResult(result); + } + + function length() public view returns (uint256) { + return _map.length(); + } + + function at(uint256 index) public view returns (${keyType} key, ${valueType} value) { + return _map.at(index); + } + + function tryGet(${keyType} key) public view returns (bool, ${valueType}) { + return _map.tryGet(key); + } + + function get(${keyType} key) public view returns (${valueType}) { + return _map.get(key); + } + + function getWithMessage(${keyType} key, string calldata errorMessage) public view returns (${valueType}) { + return _map.get(key, errorMessage); + } +} +`; + +// GENERATE +module.exports = format( + header, + ...TYPES.map(details => customSetMock(details)), +); diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js new file mode 100644 index 00000000000..f75275381b5 --- /dev/null +++ b/scripts/generate/templates/EnumerableSet.js @@ -0,0 +1,251 @@ +const format = require('../format-lines'); +const { fromBytes32, toBytes32 } = require('./conversion'); + +const TYPES = [ + { name: 'Bytes32Set', type: 'bytes32' }, + { name: 'AddressSet', type: 'address' }, + { name: 'UintSet', type: 'uint256' }, +]; + +/* eslint-disable max-len */ +const header = `\ +pragma solidity ^0.8.0; + +/** + * @dev Library for managing + * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive + * 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. + * + * \`\`\` + * contract Example { + * // Add the library methods + * using EnumerableSet for EnumerableSet.AddressSet; + * + * // Declare a set state variable + * EnumerableSet.AddressSet private mySet; + * } + * \`\`\` + * + * As of v3.3.0, sets of type \`bytes32\` (\`Bytes32Set\`), \`address\` (\`AddressSet\`) + * and \`uint256\` (\`UintSet\`) 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. + * ==== + */ +`; +/* eslint-enable max-len */ + +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 +// bytes32 values. +// The Set implementation uses private functions, and user-facing +// implementations (such as AddressSet) are just wrappers around the +// underlying Set. +// This means that we can only create new EnumerableSets for types that fit +// in bytes32. + +struct Set { + // Storage of set values + bytes32[] _values; + // Position of the value in the \`values\` array, plus 1 because index 0 + // means a value is not in the set. + mapping(bytes32 => uint256) _indexes; +} + +/** + * @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(Set storage set, bytes32 value) private returns (bool) { + if (!_contains(set, value)) { + set._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + set._indexes[value] = set._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(Set storage set, bytes32 value) private returns (bool) { + // We read and store the value's index to prevent multiple reads from the same storage slot + uint256 valueIndex = set._indexes[value]; + + if (valueIndex != 0) { + // Equivalent to contains(set, 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 toDeleteIndex = valueIndex - 1; + uint256 lastIndex = set._values.length - 1; + + if (lastIndex != toDeleteIndex) { + bytes32 lastValue = set._values[lastIndex]; + + // Move the last value to the index where the value to delete is + set._values[toDeleteIndex] = lastValue; + // Update the index for the moved value + set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex + } + + // Delete the slot where the moved value was stored + set._values.pop(); + + // Delete the index for the deleted slot + delete set._indexes[value]; + + return true; + } else { + return false; + } +} + +/** + * @dev Returns true if the value is in the set. O(1). + */ +function _contains(Set storage set, bytes32 value) private view returns (bool) { + return set._indexes[value] != 0; +} + +/** + * @dev Returns the number of values on the set. O(1). + */ +function _length(Set storage set) private view returns (uint256) { + return set._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(Set storage set, uint256 index) private view returns (bytes32) { + return set._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(Set storage set) private view returns (bytes32[] memory) { + return set._values; +} +`; + +const customSet = ({ name, type }) => `\ +// ${name} + +struct ${name} { + Set _inner; +} + +/** + * @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 set, ${type} value) internal returns (bool) { + return _add(set._inner, ${toBytes32(type, 'value')}); +} + +/** + * @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 set, ${type} value) internal returns (bool) { + return _remove(set._inner, ${toBytes32(type, 'value')}); +} + +/** + * @dev Returns true if the value is in the set. O(1). + */ +function contains(${name} storage set, ${type} value) internal view returns (bool) { + return _contains(set._inner, ${toBytes32(type, 'value')}); +} + +/** + * @dev Returns the number of values in the set. O(1). + */ +function length(${name} storage set) internal view returns (uint256) { + return _length(set._inner); +} + +/** + * @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 set, uint256 index) internal view returns (${type}) { + return ${fromBytes32(type, '_at(set._inner, 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 set) internal view returns (${type}[] memory) { + bytes32[] memory store = _values(set._inner); + ${type}[] memory result; + + /// @solidity memory-safe-assembly + assembly { + result := store + } + + return result; +} +`; + +// GENERATE +module.exports = format( + header.trimEnd(), + 'library EnumerableSet {', + [ + defaultSet(), + TYPES.map(details => customSet(details).trimEnd()).join('\n\n'), + ], + '}', +); diff --git a/scripts/generate/templates/EnumerableSetMock.js b/scripts/generate/templates/EnumerableSetMock.js new file mode 100755 index 00000000000..fbc9b850c95 --- /dev/null +++ b/scripts/generate/templates/EnumerableSetMock.js @@ -0,0 +1,56 @@ +const format = require('../format-lines'); + +const TYPES = [ + { name: 'Bytes32Set', type: 'bytes32' }, + { name: 'AddressSet', type: 'address' }, + { name: 'UintSet', type: 'uint256' }, +]; + +const header = `\ +pragma solidity ^0.8.0; + +import "../utils/structs/EnumerableSet.sol"; +`; + +const customSetMock = ({ name, type }) => `\ +// ${name} +contract Enumerable${name}Mock { + using EnumerableSet for EnumerableSet.${name}; + + event OperationResult(bool result); + + EnumerableSet.${name} private _set; + + function contains(${type} value) public view returns (bool) { + return _set.contains(value); + } + + function add(${type} value) public { + bool result = _set.add(value); + emit OperationResult(result); + } + + function remove(${type} value) public { + bool result = _set.remove(value); + emit OperationResult(result); + } + + function length() public view returns (uint256) { + return _set.length(); + } + + function at(uint256 index) public view returns (${type}) { + return _set.at(index); + } + + function values() public view returns (${type}[] memory) { + return _set.values(); + } +} +`; + +// GENERATE +module.exports = format( + header, + ...TYPES.map(details => customSetMock(details)), +); diff --git a/scripts/generate/templates/conversion.js b/scripts/generate/templates/conversion.js new file mode 100644 index 00000000000..5c26c682111 --- /dev/null +++ b/scripts/generate/templates/conversion.js @@ -0,0 +1,30 @@ +function toBytes32 (type, value) { + switch (type) { + case 'bytes32': + return value; + case 'uint256': + return `bytes32(${value})`; + case 'address': + return `bytes32(uint256(uint160(${value})))`; + default: + throw new Error(`Conversion from ${type} to bytes32 not supported`); + } +} + +function fromBytes32 (type, value) { + switch (type) { + case 'bytes32': + return value; + case 'uint256': + return `uint256(${value})`; + case 'address': + return `address(uint160(uint256(${value})))`; + default: + throw new Error(`Conversion from bytes32 to ${type} not supported`); + } +} + +module.exports = { + toBytes32, + fromBytes32, +};