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

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
This commit is contained in:
Arr00
2025-02-10 12:02:43 -05:00
committed by GitHub
parent 441dc141ac
commit 3658269505
10 changed files with 326 additions and 0 deletions

View File

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

View File

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

View File

@ -16,6 +16,7 @@ import {EnumerableSet} from "./EnumerableSet.sol";
* - Entries are added, removed, and checked for existence in constant time * - Entries are added, removed, and checked for existence in constant time
* (O(1)). * (O(1)).
* - Entries are enumerated in O(n). No guarantees are made on the ordering. * - Entries are enumerated in O(n). No guarantees are made on the ordering.
* - Map can be cleared (all entries removed) in O(n).
* *
* ```solidity * ```solidity
* contract Example { * contract Example {
@ -90,6 +91,20 @@ library EnumerableMap {
return map._keys.remove(key); 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). * @dev Returns true if the key is in the map. O(1).
*/ */
@ -185,6 +200,16 @@ library EnumerableMap {
return remove(map._inner, bytes32(key)); 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). * @dev Returns true if the key is in the map. O(1).
*/ */
@ -278,6 +303,16 @@ library EnumerableMap {
return remove(map._inner, bytes32(key)); 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). * @dev Returns true if the key is in the map. O(1).
*/ */
@ -371,6 +406,16 @@ library EnumerableMap {
return remove(map._inner, bytes32(key)); 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). * @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)))); 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). * @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)))); 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). * @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)))); 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). * @dev Returns true if the key is in the map. O(1).
*/ */
@ -743,6 +818,16 @@ library EnumerableMap {
return remove(map._inner, key); 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). * @dev Returns true if the key is in the map. O(1).
*/ */
@ -836,6 +921,16 @@ library EnumerableMap {
return remove(map._inner, key); 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). * @dev Returns true if the key is in the map. O(1).
*/ */

View File

@ -4,6 +4,7 @@
pragma solidity ^0.8.20; pragma solidity ^0.8.20;
import {Arrays} from "../Arrays.sol";
import {Hashes} from "../cryptography/Hashes.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 * - Elements are added, removed, and checked for existence in constant time
* (O(1)). * (O(1)).
* - Elements are enumerated 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 * ```solidity
* contract Example { * 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). * @dev Returns true if the value is in the set. O(1).
*/ */
@ -182,6 +198,16 @@ library EnumerableSet {
return _remove(set._inner, value); 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). * @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)))); 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). * @dev Returns true if the value is in the set. O(1).
*/ */
@ -328,6 +364,16 @@ library EnumerableSet {
return _remove(set._inner, bytes32(value)); 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). * @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). * @dev Returns true if the value is in the self. O(1).
*/ */

View File

@ -17,6 +17,7 @@ import {EnumerableSet} from "./EnumerableSet.sol";
* - Entries are added, removed, and checked for existence in constant time * - Entries are added, removed, and checked for existence in constant time
* (O(1)). * (O(1)).
* - Entries are enumerated in O(n). No guarantees are made on the ordering. * - Entries are enumerated in O(n). No guarantees are made on the ordering.
* - Map can be cleared (all entries removed) in O(n).
* *
* \`\`\`solidity * \`\`\`solidity
* contract Example { * contract Example {
@ -91,6 +92,20 @@ function remove(Bytes32ToBytes32Map storage map, bytes32 key) internal returns (
return map._keys.remove(key); 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). * @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')}); 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). * @dev Returns true if the key is in the map. O(1).
*/ */

View File

@ -5,6 +5,7 @@ const { TYPES } = require('./EnumerableSet.opts');
const header = `\ const header = `\
pragma solidity ^0.8.20; pragma solidity ^0.8.20;
import {Arrays} from "../Arrays.sol";
import {Hashes} from "../cryptography/Hashes.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 * - Elements are added, removed, and checked for existence in constant time
* (O(1)). * (O(1)).
* - Elements are enumerated 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 * \`\`\`solidity
* contract Example { * 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). * @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')}); 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). * @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). * @dev Returns true if the value is in the self. O(1).
*/ */

View File

@ -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 () { describe('read', function () {
beforeEach(async function () { beforeEach(async function () {
await this.methods.set(this.keyA, this.valueA); await this.methods.set(this.keyA, this.valueA);

View File

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

View File

@ -109,6 +109,49 @@ function shouldBehaveLikeSet() {
expect(await this.methods.contains(this.valueB)).to.be.false; 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 = { module.exports = {

View File

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