Browse Source

Add paginated version of EnumerableSet.values() and EnumerableMap.keys() (#5713)

Co-authored-by: ernestognw <ernestognw@gmail.com>
Hadrien Croubois 4 months ago
parent
commit
d20b9e30bd

+ 5 - 0
.changeset/fine-frogs-bake.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`EnumerableMap`: Add `keys(uint256,uint256)` that returns a subset (slice) of the keys in the map.

+ 5 - 0
.changeset/hot-grapes-lie.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`EnumerableSet`: Add `values(uint256,uint256)` that returns a subset (slice) of the values in the set.

+ 194 - 2
contracts/utils/structs/EnumerableMap.sol

@@ -164,7 +164,7 @@ library EnumerableMap {
     }
 
     /**
-     * @dev Return the an array containing all the keys
+     * @dev Returns 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
@@ -175,6 +175,22 @@ library EnumerableMap {
         return map._keys.values();
     }
 
+    /**
+     * @dev Returns an array containing a slice of 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(
+        Bytes32ToBytes32Map storage map,
+        uint256 start,
+        uint256 end
+    ) internal view returns (bytes32[] memory) {
+        return map._keys.values(start, end);
+    }
+
     // UintToUintMap
 
     struct UintToUintMap {
@@ -278,6 +294,25 @@ library EnumerableMap {
         return result;
     }
 
+    /**
+     * @dev Return the an array containing a slice of 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(UintToUintMap storage map, uint256 start, uint256 end) internal view returns (uint256[] memory) {
+        bytes32[] memory store = keys(map._inner, start, end);
+        uint256[] memory result;
+
+        assembly ("memory-safe") {
+            result := store
+        }
+
+        return result;
+    }
+
     // UintToAddressMap
 
     struct UintToAddressMap {
@@ -381,6 +416,25 @@ library EnumerableMap {
         return result;
     }
 
+    /**
+     * @dev Return the an array containing a slice of 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(UintToAddressMap storage map, uint256 start, uint256 end) internal view returns (uint256[] memory) {
+        bytes32[] memory store = keys(map._inner, start, end);
+        uint256[] memory result;
+
+        assembly ("memory-safe") {
+            result := store
+        }
+
+        return result;
+    }
+
     // UintToBytes32Map
 
     struct UintToBytes32Map {
@@ -484,6 +538,25 @@ library EnumerableMap {
         return result;
     }
 
+    /**
+     * @dev Return the an array containing a slice of 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(UintToBytes32Map storage map, uint256 start, uint256 end) internal view returns (uint256[] memory) {
+        bytes32[] memory store = keys(map._inner, start, end);
+        uint256[] memory result;
+
+        assembly ("memory-safe") {
+            result := store
+        }
+
+        return result;
+    }
+
     // AddressToUintMap
 
     struct AddressToUintMap {
@@ -587,6 +660,25 @@ library EnumerableMap {
         return result;
     }
 
+    /**
+     * @dev Return the an array containing a slice of 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(AddressToUintMap storage map, uint256 start, uint256 end) internal view returns (address[] memory) {
+        bytes32[] memory store = keys(map._inner, start, end);
+        address[] memory result;
+
+        assembly ("memory-safe") {
+            result := store
+        }
+
+        return result;
+    }
+
     // AddressToAddressMap
 
     struct AddressToAddressMap {
@@ -690,6 +782,29 @@ library EnumerableMap {
         return result;
     }
 
+    /**
+     * @dev Return the an array containing a slice of 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(
+        AddressToAddressMap storage map,
+        uint256 start,
+        uint256 end
+    ) internal view returns (address[] memory) {
+        bytes32[] memory store = keys(map._inner, start, end);
+        address[] memory result;
+
+        assembly ("memory-safe") {
+            result := store
+        }
+
+        return result;
+    }
+
     // AddressToBytes32Map
 
     struct AddressToBytes32Map {
@@ -793,6 +908,29 @@ library EnumerableMap {
         return result;
     }
 
+    /**
+     * @dev Return the an array containing a slice of 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(
+        AddressToBytes32Map storage map,
+        uint256 start,
+        uint256 end
+    ) internal view returns (address[] memory) {
+        bytes32[] memory store = keys(map._inner, start, end);
+        address[] memory result;
+
+        assembly ("memory-safe") {
+            result := store
+        }
+
+        return result;
+    }
+
     // Bytes32ToUintMap
 
     struct Bytes32ToUintMap {
@@ -896,6 +1034,25 @@ library EnumerableMap {
         return result;
     }
 
+    /**
+     * @dev Return the an array containing a slice of 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(Bytes32ToUintMap storage map, uint256 start, uint256 end) internal view returns (bytes32[] memory) {
+        bytes32[] memory store = keys(map._inner, start, end);
+        bytes32[] memory result;
+
+        assembly ("memory-safe") {
+            result := store
+        }
+
+        return result;
+    }
+
     // Bytes32ToAddressMap
 
     struct Bytes32ToAddressMap {
@@ -999,6 +1156,29 @@ library EnumerableMap {
         return result;
     }
 
+    /**
+     * @dev Return the an array containing a slice of 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(
+        Bytes32ToAddressMap storage map,
+        uint256 start,
+        uint256 end
+    ) internal view returns (bytes32[] memory) {
+        bytes32[] memory store = keys(map._inner, start, end);
+        bytes32[] memory result;
+
+        assembly ("memory-safe") {
+            result := store
+        }
+
+        return result;
+    }
+
     /**
      * @dev Query for a nonexistent map key.
      */
@@ -1106,7 +1286,7 @@ library EnumerableMap {
     }
 
     /**
-     * @dev Return the an array containing all the keys
+     * @dev Returns 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
@@ -1116,4 +1296,16 @@ library EnumerableMap {
     function keys(BytesToBytesMap storage map) internal view returns (bytes[] memory) {
         return map._keys.values();
     }
+
+    /**
+     * @dev Returns an array containing a slice of 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(BytesToBytesMap storage map, uint256 start, uint256 end) internal view returns (bytes[] memory) {
+        return map._keys.values(start, end);
+    }
 }

+ 124 - 0
contracts/utils/structs/EnumerableSet.sol

@@ -5,6 +5,7 @@
 pragma solidity ^0.8.20;
 
 import {Arrays} from "../Arrays.sol";
+import {Math} from "../math/Math.sol";
 
 /**
  * @dev Library for managing
@@ -176,6 +177,28 @@ library EnumerableSet {
         return set._values;
     }
 
+    /**
+     * @dev Return a slice of the 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, uint256 start, uint256 end) private view returns (bytes32[] memory) {
+        unchecked {
+            end = Math.min(end, _length(set));
+            start = Math.min(start, end);
+
+            uint256 len = end - start;
+            bytes32[] memory result = new bytes32[](len);
+            for (uint256 i = 0; i < len; ++i) {
+                result[i] = Arrays.unsafeAccess(set._values, start + i).value;
+            }
+            return result;
+        }
+    }
+
     // Bytes32Set
 
     struct Bytes32Set {
@@ -259,6 +282,25 @@ library EnumerableSet {
         return result;
     }
 
+    /**
+     * @dev Return a slice of the 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(Bytes32Set storage set, uint256 start, uint256 end) internal view returns (bytes32[] memory) {
+        bytes32[] memory store = _values(set._inner, start, end);
+        bytes32[] memory result;
+
+        assembly ("memory-safe") {
+            result := store
+        }
+
+        return result;
+    }
+
     // AddressSet
 
     struct AddressSet {
@@ -342,6 +384,25 @@ library EnumerableSet {
         return result;
     }
 
+    /**
+     * @dev Return a slice of the 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(AddressSet storage set, uint256 start, uint256 end) internal view returns (address[] memory) {
+        bytes32[] memory store = _values(set._inner, start, end);
+        address[] memory result;
+
+        assembly ("memory-safe") {
+            result := store
+        }
+
+        return result;
+    }
+
     // UintSet
 
     struct UintSet {
@@ -425,6 +486,25 @@ library EnumerableSet {
         return result;
     }
 
+    /**
+     * @dev Return a slice of the 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(UintSet storage set, uint256 start, uint256 end) internal view returns (uint256[] memory) {
+        bytes32[] memory store = _values(set._inner, start, end);
+        uint256[] memory result;
+
+        assembly ("memory-safe") {
+            result := store
+        }
+
+        return result;
+    }
+
     struct StringSet {
         // Storage of set values
         string[] _values;
@@ -545,6 +625,28 @@ library EnumerableSet {
         return self._values;
     }
 
+    /**
+     * @dev Return a slice of the 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 set, uint256 start, uint256 end) internal view returns (string[] memory) {
+        unchecked {
+            end = Math.min(end, length(set));
+            start = Math.min(start, end);
+
+            uint256 len = end - start;
+            string[] memory result = new string[](len);
+            for (uint256 i = 0; i < len; ++i) {
+                result[i] = Arrays.unsafeAccess(set._values, start + i).value;
+            }
+            return result;
+        }
+    }
+
     struct BytesSet {
         // Storage of set values
         bytes[] _values;
@@ -664,4 +766,26 @@ library EnumerableSet {
     function values(BytesSet storage self) internal view returns (bytes[] memory) {
         return self._values;
     }
+
+    /**
+     * @dev Return a slice of the 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 set, uint256 start, uint256 end) internal view returns (bytes[] memory) {
+        unchecked {
+            end = Math.min(end, length(set));
+            start = Math.min(start, end);
+
+            uint256 len = end - start;
+            bytes[] memory result = new bytes[](len);
+            for (uint256 i = 0; i < len; ++i) {
+                result[i] = Arrays.unsafeAccess(set._values, start + i).value;
+            }
+            return result;
+        }
+    }
 }

+ 45 - 2
scripts/generate/templates/EnumerableMap.js

@@ -165,7 +165,7 @@ function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns
 }
 
 /**
- * @dev Return the an array containing all the keys
+ * @dev Returns 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
@@ -175,6 +175,18 @@ function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns
 function keys(Bytes32ToBytes32Map storage map) internal view returns (bytes32[] memory) {
     return map._keys.values();
 }
+
+/**
+ * @dev Returns an array containing a slice of 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(Bytes32ToBytes32Map storage map, uint256 start, uint256 end) internal view returns (bytes32[] memory) {
+    return map._keys.values(start, end);
+}
 `;
 
 const customMap = ({ name, key, value }) => `\
@@ -280,6 +292,25 @@ function keys(${name} storage map) internal view returns (${key.type}[] memory)
 
     return result;
 }
+
+/**
+ * @dev Return the an array containing a slice of 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, uint256 start, uint256 end) internal view returns (${key.type}[] memory) {
+    bytes32[] memory store = keys(map._inner, start, end);
+    ${key.type}[] memory result;
+
+    assembly ("memory-safe") {
+        result := store
+    }
+
+    return result;
+}
 `;
 
 const memoryMap = ({ name, keySet, key, value }) => `\
@@ -390,7 +421,7 @@ function get(${name} storage map, ${key.typeLoc} key) internal view returns (${v
 }
 
 /**
- * @dev Return the an array containing all the keys
+ * @dev Returns 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
@@ -400,6 +431,18 @@ function get(${name} storage map, ${key.typeLoc} key) internal view returns (${v
 function keys(${name} storage map) internal view returns (${key.type}[] memory) {
     return map._keys.values();
 }
+
+/**
+ * @dev Returns an array containing a slice of 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, uint256 start, uint256 end) internal view returns (${key.type}[] memory) {
+    return map._keys.values(start, end);
+}
 `;
 
 // GENERATE

+ 64 - 0
scripts/generate/templates/EnumerableSet.js

@@ -6,6 +6,7 @@ const header = `\
 pragma solidity ^0.8.20;
 
 import {Arrays} from "../Arrays.sol";
+import {Math} from "../math/Math.sol";
 
 /**
  * @dev Library for managing
@@ -179,6 +180,28 @@ function _at(Set storage set, uint256 index) private view returns (bytes32) {
 function _values(Set storage set) private view returns (bytes32[] memory) {
     return set._values;
 }
+
+/**
+ * @dev Return a slice of the 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, uint256 start, uint256 end) private view returns (bytes32[] memory) {
+    unchecked {
+        end = Math.min(end, _length(set));
+        start = Math.min(start, end);
+
+        uint256 len = end - start;
+        bytes32[] memory result = new bytes32[](len);
+        for (uint256 i = 0; i < len; ++i) {
+            result[i] = Arrays.unsafeAccess(set._values, start + i).value;
+        }
+        return result;
+    }
+}
 `;
 
 // NOTE: this should be deprecated in favor of a more native construction in v6.0
@@ -265,6 +288,25 @@ function values(${name} storage set) internal view returns (${type}[] memory) {
 
     return result;
 }
+
+/**
+ * @dev Return a slice of the 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, uint256 start, uint256 end) internal view returns (${type}[] memory) {
+    bytes32[] memory store = _values(set._inner, start, end);
+    ${type}[] memory result;
+
+    assembly ("memory-safe") {
+        result := store
+    }
+
+    return result;
+}
 `;
 
 const memorySet = ({ name, value }) => `\
@@ -387,6 +429,28 @@ function at(${name} storage self, uint256 index) internal view returns (${value.
 function values(${name} storage self) internal view returns (${value.type}[] memory) {
     return self._values;
 }
+
+/**
+ * @dev Return a slice of the 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, uint256 start, uint256 end) internal view returns (${value.type}[] memory) {
+    unchecked {
+        end = Math.min(end, length(set));
+        start = Math.min(start, end);
+
+        uint256 len = end - start;
+        ${value.type}[] memory result = new ${value.type}[](len);
+        for (uint256 i = 0; i < len; ++i) {
+            result[i] = Arrays.unsafeAccess(set._values, start + i).value;
+        }
+        return result;
+    }
+}
 `;
 
 // GENERATE

+ 16 - 0
test/utils/structs/EnumerableMap.behavior.js

@@ -191,6 +191,22 @@ function shouldBehaveLikeMap() {
       });
     });
   });
+
+  it('keys (full & paginated)', async function () {
+    const keys = [this.keyA, this.keyB, this.keyC];
+    await this.methods.set(this.keyA, this.valueA);
+    await this.methods.set(this.keyB, this.valueB);
+    await this.methods.set(this.keyC, this.valueC);
+
+    // get all values
+    expect([...(await this.methods.keys())]).to.deep.equal(keys);
+
+    // try pagination
+    for (const begin of [0, 1, 2, 3, 4])
+      for (const end of [0, 1, 2, 3, 4]) {
+        expect([...(await this.methods.keysPage(begin, end))]).to.deep.equal(keys.slice(begin, end));
+      }
+  });
 }
 
 module.exports = {

+ 2 - 0
test/utils/structs/EnumerableMap.test.js

@@ -34,6 +34,7 @@ async function fixture() {
                 length: `$length_EnumerableMap_${name}(uint256)`,
                 at: `$at_EnumerableMap_${name}(uint256,uint256)`,
                 keys: `$keys_EnumerableMap_${name}(uint256)`,
+                keysPage: `$keys_EnumerableMap_${name}(uint256,uint256,uint256)`,
               }
             : {
                 set: `$set(uint256,${key.type},${value.type})`,
@@ -45,6 +46,7 @@ async function fixture() {
                 length: `$length_EnumerableMap_${name}(uint256)`,
                 at: `$at_EnumerableMap_${name}(uint256,uint256)`,
                 keys: `$keys_EnumerableMap_${name}(uint256)`,
+                keysPage: `$keys_EnumerableMap_${name}(uint256,uint256,uint256)`,
               },
           fnSig =>
             (...args) =>

+ 16 - 0
test/utils/structs/EnumerableSet.behavior.js

@@ -152,6 +152,22 @@ function shouldBehaveLikeSet() {
       await expectMembersMatch(this.methods, [this.valueA]);
     });
   });
+
+  it('values (full & paginated)', async function () {
+    const values = [this.valueA, this.valueB, this.valueC];
+    await this.methods.add(this.valueA);
+    await this.methods.add(this.valueB);
+    await this.methods.add(this.valueC);
+
+    // get all values
+    expect([...(await this.methods.values())]).to.deep.equal(values);
+
+    // try pagination
+    for (const begin of [0, 1, 2, 3, 4])
+      for (const end of [0, 1, 2, 3, 4]) {
+        expect([...(await this.methods.valuesPage(begin, end))]).to.deep.equal(values.slice(begin, end));
+      }
+  });
 }
 
 module.exports = {

+ 1 - 0
test/utils/structs/EnumerableSet.test.js

@@ -35,6 +35,7 @@ async function fixture() {
           length: `$length_EnumerableSet_${name}(uint256)`,
           at: `$at_EnumerableSet_${name}(uint256,uint256)`,
           values: `$values_EnumerableSet_${name}(uint256)`,
+          valuesPage: `$values_EnumerableSet_${name}(uint256,uint256,uint256)`,
         }),
         events: {
           addReturn: `return$add_EnumerableSet_${name}_${value.type.replace(/[[\]]/g, '_')}`,