diff --git a/contracts/drafts/EnumerableSet.sol b/contracts/drafts/EnumerableSet.sol new file mode 100644 index 000000000..14051719c --- /dev/null +++ b/contracts/drafts/EnumerableSet.sol @@ -0,0 +1,72 @@ +pragma solidity ^0.5.10; + + +/** + * @title EnumerableSet + * @dev Data structure + * @author Alberto Cuesta Cañada + */ +library EnumerableSet { + + event ValueAdded(address value); + event ValueRemoved(address value); + + struct Set { + // 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 Add a value. + */ + function add(Set storage set, address value) + internal + { + if (!contains(set, value)){ + set.index[value] = set.values.push(value); + emit ValueAdded(value); + } + } + + /** + * @dev Remove a value. + */ + function remove(Set storage set, address value) + internal + { + if (contains(set, value)) { + set.values[set.index[value] - 1] = set.values[set.values.length - 1]; + set.values.pop(); + delete set.index[value]; + emit ValueRemoved(value); + } + } + + /** + * @dev Returns true if the value is in the set. + */ + function contains(Set storage set, address value) + internal + view + returns (bool) + { + return set.index[value] != 0; + } + + /** + * @dev Return an array with all values in the set. + */ + function enumerate(Set 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; + } +} diff --git a/contracts/drafts/Enumerables.sol b/contracts/drafts/Enumerables.sol deleted file mode 100644 index 2dbabf451..000000000 --- a/contracts/drafts/Enumerables.sol +++ /dev/null @@ -1,191 +0,0 @@ -pragma solidity ^0.5.0; - - -/** - * @title Enumerables - * @dev This contract implements an enumerable address set as a doubly linked list. - * @author Alberto Cuesta Cañada - */ -library Enumerables { - - event ObjectCreated(uint256 id, address data); - event ObjectsLinked(uint256 prev, uint256 next); - event ObjectRemoved(uint256 id); - event NewHead(uint256 id); - event NewTail(uint256 id); - - struct Object{ - uint256 id; - uint256 next; - uint256 prev; - address data; - } - - struct Enumerable{ - uint256 head; - uint256 tail; - uint256 idCounter; - mapping (uint256 => Object) objects; - } - - /** - * @dev Retrieves the Object denoted by `id`. - */ - function get(Enumerable storage enumerable, uint256 id) - public - view - returns (uint256, uint256, uint256, address) - { - Object memory object = enumerable.objects[id]; - return (object.id, object.next, object.prev, object.data); - } - - /** - * @dev Insert a new Object as the new Head with `data` in the data field. - */ - function append(Enumerable storage enumerable, address data) - public - { - uint256 objectId = _createObject(enumerable, data); - if (enumerable.head == 0) { - _setHead(enumerable, objectId); - } else { - _link(enumerable, enumerable.head, objectId); - _setTail(enumerable, objectId); - } - } - - /** - * @dev Remove the Object denoted by `id` from the List. - */ - function remove(Enumerable storage enumerable, uint256 id) - public - { - Object memory removeObject = enumerable.objects[id]; - if (enumerable.head == id && enumerable.tail == id) { - _setHead(enumerable, 0); - _setTail(enumerable, 0); - } - else if (enumerable.head == id) { - _setHead(enumerable, removeObject.next); - enumerable.objects[removeObject.next].prev = 0; - } - else if (enumerable.tail == id) { - _setTail(enumerable, removeObject.prev); - enumerable.objects[removeObject.prev].next = 0; - } - else { - _link(enumerable, removeObject.prev, removeObject.next); - } - delete enumerable.objects[removeObject.id]; - emit ObjectRemoved(id); - } - - /** - * @dev Returns true if at least one Object matches `data` in the data field. - * TODO: What happens with address(0) as data? - */ - function contains(Enumerable storage enumerable, address data) - public - view - returns (bool) - { - Object memory object = enumerable.objects[enumerable.head]; - while (object.data != data) { - object = enumerable.objects[object.next]; - } - return object.data == data; - } - - /** - * @dev Returns the length of the enumerable. - */ - function length(Enumerable storage enumerable) - public - view - returns (uint256) - { - uint256 count = 0; - if (enumerable.head != 0){ - count += 1; - Object memory object = enumerable.objects[enumerable.head]; - while (object.id != enumerable.tail) { - count += 1; - object = enumerable.objects[object.next]; - } - } - return count; - } - - /** - * @dev Returns all the data fields in the enumerable, in an array ordered from head to tail. - */ - function enumerate(Enumerable storage enumerable) - public - view - returns (address[] memory) - { - uint256 count = length(enumerable); - address[] memory data = new address[](count); - Object memory object = enumerable.objects[enumerable.head]; - for (uint256 i = 0; i < count; i += 1){ - data[i] = enumerable.objects[object.id].data; - object = enumerable.objects[object.next]; - } - return data; - } - - /** - * @dev Internal function to update the Head pointer. - */ - function _setHead(Enumerable storage enumerable, uint256 id) - internal - { - enumerable.head = id; - emit NewHead(id); - } - - /** - * @dev Internal function to update the Tail pointer. - */ - function _setTail(Enumerable storage enumerable, uint256 id) - internal - { - enumerable.tail = id; - emit NewTail(id); - } - - /** - * @dev Internal function to create an unlinked Object. - */ - function _createObject(Enumerable storage enumerable, address data) - internal - returns (uint256) - { - enumerable.idCounter += 1; - uint256 newId = enumerable.idCounter; - Object memory object = Object( - newId, - 0, - 0, - data - ); - enumerable.objects[object.id] = object; - emit ObjectCreated( - object.id, - object.data - ); - return object.id; - } - - /** - * @dev Internal function to link an Object to another. - */ - function _link(Enumerable storage enumerable, uint256 prevId, uint256 nextId) - internal - { - enumerable.objects[prevId].next = nextId; - enumerable.objects[nextId].prev = prevId; - emit ObjectsLinked(prevId, nextId); - } -} diff --git a/contracts/mocks/EnumerableSetMock.sol b/contracts/mocks/EnumerableSetMock.sol new file mode 100644 index 000000000..7e4b9d085 --- /dev/null +++ b/contracts/mocks/EnumerableSetMock.sol @@ -0,0 +1,59 @@ +pragma solidity ^0.5.10; +import "../drafts/EnumerableSet.sol"; + + +/** + * @title EnumerableSetMock + * @dev Data structure + * @author Alberto Cuesta Cañada + */ +contract EnumerableSetMock{ + + using EnumerableSet for EnumerableSet.Set; + + EnumerableSet.Set private set; + + constructor() public { + set = EnumerableSet.Set({values: new address[](0)}); + } + + /** + * @dev Returns true if the value is in the set. + */ + function testContains(address value) + public + view + returns (bool) + { + return EnumerableSet.contains(set, value); + } + + /** + * @dev Insert an value as the new tail. + */ + function testAdd(address value) + public + { + EnumerableSet.add(set, value); + } + + /** + * @dev Remove an value. + */ + function testRemove(address remove) + public + { + EnumerableSet.remove(set, remove); + } + + /** + * @dev Return an array with all values in the set, from Head to Tail. + */ + function testEnumerate() + public + view + returns (address[] memory) + { + return EnumerableSet.enumerate(set); + } +} diff --git a/contracts/mocks/EnumerablesMock.sol b/contracts/mocks/EnumerablesMock.sol deleted file mode 100644 index 77de9fb76..000000000 --- a/contracts/mocks/EnumerablesMock.sol +++ /dev/null @@ -1,114 +0,0 @@ -pragma solidity ^0.5.0; -import "../drafts/Enumerables.sol"; - - -contract EnumerablesMock { - using Enumerables for Enumerables.Enumerable; - - Enumerables.Enumerable public enumerable; - - constructor() public { - enumerable = Enumerables.Enumerable(0, 0, 0); // Maybe have a factory method in the library. - } - - /** - * @dev Retrieves the Object denoted by `id`. - */ - function testGet(uint256 id) - public - view - returns (uint256, uint256, uint256, address) - { - return Enumerables.get(enumerable, id); - } - - /** - * @dev Insert a new Object as the new Head with `data` in the data field. - */ - function testAppend(address data) - public - { - Enumerables.append(enumerable, data); - } - - /** - * @dev Remove the Object denoted by `id` from the List. - */ - function testRemove(uint256 id) - public - { - Enumerables.remove(enumerable, id); - } - - /** - * @dev Returns true if at least one Object matches `data` in the data field. - * TODO: What happens with address(0) as data? - */ - function testContains(address data) - public - view - returns (bool) - { - - return Enumerables.contains(enumerable, data); - } - - /** - * @dev Returns the length of the enumerable. - */ - function testLength() - public - view - returns (uint256) - { - return Enumerables.length(enumerable); - } - - /** - * @dev Returns all the data fields in the enumerable, in an array ordered from head to tail. - */ - function enumerate() - public - view - returns (address[] memory) - { - return Enumerables.enumerate(enumerable); - } - - /** - * @dev Internal function to update the Head pointer. - */ - function testSetHead(uint256 id) - public - { - Enumerables._setHead(enumerable, id); - } - - /** - * @dev Internal function to update the Tail pointer. - */ - function testSetTail(uint256 id) - public - { - Enumerables._setTail(enumerable, id); - } - - /** - * @dev Internal function to create an unlinked Object. - */ - function testCreateObject(address data) - public - returns (uint256) - { - return Enumerables._createObject(enumerable, data); - } - - /** - * @dev Internal function to link an Object to another. - */ - function testLink(uint256 prevId, uint256 nextId) - public - { - Enumerables._link(enumerable, prevId, nextId); - } -} diff --git a/test/drafts/EnumerableSet.test.js b/test/drafts/EnumerableSet.test.js new file mode 100644 index 000000000..49a92fc0c --- /dev/null +++ b/test/drafts/EnumerableSet.test.js @@ -0,0 +1,54 @@ +const { contract } = require('@openzeppelin/test-environment'); + +const { expect } = require('chai'); + +const EnumerableSetMock = contract.fromArtifact('EnumerableSetMock'); + +const a = '0x0000000000000000000000000000000000000001'; +const b = '0x0000000000000000000000000000000000000002'; +const c = '0x0000000000000000000000000000000000000003'; + +/** @test {EnumerableSet} contract */ +describe('EnumerableSet', function () { + beforeEach(async function () { + this.enumerableSet = await EnumerableSetMock.new(); + }); + + it('contains can return false.', async function () { + expect(await this.enumerableSet.testContains(a)).to.be.false; + }); + + it('adds an value.', async function () { + await this.enumerableSet.testAdd(a); + expect(await this.enumerableSet.testContains(a)).to.be.true; + }); + + it('adds several values.', async function () { + await this.enumerableSet.testAdd(a); + await this.enumerableSet.testAdd(b); + expect(await this.enumerableSet.testContains(a)).to.be.true; + expect(await this.enumerableSet.testContains(b)).to.be.true; + expect(await this.enumerableSet.testContains(c)).to.be.false; + }); + + it('removes values.', async function () { + await this.enumerableSet.testAdd(a); + await this.enumerableSet.testRemove(a); + expect(await this.enumerableSet.testContains(a)).to.be.false; + }); + + it('Retrieve an empty array', async function () { + expect(await this.enumerableSet.testEnumerate()).to.be.empty; + }); + + it('Retrieve an array of values', async function () { + await this.enumerableSet.testAdd(a); + await this.enumerableSet.testAdd(b); + await this.enumerableSet.testAdd(c); + const result = (await this.enumerableSet.testEnumerate()); + expect(result.length).to.be.equal(3); + expect(result[0]).to.be.equal(a); + expect(result[1]).to.be.equal(b); + expect(result[2]).to.be.equal(c); + }); +}); diff --git a/test/drafts/Enumerables.test.js b/test/drafts/Enumerables.test.js deleted file mode 100644 index c5cbbf38f..000000000 --- a/test/drafts/Enumerables.test.js +++ /dev/null @@ -1,250 +0,0 @@ -const { contract } = require('@openzeppelin/test-environment'); -const { expectRevert, expectEvent } = require('@openzeppelin/test-helpers'); - -const { expect } = require('chai'); - -const EnumerablesMock = contract.fromArtifact('EnumerablesMock'); - -const emptyData = '0x0000000000000000000000000000000000000000'; -const headData = '0x0000000000000000000000000000000000000001'; -const middleData = '0x0000000000000000000000000000000000000002'; -const tailData = '0x0000000000000000000000000000000000000003'; - -describe('Enumerable', function () { - beforeEach(async function () { - this.enumerables = await EnumerablesMock.new(); - }); - - // it('starts at zero', async function () { - // expect(await this.counter.current()).to.be.bignumber.equal('0'); - // }); - - /** - * Test the two describe methods - * @test {Enumerable#set} and {Enumerable#get} - */ - it('Constructor variables.', async function () { - // expect(await this.enumerables.enumerable().idCounter()).toNumber()).to.be.equal(1); - }); - - it('get on a non existing object returns (0,0,0,0).', async function () { - const result = (await this.enumerables.testGet(0)); - expect(result[0].toNumber()).to.be.equal(0); - expect(result[1].toNumber()).to.be.equal(0); - expect(result[2].toNumber()).to.be.equal(0); - expect(result[3]).to.be.equal(emptyData); - }); - - it('appends an object at the head - event emission.', async function () { - const objectEvent = ( - await this.enumerables.testAppend(headData) - ).logs[0]; - expect(objectEvent.args.id.toNumber()).to.be.equal(1); - expect(objectEvent.args.data).to.be.equal(headData); - }); - - it('appends an object at the head - data storage.', async function () { - const objectId = ( - await this.enumerables.testAppend(headData) - ).logs[0].args.id.toNumber(); - - const result = (await this.enumerables.testGet(objectId)); - expect(result[0].toNumber()).to.be.equal(objectId); - expect(result[1].toNumber()).to.be.equal(0); - expect(result[2].toNumber()).to.be.equal(0); - expect(result[3]).to.be.equal(headData); - }); - - it('appends two objects.', async function () { - const objectOneId = ( - await this.enumerables.testAppend(middleData) - ).logs[0].args.id.toNumber(); - const objectTwoId = ( - await this.enumerables.testAppend(headData) - ).logs[0].args.id.toNumber(); - - const objectOne = (await this.enumerables.testGet(objectOneId)); - expect(objectOne[0].toNumber()).to.be.equal(objectOneId); - expect(objectOne[1].toNumber()).to.be.equal(0); - expect(objectOne[2].toNumber()).to.be.equal(objectTwoId); - expect(objectOne[3]).to.be.equal(middleData); - - const objectTwo = (await this.enumerables.testGet(objectTwoId)); - expect(objectTwo[0].toNumber()).to.be.equal(objectTwoId); - expect(objectTwo[1].toNumber()).to.be.equal(objectOneId); - expect(objectTwo[2].toNumber()).to.be.equal(0); - expect(objectTwo[3]).to.be.equal(headData); - - // expect(((await this.enumerables.head()).toNumber())).to.be.equal(objectTwoId); - }); -}); - -describe('Enumerable - length', (accounts) => { - beforeEach(async function () { - this.this.enumerables = await EnumerablesMock.new(); - }); - - it('Retrieves the length of an empty this.enumerables.', async function () { - const resultId = (await this.enumerables.testLength()); - expect(resultId.toNumber()).to.be.equal(0); - }); - - it('Retrieves the length of an this.enumerables.', async function () { - tailId = ( - await this.enumerables.testAppend(tailData) - ).logs[0].args.id.toNumber(); - middleId = ( - await this.enumerables.testAppend(middleData) - ).logs[0].args.id.toNumber(); - headId = ( - await this.enumerables.testAppend(headData) - ).logs[0].args.id.toNumber(); - const resultId = (await this.enumerables.testLength()); - expect(resultId.toNumber()).to.be.equal(3); - }); -}); - -/** @test {Enumerable} describe */ -describe('Enumerable - contains', (accounts) => { - beforeEach(async function () { - this.this.enumerables = await EnumerablesMock.new(); - tailId = ( - await this.enumerables.testAppend(tailData) - ).logs[0].args.id.toNumber(); - middleId = ( - await this.enumerables.testAppend(middleData) - ).logs[0].args.id.toNumber(); - }); - - it('Returns false for empty data.', async function () { - const resultId = (await this.enumerables.testContains(emptyData)); - expect(resultId).to.be.false; - }); - - it('Returns true for existing data.', async function () { - const resultId = (await this.enumerables.testContains(tailData)); - expect(resultId).to.be.true; - }); - - it('Returns false for non existing data.', async function () { - const resultId = (await this.enumerables.testContains(headData)); - expect(resultId).to.be.false; - }); -}); - -/** @test {Enumerable} describe */ -/* describe('Enumerable - enumerate', (accounts) => { - - let enumerable: EnumerablesMockInstance; - let headId: number; - let middleId: number; - let tailId: number; - - beforeEach(async function () { - enumerable = await EnumerablesMock.new(); - tailId = ( - await this.enumerables.testAppend(tailData) - ).logs[0].args.id.toNumber(); - middleId = ( - await this.enumerables.testAppend(middleData) - ).logs[0].args.id.toNumber(); - headId = ( - await this.enumerables.testAppend(headData) - ).logs[0].args.id.toNumber(); - }); - - it('finds an id for given data.', async function () { - let resultId = (await this.enumerables.findIdForData(headData)); - expect(resultId.toNumber()).to.be.equal(headId); - resultId = (await this.enumerables.findIdForData(middleData)); - expect(resultId.toNumber()).to.be.equal(middleId); - resultId = (await this.enumerables.findIdForData(tailData)); - expect(resultId.toNumber()).to.be.equal(tailId); - }); -}); */ - -/** @test {Enumerable} describe */ -describe('Enumerable - remove', (accounts) => { - beforeEach(async function () { - this.this.enumerables = await EnumerablesMock.new(); - tailId = ( - await this.enumerables.testAppend(tailData) - ).logs[0].args.id.toNumber(); - middleId = ( - await this.enumerables.testAppend(middleData) - ).logs[0].args.id.toNumber(); - headId = ( - await this.enumerables.testAppend(headData) - ).logs[0].args.id.toNumber(); - }); - - it('removes the head.', async function () { - const removedId = ( - await this.enumerables.testRemove(headId) - ).logs[1].args.id.toNumber(); - // expect(((await this.enumerables.head()).toNumber())).to.be.equal(middleId); - - const middleObject = (await this.enumerables.testGet(middleId)); - expect(middleObject[0].toNumber()).to.be.equal(middleId); - expect(middleObject[1].toNumber()).to.be.equal(tailId); - expect(middleObject[2].toNumber()).to.be.equal(0); - expect(middleObject[3]).to.be.equal(middleData); - - const tailObject = (await this.enumerables.testGet(tailId)); - expect(tailObject[0].toNumber()).to.be.equal(tailId); - expect(tailObject[1].toNumber()).to.be.equal(0); - expect(tailObject[2].toNumber()).to.be.equal(middleId); - expect(tailObject[3]).to.be.equal(tailData); - }); - - it('removes the tail.', async function () { - const removedId = ( - await this.enumerables.testRemove(tailId) - ).logs[1].args.id.toNumber(); - // expect(((await this.enumerables.head()).toNumber())).to.be.equal(headId); - - const headObject = (await this.enumerables.testGet(headId)); - expect(headObject[0].toNumber()).to.be.equal(headId); - expect(headObject[1].toNumber()).to.be.equal(middleId); - expect(headObject[2].toNumber()).to.be.equal(0); - expect(headObject[3]).to.be.equal(headData); - - const middleObject = (await this.enumerables.testGet(middleId)); - expect(middleObject[0].toNumber()).to.be.equal(middleId); - expect(middleObject[1].toNumber()).to.be.equal(0); - expect(middleObject[2].toNumber()).to.be.equal(headId); - expect(middleObject[3]).to.be.equal(middleData); - }); - - it('removes the middle.', async function () { - const removedId = ( - await this.enumerables.testRemove(middleId) - ).logs[1].args.id.toNumber(); - // expect(((await this.enumerables.head()).toNumber())).to.be.equal(headId); - - const headObject = (await this.enumerables.testGet(headId)); - expect(headObject[0].toNumber()).to.be.equal(headId); - expect(headObject[1].toNumber()).to.be.equal(tailId); - expect(headObject[2].toNumber()).to.be.equal(0); - expect(headObject[3]).to.be.equal(headData); - - const tailObject = (await this.enumerables.testGet(tailId)); - expect(tailObject[0].toNumber()).to.be.equal(tailId); - expect(tailObject[1].toNumber()).to.be.equal(0); - expect(tailObject[2].toNumber()).to.be.equal(headId); - expect(tailObject[3]).to.be.equal(tailData); - }); - - it('removes all.', async function () { - (await this.enumerables.testRemove(headId)).logs[1].args.id.toNumber(); - // expect(((await this.enumerables.head()).toNumber())).to.be.equal(middleId); - - (await this.enumerables.testRemove(tailId)).logs[1].args.id.toNumber(); - // expect(((await this.enumerables.head()).toNumber())).to.be.equal(middleId); - // expect(((await this.enumerables.tail()).toNumber())).to.be.equal(middleId); - - (await this.enumerables.testRemove(middleId)).logs[1].args.id.toNumber(); - // expect(((await this.enumerables.head()).toNumber())).to.be.equal(0); - // expect(((await this.enumerables.tail()).toNumber())).to.be.equal(0); - }); -});