From 40efc4d629db69a590b434fb608cdbc94a43e0f0 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:11:11 -0500 Subject: [PATCH 01/10] Add clear function to EnumerableSets --- contracts/utils/structs/EnumerableSet.sol | 44 +++++++++++++++++++- scripts/generate/templates/EnumerableSet.js | 24 ++++++++++- test/utils/structs/EnumerableSet.behavior.js | 35 ++++++++++++++++ test/utils/structs/EnumerableSet.test.js | 3 +- 4 files changed, 103 insertions(+), 3 deletions(-) diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 065202e8204..e78f844111d 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -13,7 +13,7 @@ pragma solidity ^0.8.20; * * - 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. + * - Elements are enumerated and cleared in O(n). No guarantees are made on the ordering. * * ```solidity * contract Example { @@ -114,6 +114,18 @@ 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 { + for (uint256 i = _length(set); i > 0; --i) { + _remove(set, _at(set, i - 1)); + } + } + /** * @dev Returns true if the value is in the set. O(1). */ @@ -180,6 +192,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). */ @@ -253,6 +275,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). */ @@ -326,6 +358,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). */ diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index 02eccd0df11..ca571abf043 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -14,7 +14,7 @@ pragma solidity ^0.8.20; * * - 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. + * - Elements are enumerated and cleared in O(n). No guarantees are made on the ordering. * * \`\`\`solidity * contract Example { @@ -117,6 +117,18 @@ 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 { + for (uint256 i = _length(set); i > 0; --i) { + _remove(set, _at(set, i - 1)); + } +} + /** * @dev Returns true if the value is in the set. O(1). */ @@ -185,6 +197,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). */ diff --git a/test/utils/structs/EnumerableSet.behavior.js b/test/utils/structs/EnumerableSet.behavior.js index 5324dd70b9d..1d447f8d365 100644 --- a/test/utils/structs/EnumerableSet.behavior.js +++ b/test/utils/structs/EnumerableSet.behavior.js @@ -109,6 +109,41 @@ function shouldBehaveLikeSet() { expect(await this.methods.contains(this.valueB)).to.be.false; }); }); + + describe('clear', function () { + it('clears a single item', async function () { + await this.methods.add(this.valueA); + await expect(this.methods.length()).to.eventually.equal(1); + + await this.methods.clear(); + + await expect(this.methods.length()).to.eventually.equal(0); + }); + + it('clears multiple items', async function () { + await this.methods.add(this.valueA); + await this.methods.add(this.valueB); + await this.methods.add(this.valueC); + await expect(this.methods.length()).to.eventually.equal(3); + + await this.methods.clear(); + + await expect(this.methods.length()).to.eventually.equal(0); + }); + + 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.clear(); + + await this.methods.add(this.valueA); + 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 66d666058ea..e471fb5b683 100644 --- a/test/utils/structs/EnumerableSet.test.js +++ b/test/utils/structs/EnumerableSet.test.js @@ -27,6 +27,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)`, @@ -43,7 +44,7 @@ async function fixture() { return { mock, env }; } -describe('EnumerableSet', function () { +describe.only('EnumerableSet', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); From 613cd43d82d05ceded1d6e20174d86833b4d0967 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:28:25 -0500 Subject: [PATCH 02/10] remove `.only` --- test/utils/structs/EnumerableSet.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/structs/EnumerableSet.test.js b/test/utils/structs/EnumerableSet.test.js index e471fb5b683..1f92727a4c4 100644 --- a/test/utils/structs/EnumerableSet.test.js +++ b/test/utils/structs/EnumerableSet.test.js @@ -44,7 +44,7 @@ async function fixture() { return { mock, env }; } -describe.only('EnumerableSet', function () { +describe('EnumerableSet', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); From 0d455a819564604e14f2fcf3e8636004ad0c92b7 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:04:03 -0500 Subject: [PATCH 03/10] directly mutate set --- contracts/utils/structs/EnumerableSet.sol | 8 ++++++-- scripts/generate/templates/EnumerableSet.js | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index e78f844111d..3508cc73ecd 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.20; +import {Arrays} from "../Arrays.sol"; + /** * @dev Library for managing * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive @@ -121,9 +123,11 @@ library EnumerableSet { * 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 { - for (uint256 i = _length(set); i > 0; --i) { - _remove(set, _at(set, i - 1)); + uint256 len = _length(set); + for (uint256 i = 0; i < len; ++i) { + delete set._positions[set._values[i]]; } + Arrays.unsafeSetLength(set._values, 0); } /** diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index ca571abf043..a6ea99ba9cc 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -5,6 +5,8 @@ const { TYPES } = require('./EnumerableSet.opts'); const header = `\ pragma solidity ^0.8.20; +import {Arrays} from "../Arrays.sol"; + /** * @dev Library for managing * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive @@ -124,9 +126,11 @@ function _remove(Set storage set, bytes32 value) private returns (bool) { * 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 { - for (uint256 i = _length(set); i > 0; --i) { - _remove(set, _at(set, i - 1)); + uint256 len = _length(set); + for (uint256 i = 0; i < len; ++i) { + delete set._positions[set._values[i]]; } + Arrays.unsafeSetLength(set._values, 0); } /** From 6e03440e7479dcac5e11f9e77814ae0448fda35e Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:53:57 -0500 Subject: [PATCH 04/10] add changeset --- .changeset/sixty-tips-wink.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sixty-tips-wink.md diff --git a/.changeset/sixty-tips-wink.md b/.changeset/sixty-tips-wink.md new file mode 100644 index 00000000000..58e39bbec39 --- /dev/null +++ b/.changeset/sixty-tips-wink.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`EnumerableSet`: Add `clear` function to EnumerableSets which deletes all items in the set. From eb506bc31548c421f24bfd8ae8b2d98c1f2a4550 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 4 Feb 2025 10:06:01 +0100 Subject: [PATCH 05/10] EnumerableMap clear() --- contracts/utils/structs/EnumerableMap.sol | 95 ++++++++++++++++++++ scripts/generate/templates/EnumerableMap.js | 25 ++++++ test/utils/structs/EnumerableMap.behavior.js | 43 +++++++++ test/utils/structs/EnumerableMap.test.js | 1 + test/utils/structs/EnumerableSet.behavior.js | 16 +++- 5 files changed, 176 insertions(+), 4 deletions(-) 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/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/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index 37da41795dc..2e07abd82e2 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 item', 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 items', 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 set', async function () { + await this.methods.clear(); + }); + + it('clear then add value', 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 1d447f8d365..d8e62952066 100644 --- a/test/utils/structs/EnumerableSet.behavior.js +++ b/test/utils/structs/EnumerableSet.behavior.js @@ -113,22 +113,24 @@ function shouldBehaveLikeSet() { describe('clear', function () { it('clears a single item', async function () { await this.methods.add(this.valueA); - await expect(this.methods.length()).to.eventually.equal(1); await this.methods.clear(); - await expect(this.methods.length()).to.eventually.equal(0); + expect(await this.methods.contains(this.valueA)).to.be.false; + await expectMembersMatch(this.methods, []); }); it('clears multiple items', async function () { await this.methods.add(this.valueA); await this.methods.add(this.valueB); await this.methods.add(this.valueC); - await expect(this.methods.length()).to.eventually.equal(3); await this.methods.clear(); - await expect(this.methods.length()).to.eventually.equal(0); + 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 () { @@ -138,9 +140,15 @@ function shouldBehaveLikeSet() { 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]); }); }); From aa97a63cde1fde9f6084dcec3a0ba44f107ae610 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 4 Feb 2025 10:07:56 +0100 Subject: [PATCH 06/10] changeset --- .changeset/good-cameras-rush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/good-cameras-rush.md 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. From 996ac55bbc0632e7e25a2d5e680999c1c8bdcb6b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 4 Feb 2025 10:10:11 +0100 Subject: [PATCH 07/10] update --- contracts/utils/structs/EnumerableSet.sol | 3 ++- scripts/generate/templates/EnumerableSet.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 3508cc73ecd..31829df9a04 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -15,7 +15,8 @@ import {Arrays} from "../Arrays.sol"; * * - Elements are added, removed, and checked for existence in constant time * (O(1)). - * - Elements are enumerated and cleared in O(n). No guarantees are made on the ordering. + * - 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 { diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index a6ea99ba9cc..3169d6a46f5 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -16,7 +16,8 @@ import {Arrays} from "../Arrays.sol"; * * - Elements are added, removed, and checked for existence in constant time * (O(1)). - * - Elements are enumerated and cleared in O(n). No guarantees are made on the ordering. + * - 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 { From 712d579575fa18075889440a95f41c3118cc18ec Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 4 Feb 2025 10:14:50 +0100 Subject: [PATCH 08/10] Apply suggestions from code review --- test/utils/structs/EnumerableMap.behavior.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index 2e07abd82e2..c80eec934ba 100644 --- a/test/utils/structs/EnumerableMap.behavior.js +++ b/test/utils/structs/EnumerableMap.behavior.js @@ -118,7 +118,7 @@ function shouldBehaveLikeMap() { }); describe('clear', function () { - it('clears a single item', async function () { + it('clears a single entry', async function () { await this.methods.set(this.keyA, this.valueA); await this.methods.clear(); @@ -127,7 +127,7 @@ function shouldBehaveLikeMap() { await expectMembersMatch(this.methods, [], []); }); - it('clears multiple items', async function () { + 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); @@ -140,11 +140,11 @@ function shouldBehaveLikeMap() { await expectMembersMatch(this.methods, [], []); }); - it('does not revert on empty set', async function () { + it('does not revert on empty map', async function () { await this.methods.clear(); }); - it('clear then add value', async function () { + 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); From 199c971983debb5c092a560cf468f286067ddd32 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:45:33 -0500 Subject: [PATCH 09/10] `item` -> `value` --- .changeset/sixty-tips-wink.md | 2 +- test/utils/structs/EnumerableSet.behavior.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/sixty-tips-wink.md b/.changeset/sixty-tips-wink.md index 58e39bbec39..35c14cb890d 100644 --- a/.changeset/sixty-tips-wink.md +++ b/.changeset/sixty-tips-wink.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`EnumerableSet`: Add `clear` function to EnumerableSets which deletes all items in the set. +`EnumerableSet`: Add `clear` function to EnumerableSets which deletes all values in the set. diff --git a/test/utils/structs/EnumerableSet.behavior.js b/test/utils/structs/EnumerableSet.behavior.js index d8e62952066..fb932680cdd 100644 --- a/test/utils/structs/EnumerableSet.behavior.js +++ b/test/utils/structs/EnumerableSet.behavior.js @@ -111,7 +111,7 @@ function shouldBehaveLikeSet() { }); describe('clear', function () { - it('clears a single item', async function () { + it('clears a single value', async function () { await this.methods.add(this.valueA); await this.methods.clear(); @@ -120,7 +120,7 @@ function shouldBehaveLikeSet() { await expectMembersMatch(this.methods, []); }); - it('clears multiple items', async function () { + it('clears multiple values', async function () { await this.methods.add(this.valueA); await this.methods.add(this.valueB); await this.methods.add(this.valueC); From a1b70667e50610f7a6330251908367341ab73e67 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 4 Feb 2025 20:42:22 +0100 Subject: [PATCH 10/10] add clear for Bytes32x2Set --- contracts/utils/structs/EnumerableSet.sol | 18 ++++++++++++++++++ scripts/generate/templates/EnumerableSet.js | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 32ae8d565b9..7d7ba082520 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -488,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/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index e46e0792c55..e6a3d220276 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -329,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). */