Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of an address Enumerable Set #2061

Merged
merged 27 commits into from
Jan 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b74bb85
Drafted Enumerable.sol.
alcueca Jan 15, 2020
e3227bf
Drafted test framework.
alcueca Jan 15, 2020
3a02d0d
Tweaked the tests to follow oz structure.
alcueca Jan 16, 2020
429cb6d
Coded EnumerableSet.
alcueca Jan 20, 2020
ef381e6
Moved EnumerableSet to `utils`.
alcueca Jan 20, 2020
bf335ca
Fixed linting.
alcueca Jan 20, 2020
580bbdd
Improved comments.
alcueca Jan 21, 2020
3700a34
Tweaked contract description.
alcueca Jan 21, 2020
8379d53
Merge branch 'master' into fix/enumerable-#1240
alcueca Jan 21, 2020
8b50c02
Renamed struct to AddressSet.
alcueca Jan 22, 2020
71d1716
Relaxed version pragma to 0.5.0
alcueca Jan 23, 2020
dfdf794
Removed events.
alcueca Jan 23, 2020
41cb935
Revert on useless operations.
alcueca Jan 23, 2020
31e9a54
Small comment.
alcueca Jan 23, 2020
79b98a4
Created AddressSet factory method.
alcueca Jan 23, 2020
be49c33
Failed transactions return false.
alcueca Jan 23, 2020
b770d89
Transactions now return false on failure.
alcueca Jan 23, 2020
332636d
Remove comments from mock
nventuro Jan 23, 2020
07c99bc
Rename mock functions
nventuro Jan 23, 2020
35e2af5
Adapt tests to code style, use test-helpers
nventuro Jan 23, 2020
305d714
Fix bug in remove, improve tests.
nventuro Jan 24, 2020
6742bcd
Add changelog entry
nventuro Jan 24, 2020
bc1e5b2
Add entry on Utils doc
nventuro Jan 24, 2020
81e1bf5
Add optimization for removal of last slot
nventuro Jan 24, 2020
40f6dbd
Merge branch 'master' into fix/enumerable-#1240
nventuro Jan 24, 2020
4ddcacd
Update docs
nventuro Jan 24, 2020
6a14ec4
Fix headings of utilities documentation
nventuro Jan 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### New features
* `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))
* `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))
* `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))
* `Create2`: simple library to make usage of the `CREATE2` opcode easier. ([#1744](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1744))

### Improvements
Expand Down
33 changes: 33 additions & 0 deletions contracts/mocks/EnumerableSetMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
pragma solidity ^0.5.0;

import "../utils/EnumerableSet.sol";

contract EnumerableSetMock{
using EnumerableSet for EnumerableSet.AddressSet;

event TransactionResult(bool result);

EnumerableSet.AddressSet private set;

constructor() public {
set = EnumerableSet.newAddressSet();
}

function contains(address value) public view returns (bool) {
return EnumerableSet.contains(set, value);
}

function add(address value) public {
bool result = EnumerableSet.add(set, value);
emit TransactionResult(result);
}

function remove(address value) public {
bool result = EnumerableSet.remove(set, value);
emit TransactionResult(result);
}

function enumerate() public view returns (address[] memory) {
return EnumerableSet.enumerate(set);
}
}
120 changes: 120 additions & 0 deletions contracts/utils/EnumerableSet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
pragma solidity ^0.5.0;

/**
* @dev Library for managing
* https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
* types.
*
* Sets have the following properties:
*
* - 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.
*
* As of v2.5.0, only `address` sets are supported.
*
* Include with `using EnumerableSet for EnumerableSet.AddressSet;`, and use
* {newAddressSet} to create a new `AddressSet`.
*
* _Available since v2.5.0._
*
* @author Alberto Cuesta Cañada
*/
library EnumerableSet {

struct AddressSet {
// Position of the value in the `values` array, plus 1 because index 0
// means a value is not in the set.
mapping (address => uint256) index;
address[] values;
}

/**
* @dev Creates a new empty address set.
*/
function newAddressSet()
internal
pure
returns (AddressSet memory)
{
return AddressSet({values: new address[](0)});
}

/**
* @dev Add a value to a set. O(1).
* Returns false if the value was already in the set.
*/
function add(AddressSet storage set, address value)
internal
returns (bool)
{
if (!contains(set, value)){
alcueca marked this conversation as resolved.
Show resolved Hide resolved
set.index[value] = set.values.push(value);
return true;
} else {
return false;
}
}

/**
* @dev Removes a value from a set. O(1).
* Returns false if the value was not present in the set.
*/
function remove(AddressSet storage set, address value)
internal
returns (bool)
{
if (contains(set, value)){
uint256 toDeleteIndex = set.index[value] - 1;
uint256 lastIndex = set.values.length - 1;

// If the element we're deleting is the last one, we can just remove it without doing a swap
if (lastIndex != toDeleteIndex) {
address lastValue = set.values[lastIndex];

// Move the last value to the index where the deleted value is
set.values[toDeleteIndex] = lastValue;
// Update the index for the moved value
set.index[lastValue] = toDeleteIndex + 1; // All indexes are 1-based
}

// Delete the index entry for the deleted value
delete set.index[value];
nventuro marked this conversation as resolved.
Show resolved Hide resolved

// Delete the old entry for the moved value
set.values.pop();

return true;
} else {
return false;
}
}

/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(AddressSet storage set, address value)
internal
view
returns (bool)
{
return set.index[value] != 0;
}

/**
* @dev Returns an array with all values in the set. O(N).
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*/
function enumerate(AddressSet storage set)
internal
view
returns (address[] memory)
{
address[] memory output = new address[](set.values.length);
for (uint256 i; i < set.values.length; i++){
output[i] = set.values[i];
}
return output;
}
}
2 changes: 2 additions & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Miscellaneous contracts containing utility functions, often related to working w

{{Arrays}}

{{EnumerableSet}}

{{Create2}}

{{ReentrancyGuard}}
11 changes: 8 additions & 3 deletions docs/modules/ROOT/pages/utilities.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,18 @@ Easy!

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`]!

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.
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.

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.

