Skip to content

Commit 58b3dc5

Browse files
alcuecanventuro
authored andcommitted
Implementation of an address Enumerable Set (OpenZeppelin#2061)
* Drafted Enumerable.sol. * Drafted test framework. * Tweaked the tests to follow oz structure. * Coded EnumerableSet. * Moved EnumerableSet to `utils`. * Fixed linting. * Improved comments. * Tweaked contract description. * Renamed struct to AddressSet. * Relaxed version pragma to 0.5.0 * Removed events. * Revert on useless operations. * Small comment. * Created AddressSet factory method. * Failed transactions return false. * Transactions now return false on failure. * Remove comments from mock * Rename mock functions * Adapt tests to code style, use test-helpers * Fix bug in remove, improve tests. * Add changelog entry * Add entry on Utils doc * Add optimization for removal of last slot * Update docs * Fix headings of utilities documentation Co-authored-by: Nicolás Venturo <[email protected]>
1 parent 55f6cf0 commit 58b3dc5

File tree

6 files changed

+272
-3
lines changed

6 files changed

+272
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### New features
66
* `SafeCast.toUintXX`: new library for integer downcasting, which allows for safe operation on smaller types (e.g. `uint32`) when combined with `SafeMath`. ([#1926](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1926))
77
* `ERC721Metadata`: added `baseURI`, which can be used for dramatic gas savings when all token URIs share a prefix (e.g. `http://api.myapp.com/tokens/<id>`). ([#1970](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/1970))
8+
* `EnumerableSet`: new library for storing enumerable sets of values. Only `AddressSet` is supported in this release. ([#2061](https://github.com/OpenZeppelin/openzeppelin-solidity/pull/2061))
89
* `Create2`: simple library to make usage of the `CREATE2` opcode easier. ([#1744](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1744))
910

1011
### Improvements

contracts/mocks/EnumerableSetMock.sol

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
pragma solidity ^0.5.0;
2+
3+
import "../utils/EnumerableSet.sol";
4+
5+
contract EnumerableSetMock{
6+
using EnumerableSet for EnumerableSet.AddressSet;
7+
8+
event TransactionResult(bool result);
9+
10+
EnumerableSet.AddressSet private set;
11+
12+
constructor() public {
13+
set = EnumerableSet.newAddressSet();
14+
}
15+
16+
function contains(address value) public view returns (bool) {
17+
return EnumerableSet.contains(set, value);
18+
}
19+
20+
function add(address value) public {
21+
bool result = EnumerableSet.add(set, value);
22+
emit TransactionResult(result);
23+
}
24+
25+
function remove(address value) public {
26+
bool result = EnumerableSet.remove(set, value);
27+
emit TransactionResult(result);
28+
}
29+
30+
function enumerate() public view returns (address[] memory) {
31+
return EnumerableSet.enumerate(set);
32+
}
33+
}

contracts/utils/EnumerableSet.sol

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
pragma solidity ^0.5.0;
2+
3+
/**
4+
* @dev Library for managing
5+
* https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
6+
* types.
7+
*
8+
* Sets have the following properties:
9+
*
10+
* - Elements are added, removed, and checked for existence in constant time
11+
* (O(1)).
12+
* - Elements are enumerated in O(n). No guarantees are made on the ordering.
13+
*
14+
* As of v2.5.0, only `address` sets are supported.
15+
*
16+
* Include with `using EnumerableSet for EnumerableSet.AddressSet;`, and use
17+
* {newAddressSet} to create a new `AddressSet`.
18+
*
19+
* _Available since v2.5.0._
20+
*
21+
* @author Alberto Cuesta Cañada
22+
*/
23+
library EnumerableSet {
24+
25+
struct AddressSet {
26+
// Position of the value in the `values` array, plus 1 because index 0
27+
// means a value is not in the set.
28+
mapping (address => uint256) index;
29+
address[] values;
30+
}
31+
32+
/**
33+
* @dev Creates a new empty address set.
34+
*/
35+
function newAddressSet()
36+
internal
37+
pure
38+
returns (AddressSet memory)
39+
{
40+
return AddressSet({values: new address[](0)});
41+
}
42+
43+
/**
44+
* @dev Add a value to a set. O(1).
45+
* Returns false if the value was already in the set.
46+
*/
47+
function add(AddressSet storage set, address value)
48+
internal
49+
returns (bool)
50+
{
51+
if (!contains(set, value)){
52+
set.index[value] = set.values.push(value);
53+
return true;
54+
} else {
55+
return false;
56+
}
57+
}
58+
59+
/**
60+
* @dev Removes a value from a set. O(1).
61+
* Returns false if the value was not present in the set.
62+
*/
63+
function remove(AddressSet storage set, address value)
64+
internal
65+
returns (bool)
66+
{
67+
if (contains(set, value)){
68+
uint256 toDeleteIndex = set.index[value] - 1;
69+
uint256 lastIndex = set.values.length - 1;
70+
71+
// If the element we're deleting is the last one, we can just remove it without doing a swap
72+
if (lastIndex != toDeleteIndex) {
73+
address lastValue = set.values[lastIndex];
74+
75+
// Move the last value to the index where the deleted value is
76+
set.values[toDeleteIndex] = lastValue;
77+
// Update the index for the moved value
78+
set.index[lastValue] = toDeleteIndex + 1; // All indexes are 1-based
79+
}
80+
81+
// Delete the index entry for the deleted value
82+
delete set.index[value];
83+
84+
// Delete the old entry for the moved value
85+
set.values.pop();
86+
87+
return true;
88+
} else {
89+
return false;
90+
}
91+
}
92+
93+
/**
94+
* @dev Returns true if the value is in the set. O(1).
95+
*/
96+
function contains(AddressSet storage set, address value)
97+
internal
98+
view
99+
returns (bool)
100+
{
101+
return set.index[value] != 0;
102+
}
103+
104+
/**
105+
* @dev Returns an array with all values in the set. O(N).
106+
* Note that there are no guarantees on the ordering of values inside the
107+
* array, and it may change when more values are added or removed.
108+
*/
109+
function enumerate(AddressSet storage set)
110+
internal
111+
view
112+
returns (address[] memory)
113+
{
114+
address[] memory output = new address[](set.values.length);
115+
for (uint256 i; i < set.values.length; i++){
116+
output[i] = set.values[i];
117+
}
118+
return output;
119+
}
120+
}

contracts/utils/README.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Miscellaneous contracts containing utility functions, often related to working w
1010

1111
{{Arrays}}
1212

13+
{{EnumerableSet}}
14+
1315
{{Create2}}
1416

1517
{{ReentrancyGuard}}

docs/modules/ROOT/pages/utilities.adoc

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,18 @@ Easy!
8383

8484
Want to split some payments between multiple people? Maybe you have an app that sends 30% of art purchases to the original creator and 70% of the profits to the current owner; you can build that with xref:api:payment.adoc#PaymentSplitter[`PaymentSplitter`]!
8585

86-
In solidity, there are some security concerns with blindly sending money to accounts, since it allows them to execute arbitrary code. You can read up on these security concerns in the https://consensys.github.io/smart-contract-best-practices/[Ethereum Smart Contract Best Practices] website. One of the ways to fix reentrancy and stalling problems is, instead of immediately sending Ether to accounts that need it, you can use xref:api:payment.adoc#PullPayment[`PullPayment`], which offers an xref:api:payment.adoc#PullPayment-_asyncTransfer-address-uint256-[`_asyncTransfer`] function for sending money to something and requesting that they xref:api:payment.adoc#PullPayment-withdrawPayments-address-payable-[`withdrawPayments()`] it later.
86+
In Solidity, there are some security concerns with blindly sending money to accounts, since it allows them to execute arbitrary code. You can read up on these security concerns in the https://consensys.github.io/smart-contract-best-practices/[Ethereum Smart Contract Best Practices] website. One of the ways to fix reentrancy and stalling problems is, instead of immediately sending Ether to accounts that need it, you can use xref:api:payment.adoc#PullPayment[`PullPayment`], which offers an xref:api:payment.adoc#PullPayment-_asyncTransfer-address-uint256-[`_asyncTransfer`] function for sending money to something and requesting that they xref:api:payment.adoc#PullPayment-withdrawPayments-address-payable-[`withdrawPayments()`] it later.
8787

8888
If you want to Escrow some funds, check out xref:api:payment.adoc#Escrow[`Escrow`] and xref:api:payment.adoc#ConditionalEscrow[`ConditionalEscrow`] for governing the release of some escrowed Ether.
8989

90+
[[collections]]
91+
== Collections
92+
93+
If you need support for more powerful collections than Solidity's native arrays and mappings, take a look at xref:api:utils.adoc#EnumerableSet[`EnumerableSet`]. It is similar to a mapping in that it stores and removes elements in constant time and doesn't allow for repeated entries, but it also supports _enumeration_, which means you can easily query all elements of the set both on and off-chain.
94+
9095
[[misc]]
91-
=== Misc
96+
== Misc
9297

9398
Want to check if an address is a contract? Use xref:api:utils.adoc#Address[`Address`] and xref:api:utils.adoc#Address-isContract-address-[`Address.isContract()`].
9499

95-
Want to keep track of some numbers that increment by 1 every time you want another one? Check out xref:api:drafts.adoc#Counter[`Counter`]. This is especially useful for creating incremental ERC721 `tokenId` s like we did in the last section.
100+
Want to keep track of some numbers that increment by 1 every time you want another one? Check out xref:api:drafts.adoc#Counter[`Counter`]. This is useful for lots of things, like creating incremental identifiers, as shown on the xref:721.adoc[ERC721 guide].

test/utils/EnumerableSet.test.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
const { accounts, contract } = require('@openzeppelin/test-environment');
2+
const { expectEvent } = require('@openzeppelin/test-helpers');
3+
const { expect } = require('chai');
4+
5+
const EnumerableSetMock = contract.fromArtifact('EnumerableSetMock');
6+
7+
describe('EnumerableSet', function () {
8+
const [ accountA, accountB, accountC ] = accounts;
9+
10+
beforeEach(async function () {
11+
this.set = await EnumerableSetMock.new();
12+
});
13+
14+
it('starts empty', async function () {
15+
expect(await this.set.contains(accountA)).to.equal(false);
16+
expect(await this.set.enumerate()).to.have.same.members([]);
17+
});
18+
19+
it('adds a value', async function () {
20+
const receipt = await this.set.add(accountA);
21+
expectEvent(receipt, 'TransactionResult', { result: true });
22+
23+
expect(await this.set.contains(accountA)).to.equal(true);
24+
expect(await this.set.enumerate()).to.have.same.members([ accountA ]);
25+
});
26+
27+
it('adds several values', async function () {
28+
await this.set.add(accountA);
29+
await this.set.add(accountB);
30+
31+
expect(await this.set.contains(accountA)).to.equal(true);
32+
expect(await this.set.contains(accountB)).to.equal(true);
33+
34+
expect(await this.set.contains(accountC)).to.equal(false);
35+
36+
expect(await this.set.enumerate()).to.have.same.members([ accountA, accountB ]);
37+
});
38+
39+
it('returns false when adding elements already in the set', async function () {
40+
await this.set.add(accountA);
41+
42+
const receipt = (await this.set.add(accountA));
43+
expectEvent(receipt, 'TransactionResult', { result: false });
44+
45+
expect(await this.set.enumerate()).to.have.same.members([ accountA ]);
46+
});
47+
48+
it('removes added values', async function () {
49+
await this.set.add(accountA);
50+
51+
const receipt = await this.set.remove(accountA);
52+
expectEvent(receipt, 'TransactionResult', { result: true });
53+
54+
expect(await this.set.contains(accountA)).to.equal(false);
55+
expect(await this.set.enumerate()).to.have.same.members([]);
56+
});
57+
58+
it('returns false when removing elements not in the set', async function () {
59+
const receipt = await this.set.remove(accountA);
60+
expectEvent(receipt, 'TransactionResult', { result: false });
61+
62+
expect(await this.set.contains(accountA)).to.equal(false);
63+
});
64+
65+
it('adds and removes multiple values', async function () {
66+
// []
67+
68+
await this.set.add(accountA);
69+
await this.set.add(accountC);
70+
71+
// [A, C]
72+
73+
await this.set.remove(accountA);
74+
await this.set.remove(accountB);
75+
76+
// [C]
77+
78+
await this.set.add(accountB);
79+
80+
// [C, B]
81+
82+
await this.set.add(accountA);
83+
await this.set.remove(accountC);
84+
85+
// [A, B]
86+
87+
await this.set.add(accountA);
88+
await this.set.add(accountB);
89+
90+
// [A, B]
91+
92+
await this.set.add(accountC);
93+
await this.set.remove(accountA);
94+
95+
// [B, C]
96+
97+
await this.set.add(accountA);
98+
await this.set.remove(accountB);
99+
100+
// [A, C]
101+
102+
expect(await this.set.contains(accountA)).to.equal(true);
103+
expect(await this.set.contains(accountB)).to.equal(false);
104+
expect(await this.set.contains(accountC)).to.equal(true);
105+
106+
expect(await this.set.enumerate()).to.have.same.members([ accountA, accountC ]);
107+
});
108+
});

0 commit comments

Comments
 (0)