Explorar el Código

Add clear function to Enumerable{Set,Map} (#5486)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Arr00 hace 7 meses
padre
commit
3658269505

+ 5 - 0
.changeset/good-cameras-rush.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`EnumerableMap`: Add `clear` function to EnumerableMaps which deletes all entries in the map.

+ 5 - 0
.changeset/sixty-tips-wink.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`EnumerableSet`: Add `clear` function to EnumerableSets which deletes all values in the set.

+ 95 - 0
contracts/utils/structs/EnumerableMap.sol

@@ -16,6 +16,7 @@ import {EnumerableSet} from "./EnumerableSet.sol";
  * - 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 {
@@ -90,6 +91,20 @@ library EnumerableMap {
         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(Bytes32ToBytes32Map 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).
      */
@@ -185,6 +200,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(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(UintToUintMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -278,6 +303,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(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(UintToAddressMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -371,6 +406,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(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(UintToBytes32Map storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -464,6 +509,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(uint256(uint160(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(AddressToUintMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -557,6 +612,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(uint256(uint160(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(AddressToAddressMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -650,6 +715,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(uint256(uint160(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(AddressToBytes32Map storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -743,6 +818,16 @@ library EnumerableMap {
         return remove(map._inner, 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(Bytes32ToUintMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -836,6 +921,16 @@ library EnumerableMap {
         return remove(map._inner, 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(Bytes32ToAddressMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */

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

@@ -4,6 +4,7 @@
 
 pragma solidity ^0.8.20;
 
+import {Arrays} from "../Arrays.sol";
 import {Hashes} from "../cryptography/Hashes.sol";
 
 /**
@@ -16,6 +17,7 @@ import {Hashes} from "../cryptography/Hashes.sol";
  * - 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 {
@@ -116,6 +118,20 @@ library EnumerableSet {
         }
     }
 
+    /**
+     * @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(Set storage set) private {
+        uint256 len = _length(set);
+        for (uint256 i = 0; i < len; ++i) {
+            delete set._positions[set._values[i]];
+        }
+        Arrays.unsafeSetLength(set._values, 0);
+    }
+
     /**
      * @dev Returns true if the value is in the set. O(1).
      */
@@ -182,6 +198,16 @@ library EnumerableSet {
         return _remove(set._inner, value);
     }
 
+    /**
+     * @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(Bytes32Set storage set) internal {
+        _clear(set._inner);
+    }
+
     /**
      * @dev Returns true if the value is in the set. O(1).
      */
@@ -255,6 +281,16 @@ library EnumerableSet {
         return _remove(set._inner, bytes32(uint256(uint160(value))));
     }
 
+    /**
+     * @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(AddressSet storage set) internal {
+        _clear(set._inner);
+    }
+
     /**
      * @dev Returns true if the value is in the set. O(1).
      */
@@ -328,6 +364,16 @@ library EnumerableSet {
         return _remove(set._inner, bytes32(value));
     }
 
+    /**
+     * @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(UintSet storage set) internal {
+        _clear(set._inner);
+    }
+
     /**
      * @dev Returns true if the value is in the set. O(1).
      */
@@ -442,6 +488,24 @@ library EnumerableSet {
         }
     }
 
+    /**
+     * @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 self. O(1).
      */

+ 25 - 0
scripts/generate/templates/EnumerableMap.js

@@ -17,6 +17,7 @@ import {EnumerableSet} from "./EnumerableSet.sol";
  * - 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 {
@@ -91,6 +92,20 @@ function remove(Bytes32ToBytes32Map storage map, bytes32 key) internal returns (
     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(Bytes32ToBytes32Map 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).
  */
@@ -188,6 +203,16 @@ function remove(${name} storage map, ${keyType} key) internal returns (bool) {
     return remove(map._inner, ${toBytes32(keyType, '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 {
+    clear(map._inner);
+}
+
 /**
  * @dev Returns true if the key is in the map. O(1).
  */

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

@@ -5,6 +5,7 @@ const { TYPES } = require('./EnumerableSet.opts');
 const header = `\
 pragma solidity ^0.8.20;
 
+import {Arrays} from "../Arrays.sol";
 import {Hashes} from "../cryptography/Hashes.sol";
 
 /**
@@ -17,6 +18,7 @@ import {Hashes} from "../cryptography/Hashes.sol";
  * - 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 {
@@ -119,6 +121,20 @@ function _remove(Set storage set, bytes32 value) private returns (bool) {
     }
 }
 
+/**
+ * @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(Set storage set) private {
+    uint256 len = _length(set);
+    for (uint256 i = 0; i < len; ++i) {
+        delete set._positions[set._values[i]];
+    }
+    Arrays.unsafeSetLength(set._values, 0);
+}
+
 /**
  * @dev Returns true if the value is in the set. O(1).
  */
@@ -187,6 +203,16 @@ function remove(${name} storage set, ${type} value) internal returns (bool) {
     return _remove(set._inner, ${toBytes32(type, 'value')});
 }
 
+/**
+ * @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 {
+    _clear(set._inner);
+}
+
 /**
  * @dev Returns true if the value is in the set. O(1).
  */
@@ -303,6 +329,24 @@ function remove(${name} storage self, ${type} memory value) internal returns (bo
     }
 }
 
+/**
+ * @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 {
+    ${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 self. O(1).
  */

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

@@ -117,6 +117,49 @@ function shouldBehaveLikeMap() {
     });
   });
 
+  describe('clear', function () {
+    it('clears a single entry', async function () {
+      await this.methods.set(this.keyA, this.valueA);
+
+      await this.methods.clear();
+
+      expect(await this.methods.contains(this.keyA)).to.be.false;
+      await expectMembersMatch(this.methods, [], []);
+    });
+
+    it('clears multiple entries', async function () {
+      await this.methods.set(this.keyA, this.valueA);
+      await this.methods.set(this.keyB, this.valueB);
+      await this.methods.set(this.keyC, this.valueC);
+
+      await this.methods.clear();
+
+      expect(await this.methods.contains(this.keyA)).to.be.false;
+      expect(await this.methods.contains(this.keyB)).to.be.false;
+      expect(await this.methods.contains(this.keyC)).to.be.false;
+      await expectMembersMatch(this.methods, [], []);
+    });
+
+    it('does not revert on empty map', async function () {
+      await this.methods.clear();
+    });
+
+    it('clear then add entry', async function () {
+      await this.methods.set(this.keyA, this.valueA);
+      await this.methods.set(this.keyB, this.valueB);
+      await this.methods.set(this.keyC, this.valueC);
+
+      await this.methods.clear();
+
+      await this.methods.set(this.keyA, this.valueA);
+
+      expect(await this.methods.contains(this.keyA)).to.be.true;
+      expect(await this.methods.contains(this.keyB)).to.be.false;
+      expect(await this.methods.contains(this.keyC)).to.be.false;
+      await expectMembersMatch(this.methods, [this.keyA], [this.valueA]);
+    });
+  });
+
   describe('read', function () {
     beforeEach(async function () {
       await this.methods.set(this.keyA, this.valueA);

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

@@ -26,6 +26,7 @@ async function fixture() {
             get: `$get_EnumerableMap_${name}(uint256,${keyType})`,
             tryGet: `$tryGet_EnumerableMap_${name}(uint256,${keyType})`,
             remove: `$remove_EnumerableMap_${name}(uint256,${keyType})`,
+            clear: `$clear_EnumerableMap_${name}(uint256)`,
             length: `$length_EnumerableMap_${name}(uint256)`,
             at: `$at_EnumerableMap_${name}(uint256,uint256)`,
             contains: `$contains_EnumerableMap_${name}(uint256,${keyType})`,

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

@@ -109,6 +109,49 @@ function shouldBehaveLikeSet() {
       expect(await this.methods.contains(this.valueB)).to.be.false;
     });
   });
+
+  describe('clear', function () {
+    it('clears a single value', async function () {
+      await this.methods.add(this.valueA);
+
+      await this.methods.clear();
+
+      expect(await this.methods.contains(this.valueA)).to.be.false;
+      await expectMembersMatch(this.methods, []);
+    });
+
+    it('clears multiple values', async function () {
+      await this.methods.add(this.valueA);
+      await this.methods.add(this.valueB);
+      await this.methods.add(this.valueC);
+
+      await this.methods.clear();
+
+      expect(await this.methods.contains(this.valueA)).to.be.false;
+      expect(await this.methods.contains(this.valueB)).to.be.false;
+      expect(await this.methods.contains(this.valueC)).to.be.false;
+      await expectMembersMatch(this.methods, []);
+    });
+
+    it('does not revert on empty set', async function () {
+      await this.methods.clear();
+    });
+
+    it('clear then add value', async function () {
+      await this.methods.add(this.valueA);
+      await this.methods.add(this.valueB);
+      await this.methods.add(this.valueC);
+
+      await this.methods.clear();
+
+      await this.methods.add(this.valueA);
+
+      expect(await this.methods.contains(this.valueA)).to.be.true;
+      expect(await this.methods.contains(this.valueB)).to.be.false;
+      expect(await this.methods.contains(this.valueC)).to.be.false;
+      await expectMembersMatch(this.methods, [this.valueA]);
+    });
+  });
 }
 
 module.exports = {

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

@@ -30,6 +30,7 @@ async function fixture() {
         methods: getMethods(mock, {
           add: `$add(uint256,${type})`,
           remove: `$remove(uint256,${type})`,
+          clear: `$clear_EnumerableSet_${name}(uint256)`,
           contains: `$contains(uint256,${type})`,
           length: `$length_EnumerableSet_${name}(uint256)`,
           at: `$at_EnumerableSet_${name}(uint256,uint256)`,