[[collections]]
== Collections

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.

[[misc]]
=== Misc
== Misc

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()`].

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.
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].
108 changes: 108 additions & 0 deletions test/utils/EnumerableSet.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const { accounts, contract } = require('@openzeppelin/test-environment');
const { expectEvent } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');

const EnumerableSetMock = contract.fromArtifact('EnumerableSetMock');

describe('EnumerableSet', function () {
const [ accountA, accountB, accountC ] = accounts;

beforeEach(async function () {
this.set = await EnumerableSetMock.new();
});

it('starts empty', async function () {
expect(await this.set.contains(accountA)).to.equal(false);
expect(await this.set.enumerate()).to.have.same.members([]);
});

it('adds a value', async function () {
const receipt = await this.set.add(accountA);
expectEvent(receipt, 'TransactionResult', { result: true });

expect(await this.set.contains(accountA)).to.equal(true);
expect(await this.set.enumerate()).to.have.same.members([ accountA ]);
});

it('adds several values', async function () {
await this.set.add(accountA);
await this.set.add(accountB);

expect(await this.set.contains(accountA)).to.equal(true);
expect(await this.set.contains(accountB)).to.equal(true);

expect(await this.set.contains(accountC)).to.equal(false);

expect(await this.set.enumerate()).to.have.same.members([ accountA, accountB ]);
});

it('returns false when adding elements already in the set', async function () {
await this.set.add(accountA);

const receipt = (await this.set.add(accountA));
expectEvent(receipt, 'TransactionResult', { result: false });

expect(await this.set.enumerate()).to.have.same.members([ accountA ]);
});

it('removes added values', async function () {
await this.set.add(accountA);

const receipt = await this.set.remove(accountA);
expectEvent(receipt, 'TransactionResult', { result: true });

expect(await this.set.contains(accountA)).to.equal(false);
expect(await this.set.enumerate()).to.have.same.members([]);
});

it('returns false when removing elements not in the set', async function () {
const receipt = await this.set.remove(accountA);
expectEvent(receipt, 'TransactionResult', { result: false });

expect(await this.set.contains(accountA)).to.equal(false);
});

it('adds and removes multiple values', async function () {
// []

await this.set.add(accountA);
await this.set.add(accountC);

// [A, C]

await this.set.remove(accountA);
await this.set.remove(accountB);

// [C]

await this.set.add(accountB);

// [C, B]

await this.set.add(accountA);
await this.set.remove(accountC);

// [A, B]

await this.set.add(accountA);
await this.set.add(accountB);

// [A, B]

await this.set.add(accountC);
await this.set.remove(accountA);

// [B, C]

await this.set.add(accountA);
await this.set.remove(accountB);

// [A, C]

expect(await this.set.contains(accountA)).to.equal(true);
expect(await this.set.contains(accountB)).to.equal(false);
expect(await this.set.contains(accountC)).to.equal(true);

expect(await this.set.enumerate()).to.have.same.members([ accountA, accountC ]);
});
});