Implementation of an address Enumerable Set (#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 <nicolas.venturo@gmail.com>
This commit is contained in:
committed by
Nicolás Venturo
parent
73abd54cbe
commit
1e0f07751e
@ -5,6 +5,7 @@
|
|||||||
### New features
|
### 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))
|
* `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))
|
* `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))
|
* `Create2`: simple library to make usage of the `CREATE2` opcode easier. ([#1744](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1744))
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|||||||
33
contracts/mocks/EnumerableSetMock.sol
Normal file
33
contracts/mocks/EnumerableSetMock.sol
Normal file
@ -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
contracts/utils/EnumerableSet.sol
Normal file
120
contracts/utils/EnumerableSet.sol
Normal file
@ -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)){
|
||||||
|
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];
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,8 @@ Miscellaneous contracts containing utility functions, often related to working w
|
|||||||
|
|
||||||
{{Arrays}}
|
{{Arrays}}
|
||||||
|
|
||||||
|
{{EnumerableSet}}
|
||||||
|
|
||||||
{{Create2}}
|
{{Create2}}
|
||||||
|
|
||||||
{{ReentrancyGuard}}
|
{{ReentrancyGuard}}
|
||||||
|
|||||||
@ -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`]!
|
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.
|
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
|
== 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 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
test/utils/EnumerableSet.test.js
Normal file
108
test/utils/EnumerableSet.test.js
Normal file
@ -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 ]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user