Răsfoiți Sursa

Procedurally generate EnumerableSet and EnumerableMap (#3429)

Hadrien Croubois 3 ani în urmă
părinte
comite
17bc2dabe0

+ 1 - 0
contracts/mocks/EnumerableMapMock.sol

@@ -90,6 +90,7 @@ contract AddressToUintMapMock {
     }
 }
 
+// Bytes32ToBytes32Map
 contract Bytes32ToBytes32MapMock {
     using EnumerableMap for EnumerableMap.Bytes32ToBytes32Map;
 

+ 11 - 13
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:
      *

+ 10 - 2
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);

+ 7 - 0
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);

+ 276 - 0
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'),
+  ],
+  '}',
+);

+ 66 - 0
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)),
+);

+ 251 - 0
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'),
+  ],
+  '}',
+);

+ 56 - 0
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)),
+);

+ 30 - 0
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,
+};