diff --git a/.changeset/good-cameras-rush.md b/.changeset/good-cameras-rush.md new file mode 100644 index 00000000000..ebe663c7a6c --- /dev/null +++ b/.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. diff --git a/.changeset/sixty-tips-wink.md b/.changeset/sixty-tips-wink.md new file mode 100644 index 00000000000..35c14cb890d --- /dev/null +++ b/.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. diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index 4e12acec142..d455c4d48a7 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/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). */ diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 4aa27da1750..7d7ba082520 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/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). */ diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index c9cad6c1bc8..284e5ac0281 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/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). */ diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index cf9a1fc6771..e6a3d220276 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/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). */ diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index 37da41795dc..c80eec934ba 100644 --- a/test/utils/structs/EnumerableMap.behavior.js +++ b/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); diff --git a/test/utils/structs/EnumerableMap.test.js b/test/utils/structs/EnumerableMap.test.js index 5362e873aa3..cb4b77a651f 100644 --- a/test/utils/structs/EnumerableMap.test.js +++ b/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})`, diff --git a/test/utils/structs/EnumerableSet.behavior.js b/test/utils/structs/EnumerableSet.behavior.js index 5324dd70b9d..fb932680cdd 100644 --- a/test/utils/structs/EnumerableSet.behavior.js +++ b/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 = { diff --git a/test/utils/structs/EnumerableSet.test.js b/test/utils/structs/EnumerableSet.test.js index cccc4a3e0cf..f01810013a0 100644 --- a/test/utils/structs/EnumerableSet.test.js +++ b/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)`,