diff --git a/.eslintrc b/.eslintrc index 56e2872..f6b4d3e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,10 +19,10 @@ "web3": false }, "rules": { - + // Strict mode "strict": [2, "global"], - + // Code style "indent": [2, 4], "quotes": [2, "single"], @@ -44,7 +44,7 @@ "no-debugger": 0, "no-undef": 2, "object-curly-spacing": [2, "always"], - "max-len": [2, 120, 2], + "max-len": [2, 130, 2], "generator-star-spacing": ["error", "before"], "promise/avoid-new": 0, "promise/always-return": 0 diff --git a/contracts/Multiownable.sol b/contracts/Multiownable.sol index 3ea93bf..6d917e6 100644 --- a/contracts/Multiownable.sol +++ b/contracts/Multiownable.sol @@ -1,46 +1,82 @@ pragma solidity ^0.4.23; +import "./Set.sol"; + contract Multiownable { + using Set for Set.Data; + + uint256 public constant MAX_PENDING_OPERATIONS_PER_OWNER = 20; + // VARIABLES - uint256 public ownersGeneration; + uint256 public nextOwnerId = 1; // Unique for every reowning uint256 public howManyOwnersDecide; - address[] public owners; - bytes32[] public allOperations; + Set.Data internal owners; + Set.Data internal operations; + mapping(uint256 => Set.Data) internal operationsByOwnerId; + mapping(address => uint256) public ownerIds; + mapping(bytes32 => uint256) public dataHashGeneration; + mapping(bytes32 => uint256) public votesCountByOperation; + mapping(bytes32 => bytes) public dataByOperation; + address internal insideCallSender; uint256 internal insideCallCount; - - // Reverse lookup tables for owners and allOperations - mapping(address => uint) public ownersIndices; // Starts from 1 - mapping(bytes32 => uint) public allOperationsIndicies; - - // Owners voting mask per operations - mapping(bytes32 => uint256) public votesMaskByOperation; - mapping(bytes32 => uint256) public votesCountByOperation; - + // EVENTS - event OwnershipTransferred(address[] previousOwners, uint howManyOwnersDecide, address[] newOwners, uint newHowManyOwnersDecide); + event OwnersAdded(address[] newOwners, uint newHowManyOwnersDecide); + event OwnerRemoved(address oldOwner, uint newHowManyOwnersDecide); event OperationCreated(bytes32 operation, uint howMany, uint ownersCount, address proposer); event OperationUpvoted(bytes32 operation, uint votes, uint howMany, uint ownersCount, address upvoter); event OperationPerformed(bytes32 operation, uint howMany, uint ownersCount, address performer); event OperationDownvoted(bytes32 operation, uint votes, uint ownersCount, address downvoter); event OperationCancelled(bytes32 operation, address lastCanceller); - + // ACCESSORS - function isOwner(address wallet) public constant returns(bool) { - return ownersIndices[wallet] > 0; + function isOwner(address wallet) public view returns(bool) { + return owners.contains(bytes32(wallet)); + } + + function ownersLength() public view returns(uint) { + return owners.length(); + } + + function ownerAt(uint i) public view returns(address) { + return address(owners.at(i)); + } + + function allOwners() public view returns(bytes32[]) { + return owners.items; + } + + function operationsLength() public view returns(uint) { + return operations.length(); + } + + function operationAt(uint i) public view returns(bytes32) { + return operations.at(i); + } + + function allOperations() public view returns(bytes32[]) { + return operations.items; } - function ownersCount() public constant returns(uint) { - return owners.length; + function ownerOperationsLength(address theOwner) public view returns(uint256) { + uint256 ownerId = ownerIds[theOwner]; + return operationsByOwnerId[ownerId].length(); } - function allOperationsCount() public constant returns(uint) { - return allOperations.length; + function ownerOperationsAt(address theOwner, uint i) public view returns(bytes32) { + uint256 ownerId = ownerIds[theOwner]; + return operationsByOwnerId[ownerId].at(i); + } + + function allOwnerOperations(address theOwner) public view returns(bytes32[]) { + uint256 ownerId = ownerIds[theOwner]; + return operationsByOwnerId[ownerId].items; } // MODIFIERS @@ -49,17 +85,17 @@ contract Multiownable { * @dev Allows to perform method by any of the owners */ modifier onlyAnyOwner { - if (checkHowManyOwners(1)) { - bool update = (insideCallSender == address(0)); - if (update) { - insideCallSender = msg.sender; - insideCallCount = 1; - } - _; - if (update) { - insideCallSender = address(0); - insideCallCount = 0; - } + require(isOwner(msg.sender)); + + bool update = (insideCallSender == address(0)); + if (update) { + insideCallSender = msg.sender; + insideCallCount = 1; + } + _; + if (update) { + insideCallSender = address(0); + insideCallCount = 0; } } @@ -67,7 +103,7 @@ contract Multiownable { * @dev Allows to perform method only after many owners call it with the same arguments */ modifier onlyManyOwners { - if (checkHowManyOwners(howManyOwnersDecide)) { + if (_voteAndCheck(howManyOwnersDecide)) { bool update = (insideCallSender == address(0)); if (update) { insideCallSender = msg.sender; @@ -85,11 +121,11 @@ contract Multiownable { * @dev Allows to perform method only after all owners call it with the same arguments */ modifier onlyAllOwners { - if (checkHowManyOwners(owners.length)) { + if (_voteAndCheck(owners.length())) { bool update = (insideCallSender == address(0)); if (update) { insideCallSender = msg.sender; - insideCallCount = owners.length; + insideCallCount = owners.length(); } _; if (update) { @@ -104,9 +140,9 @@ contract Multiownable { */ modifier onlySomeOwners(uint howMany) { require(howMany > 0, "onlySomeOwners: howMany argument is zero"); - require(howMany <= owners.length, "onlySomeOwners: howMany argument exceeds the number of owners"); - - if (checkHowManyOwners(howMany)) { + require(howMany <= owners.length(), "onlySomeOwners: howMany argument exceeds the number of owners"); + + if (_voteAndCheck(howMany)) { bool update = (insideCallSender == address(0)); if (update) { insideCallSender = msg.sender; @@ -123,117 +159,166 @@ contract Multiownable { // CONSTRUCTOR constructor() public { - owners.push(msg.sender); - ownersIndices[msg.sender] = 1; + owners.add(bytes32(msg.sender)); howManyOwnersDecide = 1; } + // PRIVATE METHODS + + function _deleteOperation(bytes32 operation) private { + operations.remove(operation); + delete dataByOperation[operation]; + delete votesCountByOperation[operation]; + } + // INTERNAL METHODS /** * @dev onlyManyOwners modifier helper */ - function checkHowManyOwners(uint howMany) internal returns(bool) { + function _voteAndCheck(uint howMany) internal returns(bool) { if (insideCallSender == msg.sender) { require(howMany <= insideCallCount, "checkHowManyOwners: nested owners modifier check require more owners"); return true; } - uint ownerIndex = ownersIndices[msg.sender] - 1; - require(ownerIndex < owners.length, "checkHowManyOwners: msg.sender is not an owner"); - bytes32 operation = keccak256(msg.data, ownersGeneration); + require(owners.contains(bytes32(msg.sender))); + uint256 ownerId = ownerIds[msg.sender]; + bytes32 calldataHash = keccak256(msg.data); + bytes32 operation = bytes32(uint256(calldataHash) + dataHashGeneration[calldataHash]); - require((votesMaskByOperation[operation] & (2 ** ownerIndex)) == 0, "checkHowManyOwners: owner already voted for the operation"); - votesMaskByOperation[operation] |= (2 ** ownerIndex); uint operationVotesCount = votesCountByOperation[operation] + 1; votesCountByOperation[operation] = operationVotesCount; + require(operationsByOwnerId[ownerId].add(operation), "checkHowManyOwners: owner already voted for the operation"); + require(operationsByOwnerId[ownerId].length() <= MAX_PENDING_OPERATIONS_PER_OWNER, + "checkHowManyOwners: owner already have MAX allowed pending operations"); if (operationVotesCount == 1) { - allOperationsIndicies[operation] = allOperations.length; - allOperations.push(operation); - emit OperationCreated(operation, howMany, owners.length, msg.sender); + operations.add(operation); + dataByOperation[operation] = msg.data; + emit OperationCreated(operation, howMany, owners.length(), msg.sender); } - emit OperationUpvoted(operation, operationVotesCount, howMany, owners.length, msg.sender); + emit OperationUpvoted(operation, operationVotesCount, howMany, owners.length(), msg.sender); // If enough owners confirmed the same operation - if (votesCountByOperation[operation] == howMany) { - deleteOperation(operation); - emit OperationPerformed(operation, howMany, owners.length, msg.sender); + if (votesCountByOperation[operation] >= howMany) { + _deleteOperation(operation); + dataHashGeneration[calldataHash]++; + emit OperationPerformed(operation, howMany, owners.length(), msg.sender); return true; } return false; } - /** - * @dev Used to delete cancelled or performed operation - * @param operation defines which operation to delete - */ - function deleteOperation(bytes32 operation) internal { - uint index = allOperationsIndicies[operation]; - if (index < allOperations.length - 1) { // Not last - allOperations[index] = allOperations[allOperations.length - 1]; - allOperationsIndicies[allOperations[index]] = index; + function _cancelOperation(bytes32 operation, address theOwner) internal { + uint256 ownerId = ownerIds[theOwner]; + + require(operationsByOwnerId[ownerId].remove(operation), "_cancelOperation: operation not found for this user"); + + uint operationVotesCount = votesCountByOperation[operation] - 1; + votesCountByOperation[operation] = operationVotesCount; + emit OperationDownvoted(operation, operationVotesCount, owners.length(), msg.sender); + if (operationVotesCount == 0) { + _deleteOperation(operation); + emit OperationCancelled(operation, msg.sender); } - allOperations.length--; + } - delete votesMaskByOperation[operation]; - delete votesCountByOperation[operation]; - delete allOperationsIndicies[operation]; + function _addOwner(address newOwner) internal { + require(newOwner != address(0), "_addOwner: owners array contains zero"); + require(owners.add(bytes32(newOwner)), "_addOwner: owners array contains duplicates"); + ownerIds[newOwner] = nextOwnerId++; + howManyOwnersDecide += 1; + } + + function _removeOwner(address theOwner) internal { + require(owners.remove(bytes32(theOwner)), "_removeOwner: theOwner do not exist"); + howManyOwnersDecide -= ((howManyOwnersDecide > 1) ? 1 : 0); + uint256 ownerId = ownerIds[theOwner]; + for (uint i = operationsByOwnerId[ownerId].length(); i > 0; i--) { + bytes32 operation = operationsByOwnerId[ownerId].at(i - 1); + _cancelOperation(operation, theOwner); + } + emit OwnerRemoved(theOwner, howManyOwnersDecide); + } + + function _setHowManyOwnersDecide(uint howMany) internal { + require(howMany > 0, "_setHowManyOwnersDecide: howMany equal to 0"); + require(howMany <= owners.length(), "_setHowManyOwnersDecide: howMany exceeds the number of owners"); + howManyOwnersDecide = howMany; } // PUBLIC METHODS /** - * @dev Allows owners to change their mind by cacnelling votesMaskByOperation operations - * @param operation defines which operation to delete + * @dev Allows owners to change their mind by cancelling votedByOperationAndIndex operations + * @param operation defines which operation to cancel */ - function cancelPending(bytes32 operation) public onlyAnyOwner { - uint ownerIndex = ownersIndices[msg.sender] - 1; - require((votesMaskByOperation[operation] & (2 ** ownerIndex)) != 0, "cancelPending: operation not found for this user"); - votesMaskByOperation[operation] &= ~(2 ** ownerIndex); - uint operationVotesCount = votesCountByOperation[operation] - 1; - votesCountByOperation[operation] = operationVotesCount; - emit OperationDownvoted(operation, operationVotesCount, owners.length, msg.sender); - if (operationVotesCount == 0) { - deleteOperation(operation); - emit OperationCancelled(operation, msg.sender); - } + function cancelOperation(bytes32 operation) public onlyAnyOwner { + return _cancelOperation(operation, msg.sender); } /** - * @dev Allows owners to change ownership + * @dev Allows owners to add new owners * @param newOwners defines array of addresses of new owners */ - function transferOwnership(address[] newOwners) public { - transferOwnershipWithHowMany(newOwners, newOwners.length); + function addOwners(address[] newOwners) public { + addOwnersWithHowMany(newOwners, howManyOwnersDecide + newOwners.length); } /** - * @dev Allows owners to change ownership + * @dev Allows owners to remove themselves + */ + function resignOwnership() public onlyAnyOwner { + require(owners.length() > 1); + _removeOwner(msg.sender); + } + + /** + * @dev Allows owners to remove other owners + */ + function removeOwners(address[] theOwners) public { + removeOwnersWithHowMany(theOwners, howManyOwnersDecide > theOwners.length ? howManyOwnersDecide - theOwners.length : 1); + } + + /** + * @dev Allows owners to add new owners * @param newOwners defines array of addresses of new owners * @param newHowManyOwnersDecide defines how many owners can decide */ - function transferOwnershipWithHowMany(address[] newOwners, uint256 newHowManyOwnersDecide) public onlyManyOwners { - require(newOwners.length > 0, "transferOwnershipWithHowMany: owners array is empty"); - require(newOwners.length <= 256, "transferOwnershipWithHowMany: owners count is greater then 256"); - require(newHowManyOwnersDecide > 0, "transferOwnershipWithHowMany: newHowManyOwnersDecide equal to 0"); - require(newHowManyOwnersDecide <= newOwners.length, "transferOwnershipWithHowMany: newHowManyOwnersDecide exceeds the number of owners"); - - // Reset owners reverse lookup table - for (uint j = 0; j < owners.length; j++) { - delete ownersIndices[owners[j]]; - } + function addOwnersWithHowMany(address[] newOwners, uint256 newHowManyOwnersDecide) public onlyManyOwners { for (uint i = 0; i < newOwners.length; i++) { - require(newOwners[i] != address(0), "transferOwnershipWithHowMany: owners array contains zero"); - require(ownersIndices[newOwners[i]] == 0, "transferOwnershipWithHowMany: owners array contains duplicates"); - ownersIndices[newOwners[i]] = i + 1; + _addOwner(newOwners[i]); } - - emit OwnershipTransferred(owners, howManyOwnersDecide, newOwners, newHowManyOwnersDecide); - owners = newOwners; - howManyOwnersDecide = newHowManyOwnersDecide; - allOperations.length = 0; - ownersGeneration++; + _setHowManyOwnersDecide(newHowManyOwnersDecide); + emit OwnersAdded(newOwners, howManyOwnersDecide); + } + + /** + * @dev Allows owners to transfer ownership to new ones + * @param newOwners defines array of addresses of new owners + * @param howMany defines how many owners can decide + */ + function transferOwnershipWithHowMany(address[] newOwners, uint256 howMany) public onlyManyOwners { + require(newOwners.length > 0, "transferOwnershipWithHowMany: newOwners length should be at least 1"); + for (uint i = owners.length(); i > 0; i--) { + address oldOwner = address(owners.at(i - 1)); + _removeOwner(oldOwner); + } + addOwnersWithHowMany(newOwners, howMany); + } + + /** + * @dev Allows owners to remove other owners + * @param theOwners defines array of addresses of old owners + * @param howMany defines how many owners can decide + */ + function removeOwnersWithHowMany(address[] theOwners, uint256 howMany) public onlyManyOwners { + require(owners.length() > theOwners.length); + for (uint i = 0; i < theOwners.length; i++) { + _removeOwner(theOwners[i]); + } + _setHowManyOwnersDecide(howMany); } } diff --git a/contracts/Set.sol b/contracts/Set.sol new file mode 100644 index 0000000..fd80231 --- /dev/null +++ b/contracts/Set.sol @@ -0,0 +1,46 @@ +pragma solidity ^0.4.23; + + +library Set { + + struct Data { + bytes32[] items; + mapping(bytes32 => uint) lookup; + } + + function length(Data storage s) internal view returns(uint) { + return s.items.length; + } + + function at(Data storage s, uint index) internal view returns(bytes32) { + return s.items[index]; + } + + function contains(Data storage s, bytes32 item) internal view returns(bool) { + return s.lookup[item] != 0; + } + + function add(Data storage s, bytes32 item) public returns(bool) { + if (s.lookup[item] > 0) { + return false; + } + s.lookup[item] = s.items.push(item); + return true; + } + + function remove(Data storage s, bytes32 item) public returns(bool) { + uint index = s.lookup[item]; + if (index == 0) { + return false; + } + if (index < s.items.length) { + bytes32 lastItem = s.items[s.items.length - 1]; + s.items[index - 1] = lastItem; + s.lookup[lastItem] = index; + } + s.items.length -= 1; + delete s.lookup[item]; + return true; + } + +} diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index a670e45..63b81e8 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -1,5 +1,7 @@ const Multiownable = artifacts.require('Multiownable'); +const Set = artifacts.require('Set'); -module.exports = function (deployer) { +module.exports = async function (deployer) { + Multiownable.link('Set', (await Set.new()).address); deployer.deploy(Multiownable); }; diff --git a/test/MultiAttack.js b/test/MultiAttack.js index 6543b8e..5795e90 100644 --- a/test/MultiAttack.js +++ b/test/MultiAttack.js @@ -7,16 +7,22 @@ require('chai') .use(require('chai-bignumber')(web3.BigNumber)) .should(); +const Set = artifacts.require('Set.sol'); const MultiAttackable = artifacts.require('./impl/MultiAttackable.sol'); const MultiAttacker = artifacts.require('./impl/MultiAttacker.sol'); contract('MultiAttack', function ([_, wallet1, wallet2, wallet3, wallet4, wallet5]) { + before(async function () { + MultiAttackable.link('Set', (await Set.new()).address); + }); + it('should handle reentracy attack', async function () { const victim = await MultiAttackable.new(); const hacker = await MultiAttacker.new(); // Prepare victim wallet - await victim.transferOwnership([wallet1, wallet2]); + await victim.addOwners([wallet1, wallet2]); + await victim.resignOwnership({ from: _ }); await web3.eth.sendTransaction({ from: _, to: victim.address, value: ether(3) }); // Try reentrace attack diff --git a/test/Multiownable.js b/test/Multiownable.js index c723e61..e441033 100644 --- a/test/Multiownable.js +++ b/test/Multiownable.js @@ -6,306 +6,642 @@ require('chai') .use(require('chai-bignumber')(web3.BigNumber)) .should(); +const Set = artifacts.require('Set.sol'); const Multiownable = artifacts.require('Multiownable.sol'); const MultiownableImpl = artifacts.require('./impl/MultiownableImpl.sol'); contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, wallet5]) { - it('should be initialized correctly', async function () { - const obj = await Multiownable.new(); + before(async function () { + Multiownable.link('Set', (await Set.new()).address); + MultiownableImpl.link('Set', (await Set.new()).address); + }); - (await obj.owners.call(0)).should.be.equal(_); - (await obj.ownersCount.call()).should.be.bignumber.equal(1); + it('should be initialized', async function () { + const obj = await Multiownable.new(); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); + (await obj.ownersLength.call()).should.be.bignumber.equal(1); + (await obj.ownerAt.call(0)).should.be.equal(_); (await obj.isOwner.call(_)).should.be.true; (await obj.isOwner.call(wallet1)).should.be.false; (await obj.isOwner.call(wallet2)).should.be.false; (await obj.isOwner.call(wallet3)).should.be.false; - (await obj.isOwner.call(wallet4)).should.be.false; - (await obj.isOwner.call(wallet5)).should.be.false; + (await obj.allOwners.call()).should.be.deep.equal([ + '0x000000000000000000000000' + _.substr(2), + ]); }); - it('should transfer ownership 1 => 1 correctly', async function () { - const obj = await Multiownable.new(); - - await obj.transferOwnership([wallet1]); + describe('addOwners', async function () { + it('should add 1 mandatory owner', async function () { + const obj = await Multiownable.new(); - (await obj.owners.call(0)).should.be.equal(wallet1); - (await obj.ownersCount.call()).should.be.bignumber.equal(1); - - (await obj.isOwner.call(_)).should.be.false; - (await obj.isOwner.call(wallet1)).should.be.true; - (await obj.isOwner.call(wallet2)).should.be.false; - (await obj.isOwner.call(wallet3)).should.be.false; - (await obj.isOwner.call(wallet4)).should.be.false; - (await obj.isOwner.call(wallet5)).should.be.false; - }); + await obj.addOwners([wallet1]); - it('should transfer ownership 1 => 2 correctly', async function () { - const obj = await Multiownable.new(); + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownerAt.call(1)).should.be.equal(wallet1); + (await obj.ownersLength.call()).should.be.bignumber.equal(2); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(2); - await obj.transferOwnership([wallet1, wallet2]); + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.true; + (await obj.isOwner.call(wallet2)).should.be.false; + (await obj.isOwner.call(wallet3)).should.be.false; + }); - (await obj.owners.call(0)).should.be.equal(wallet1); - (await obj.owners.call(1)).should.be.equal(wallet2); - (await obj.ownersCount.call()).should.be.bignumber.equal(2); + it('should increase howMany respectively 2 => 4 for 2 => 4 owners', async function () { + const obj = await Multiownable.new(); - (await obj.isOwner.call(_)).should.be.false; - (await obj.isOwner.call(wallet1)).should.be.true; - (await obj.isOwner.call(wallet2)).should.be.true; - (await obj.isOwner.call(wallet3)).should.be.false; - (await obj.isOwner.call(wallet4)).should.be.false; - (await obj.isOwner.call(wallet5)).should.be.false; - }); + await obj.addOwnersWithHowMany([wallet1], 2); - it('should transfer ownership 1 => 3 correctly', async function () { - const obj = await Multiownable.new(); + (await obj.ownersLength.call()).should.be.bignumber.equal(2); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(2); - await obj.transferOwnership([wallet1, wallet2, wallet3]); + await obj.addOwners([wallet2, wallet3], { from: _ }); + await obj.addOwners([wallet2, wallet3], { from: wallet1 }); - (await obj.owners.call(0)).should.be.equal(wallet1); - (await obj.owners.call(1)).should.be.equal(wallet2); - (await obj.owners.call(2)).should.be.equal(wallet3); - (await obj.ownersCount.call()).should.be.bignumber.equal(3); + (await obj.ownersLength.call()).should.be.bignumber.equal(4); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(4); + }); - (await obj.isOwner.call(_)).should.be.false; - (await obj.isOwner.call(wallet1)).should.be.true; - (await obj.isOwner.call(wallet2)).should.be.true; - (await obj.isOwner.call(wallet3)).should.be.true; - (await obj.isOwner.call(wallet4)).should.be.false; - (await obj.isOwner.call(wallet5)).should.be.false; - }); + it('should increase howMany respectively 1 => 3 for 2 => 4 owners', async function () { + const obj = await Multiownable.new(); - it('should transfer ownership 2 => 1 correctly', async function () { - const obj = await Multiownable.new(); + await obj.addOwnersWithHowMany([wallet1], 1); - await obj.transferOwnership([wallet1, wallet2]); - await obj.transferOwnership([wallet3], { from: wallet1 }); - await obj.transferOwnership([wallet3], { from: wallet2 }); + (await obj.ownersLength.call()).should.be.bignumber.equal(2); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); - (await obj.owners.call(0)).should.be.equal(wallet3); - (await obj.ownersCount.call()).should.be.bignumber.equal(1); + await obj.addOwners([wallet2, wallet3], { from: _ }); + await obj.addOwners([wallet2, wallet3], { from: wallet1 }); - (await obj.isOwner.call(_)).should.be.false; - (await obj.isOwner.call(wallet1)).should.be.false; - (await obj.isOwner.call(wallet2)).should.be.false; - (await obj.isOwner.call(wallet3)).should.be.true; - (await obj.isOwner.call(wallet4)).should.be.false; - (await obj.isOwner.call(wallet5)).should.be.false; + (await obj.ownersLength.call()).should.be.bignumber.equal(4); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(3); + }); }); - it('should transfer ownership 3 => 1 correctly', async function () { - const obj = await Multiownable.new(); + describe('addOwnersWithHowMany', async function () { + it('should fail with too low howMany arg', async function () { + const obj = await Multiownable.new(); + await obj.addOwnersWithHowMany([wallet1], 0).should.be.rejectedWith(EVMRevert); + }); - await obj.transferOwnership([wallet1, wallet2, wallet3]); - await obj.transferOwnership([wallet4], { from: wallet1 }); - await obj.transferOwnership([wallet4], { from: wallet2 }); - await obj.transferOwnership([wallet4], { from: wallet3 }); + it('should fail with too high howMany arg', async function () { + const obj = await Multiownable.new(); + await obj.addOwnersWithHowMany([wallet1], 3).should.be.rejectedWith(EVMRevert); + }); - (await obj.owners.call(0)).should.be.equal(wallet4); - (await obj.ownersCount.call()).should.be.bignumber.equal(1); + it('should add 1 optional owner', async function () { + const obj = await Multiownable.new(); - (await obj.isOwner.call(_)).should.be.false; - (await obj.isOwner.call(wallet1)).should.be.false; - (await obj.isOwner.call(wallet2)).should.be.false; - (await obj.isOwner.call(wallet3)).should.be.false; - (await obj.isOwner.call(wallet4)).should.be.true; - (await obj.isOwner.call(wallet5)).should.be.false; - }); + await obj.addOwnersWithHowMany([wallet1], 1); - it('should transfer ownership 2 => 2 correctly', async function () { - const obj = await Multiownable.new(); + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownerAt.call(1)).should.be.equal(wallet1); + (await obj.ownersLength.call()).should.be.bignumber.equal(2); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); - await obj.transferOwnership([wallet1, wallet2]); - await obj.transferOwnership([wallet3, wallet4], { from: wallet1 }); - await obj.transferOwnership([wallet3, wallet4], { from: wallet2 }); + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.true; + (await obj.isOwner.call(wallet2)).should.be.false; + (await obj.isOwner.call(wallet3)).should.be.false; + }); - (await obj.owners.call(0)).should.be.equal(wallet3); - (await obj.owners.call(1)).should.be.equal(wallet4); - (await obj.ownersCount.call()).should.be.bignumber.equal(2); + it('should add 2 mandatory owners', async function () { + const obj = await Multiownable.new(); - (await obj.isOwner.call(_)).should.be.false; - (await obj.isOwner.call(wallet1)).should.be.false; - (await obj.isOwner.call(wallet2)).should.be.false; - (await obj.isOwner.call(wallet3)).should.be.true; - (await obj.isOwner.call(wallet4)).should.be.true; - (await obj.isOwner.call(wallet5)).should.be.false; - }); + await obj.addOwners([wallet1, wallet2]); - it('should transfer ownership 2 => 3 correctly', async function () { - const obj = await Multiownable.new(); + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownerAt.call(1)).should.be.equal(wallet1); + (await obj.ownerAt.call(2)).should.be.equal(wallet2); + (await obj.ownersLength.call()).should.be.bignumber.equal(3); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(3); - await obj.transferOwnership([wallet1, wallet2]); - await obj.transferOwnership([wallet3, wallet4, wallet5], { from: wallet1 }); - await obj.transferOwnership([wallet3, wallet4, wallet5], { from: wallet2 }); + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.true; + (await obj.isOwner.call(wallet2)).should.be.true; + (await obj.isOwner.call(wallet3)).should.be.false; + }); - (await obj.owners.call(0)).should.be.equal(wallet3); - (await obj.owners.call(1)).should.be.equal(wallet4); - (await obj.owners.call(2)).should.be.equal(wallet5); - (await obj.ownersCount.call()).should.be.bignumber.equal(3); + it('should add 2 owners with 1 mandatory', async function () { + const obj = await Multiownable.new(); - (await obj.isOwner.call(_)).should.be.false; - (await obj.isOwner.call(wallet1)).should.be.false; - (await obj.isOwner.call(wallet2)).should.be.false; - (await obj.isOwner.call(wallet3)).should.be.true; - (await obj.isOwner.call(wallet4)).should.be.true; - (await obj.isOwner.call(wallet5)).should.be.true; - }); + await obj.addOwnersWithHowMany([wallet1, wallet2], 2); - it('should transfer ownership 3 => 2 correctly', async function () { - const obj = await Multiownable.new(); + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownerAt.call(1)).should.be.equal(wallet1); + (await obj.ownerAt.call(2)).should.be.equal(wallet2); + (await obj.ownersLength.call()).should.be.bignumber.equal(3); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(2); - await obj.transferOwnership([wallet1, wallet2, wallet3]); - await obj.transferOwnership([wallet4, wallet5], { from: wallet1 }); - await obj.transferOwnership([wallet4, wallet5], { from: wallet2 }); - await obj.transferOwnership([wallet4, wallet5], { from: wallet3 }); + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.true; + (await obj.isOwner.call(wallet2)).should.be.true; + (await obj.isOwner.call(wallet3)).should.be.false; + }); - (await obj.owners.call(0)).should.be.equal(wallet4); - (await obj.owners.call(1)).should.be.equal(wallet5); - (await obj.ownersCount.call()).should.be.bignumber.equal(2); + it('should add 2 optional owners', async function () { + const obj = await Multiownable.new(); + + await obj.addOwnersWithHowMany([wallet1, wallet2], 1); + + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownerAt.call(1)).should.be.equal(wallet1); + (await obj.ownerAt.call(2)).should.be.equal(wallet2); + (await obj.ownersLength.call()).should.be.bignumber.equal(3); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); - (await obj.isOwner.call(_)).should.be.false; - (await obj.isOwner.call(wallet1)).should.be.false; - (await obj.isOwner.call(wallet2)).should.be.false; - (await obj.isOwner.call(wallet3)).should.be.false; - (await obj.isOwner.call(wallet4)).should.be.true; - (await obj.isOwner.call(wallet5)).should.be.true; - }); + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.true; + (await obj.isOwner.call(wallet2)).should.be.true; + (await obj.isOwner.call(wallet3)).should.be.false; + }); + + it('should add 1 owners by 2 owners', async function () { + const obj = await Multiownable.new(); + + await obj.addOwners([wallet1]); + await obj.addOwners([wallet2], { from: _ }); + await obj.addOwners([wallet2], { from: wallet1 }); + + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownerAt.call(1)).should.be.equal(wallet1); + (await obj.ownerAt.call(2)).should.be.equal(wallet2); + (await obj.ownersLength.call()).should.be.bignumber.equal(3); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(3); + + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.true; + (await obj.isOwner.call(wallet2)).should.be.true; + (await obj.isOwner.call(wallet3)).should.be.false; + }); + + it('should add 2 owners by 2 owners', async function () { + const obj = await Multiownable.new(); + + await obj.addOwners([wallet1]); + await obj.addOwners([wallet2, wallet3], { from: wallet1 }); + await obj.addOwners([wallet2, wallet3], { from: _ }); + + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownerAt.call(1)).should.be.equal(wallet1); + (await obj.ownerAt.call(2)).should.be.equal(wallet2); + (await obj.ownerAt.call(3)).should.be.equal(wallet3); + (await obj.ownersLength.call()).should.be.bignumber.equal(4); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(4); + + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.true; + (await obj.isOwner.call(wallet2)).should.be.true; + (await obj.isOwner.call(wallet3)).should.be.true; + }); + }); + + describe('removeOwners', async function () { + it('should remove first of 2 owners', async function () { + const obj = await Multiownable.new(); + + await obj.addOwners([wallet1]); + await obj.removeOwnersWithHowMany([_], 1, { from: _ }); + await obj.removeOwnersWithHowMany([_], 1, { from: wallet1 }); + + (await obj.ownerAt.call(0)).should.be.equal(wallet1); + (await obj.ownersLength.call()).should.be.bignumber.equal(1); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); + + (await obj.isOwner.call(_)).should.be.false; + (await obj.isOwner.call(wallet1)).should.be.true; + (await obj.isOwner.call(wallet2)).should.be.false; + (await obj.isOwner.call(wallet3)).should.be.false; + }); + + it('should remove last of 2 owners', async function () { + const obj = await Multiownable.new(); + + await obj.addOwners([wallet1]); + await obj.removeOwnersWithHowMany([wallet1], 1, { from: _ }); + await obj.removeOwnersWithHowMany([wallet1], 1, { from: wallet1 }); + + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownersLength.call()).should.be.bignumber.equal(1); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); + + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.false; + (await obj.isOwner.call(wallet2)).should.be.false; + (await obj.isOwner.call(wallet3)).should.be.false; + }); + + it('should not remove all owners', async function () { + const obj = await Multiownable.new(); + + await obj.addOwners([wallet1]); + await obj.removeOwnersWithHowMany([_, wallet1], 1, { from: _ }); + await obj.removeOwnersWithHowMany([_, wallet1], 1, { from: wallet1 }).should.be.rejectedWith(EVMRevert); + + await obj.addOwners([wallet2], { from: _ }); + await obj.addOwners([wallet2], { from: wallet1 }); + await obj.removeOwnersWithHowMany([_, wallet1, wallet2], 1, { from: _ }); + await obj.removeOwnersWithHowMany([_, wallet1, wallet2], 1, { from: wallet1 }); + await obj.removeOwnersWithHowMany([_, wallet1, wallet2], 1, { from: wallet2 }).should.be.rejectedWith(EVMRevert); + }); + + it('should remove mid of 3 owners', async function () { + const obj = await Multiownable.new(); + + await obj.addOwners([wallet1, wallet2]); + await obj.removeOwnersWithHowMany([wallet1], 2, { from: _ }); + await obj.removeOwnersWithHowMany([wallet1], 2, { from: wallet1 }); + await obj.removeOwnersWithHowMany([wallet1], 2, { from: wallet2 }); + + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownerAt.call(1)).should.be.equal(wallet2); + (await obj.ownersLength.call()).should.be.bignumber.equal(2); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(2); + + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.false; + (await obj.isOwner.call(wallet2)).should.be.true; + (await obj.isOwner.call(wallet3)).should.be.false; + }); + + it('should remove one of 2 owners with howMany=1', async function () { + const obj = await Multiownable.new(); + + await obj.addOwnersWithHowMany([wallet1], 1); + await obj.removeOwners([wallet1], { from: _ }); // howMany=1 + + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownersLength.call()).should.be.bignumber.equal(1); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); // ! + + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.false; + (await obj.isOwner.call(wallet2)).should.be.false; + (await obj.isOwner.call(wallet3)).should.be.false; + }); + + it('should not be able to remove wrong owner from unsafe subcontract', async function () { + const obj = await MultiownableImpl.new(); + + await obj.unsafeResignOwnership({ from: wallet1 }).should.be.rejectedWith(EVMRevert); + }); + }); - it('should transfer ownership 1,2 of 3 => 2 correctly', async function () { - const obj = await Multiownable.new(); - - await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet3], 2); - await obj.transferOwnership([wallet4, wallet5], { from: wallet1 }); - await obj.transferOwnership([wallet4, wallet5], { from: wallet2 }); + describe('resignOwnership', async function () { + it('should remove first of 2 owners', async function () { + const obj = await Multiownable.new(); - (await obj.owners.call(0)).should.be.equal(wallet4); - (await obj.owners.call(1)).should.be.equal(wallet5); - (await obj.ownersCount.call()).should.be.bignumber.equal(2); - }); - - it('should transfer ownership 2,3 of 3 => 2 correctly', async function () { - const obj = await Multiownable.new(); + await obj.addOwners([wallet1]); + await obj.resignOwnership({ from: _ }); - await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet3], 2); - await obj.transferOwnership([wallet4, wallet5], { from: wallet2 }); - await obj.transferOwnership([wallet4, wallet5], { from: wallet3 }); - - (await obj.owners.call(0)).should.be.equal(wallet4); - (await obj.owners.call(1)).should.be.equal(wallet5); - (await obj.ownersCount.call()).should.be.bignumber.equal(2); - }); - - it('should transfer ownership 1,3 of 3 => 2 correctly', async function () { - const obj = await Multiownable.new(); + (await obj.ownerAt.call(0)).should.be.equal(wallet1); + (await obj.ownersLength.call()).should.be.bignumber.equal(1); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); - await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet3], 2); - await obj.transferOwnership([wallet4, wallet5], { from: wallet1 }); - await obj.transferOwnership([wallet4, wallet5], { from: wallet3 }); - - (await obj.owners.call(0)).should.be.equal(wallet4); - (await obj.owners.call(1)).should.be.equal(wallet5); - (await obj.ownersCount.call()).should.be.bignumber.equal(2); - }); - - it('should not transfer ownership with wrong how many argument', async function () { - const obj = await Multiownable.new(); - - await obj.transferOwnershipWithHowMany([wallet1], 0).should.be.rejectedWith(EVMRevert); - await obj.transferOwnershipWithHowMany([wallet1, wallet2], 3).should.be.rejectedWith(EVMRevert); - await obj.transferOwnershipWithHowMany([wallet1, wallet2], 4).should.be.rejectedWith(EVMRevert); - }); - - it('should correctly manage allOperations array', async function () { - const obj = await Multiownable.new(); + (await obj.isOwner.call(_)).should.be.false; + (await obj.isOwner.call(wallet1)).should.be.true; + (await obj.isOwner.call(wallet2)).should.be.false; + (await obj.isOwner.call(wallet3)).should.be.false; + }); - // Transfer ownership 1 => 1 - (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); - await obj.transferOwnership([wallet1]); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); - - // Transfer ownership 1 => 2 - (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); - await obj.transferOwnership([wallet2, wallet3], { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); - - // Transfer ownership 2 => 2 - (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); - await obj.transferOwnership([wallet4, wallet5], { from: wallet2 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); - await obj.transferOwnership([wallet4, wallet5], { from: wallet3 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); - }); + it('should remove last of 2 owners', async function () { + const obj = await Multiownable.new(); - it('should allow to cancel pending operations', async function () { - const obj = await Multiownable.new(); - await obj.transferOwnership([wallet1, wallet2, wallet3]); - - // First owner agree - await obj.transferOwnership([wallet4], { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); - - // First owner disagree - const operation1 = await obj.allOperations.call(0); - await obj.cancelPending(operation1, { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); - - // First and Second owners agree - await obj.transferOwnership([wallet4], { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); - await obj.transferOwnership([wallet4], { from: wallet2 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); - - // Second owner disagree - const operation2 = await obj.allOperations.call(0); - await obj.cancelPending(operation2, { from: wallet2 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); - - // Third owner agree - await obj.transferOwnership([wallet4], { from: wallet3 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); - - // Second owner agree - await obj.transferOwnership([wallet4], { from: wallet2 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); - }); + await obj.addOwners([wallet1]); + await obj.resignOwnership({ from: wallet1 }); - it('should reset all pending operations when owners change', async function () { - const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2]); + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownersLength.call()).should.be.bignumber.equal(1); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); - await obj.setValue(1, { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.false; + (await obj.isOwner.call(wallet2)).should.be.false; + (await obj.isOwner.call(wallet3)).should.be.false; + }); - await obj.transferOwnership([wallet3], { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(2); + it('should remove mid of 3 owners', async function () { + const obj = await Multiownable.new(); - await obj.transferOwnership([wallet3], { from: wallet2 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); - }); + await obj.addOwners([wallet1, wallet2]); + await obj.resignOwnership({ from: wallet1 }); - it('should correctly perform last operation', async function () { - const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2]); + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownerAt.call(1)).should.be.equal(wallet2); + (await obj.ownersLength.call()).should.be.bignumber.equal(2); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(2); - await obj.setValue(1, { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.false; + (await obj.isOwner.call(wallet2)).should.be.true; + (await obj.isOwner.call(wallet3)).should.be.false; + }); - await obj.transferOwnership([wallet3], { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(2); + it('should remove one of 2 owners with howMany=1', async function () { + const obj = await Multiownable.new(); + + await obj.addOwnersWithHowMany([wallet1], 1); + await obj.resignOwnership({ from: wallet1 }); + + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownersLength.call()).should.be.bignumber.equal(1); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); // ! + + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.false; + (await obj.isOwner.call(wallet2)).should.be.false; + (await obj.isOwner.call(wallet3)).should.be.false; + }); - await obj.transferOwnership([wallet3], { from: wallet2 }); - (await obj.owners.call(0)).should.be.equal(wallet3); + it('should remove one of 3 owners with howMany=2', async function () { + const obj = await Multiownable.new(); + + await obj.addOwnersWithHowMany([wallet1, wallet2], 2); + await obj.resignOwnership({ from: wallet1 }); + + (await obj.ownerAt.call(0)).should.be.equal(_); + (await obj.ownerAt.call(1)).should.be.equal(wallet2); + (await obj.ownersLength.call()).should.be.bignumber.equal(2); + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); // ! + + (await obj.isOwner.call(_)).should.be.true; + (await obj.isOwner.call(wallet1)).should.be.false; + (await obj.isOwner.call(wallet2)).should.be.true; + (await obj.isOwner.call(wallet3)).should.be.false; + }); + + it('should fail to remove last owner', async function () { + const obj = await Multiownable.new(); + await obj.resignOwnership({ from: _ }).should.be.rejectedWith(EVMRevert); + }); + }); + + describe('transferOwnershipWithHowMany', async function () { + it('should transfer from 1 to 1', async function () { + const obj = await Multiownable.new(); + + await obj.transferOwnershipWithHowMany([wallet1], 1); + + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); + (await obj.ownersLength.call()).should.be.bignumber.equal(1); + (await obj.ownerAt.call(0)).should.be.equal(wallet1); + }); + + it('should transfer from 1 to 2', async function () { + const obj = await Multiownable.new(); + + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); + + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(2); + (await obj.ownersLength.call()).should.be.bignumber.equal(2); + (await obj.ownerAt.call(0)).should.be.equal(wallet1); + (await obj.ownerAt.call(1)).should.be.equal(wallet2); + }); + + it('should transfer from 2 to 1', async function () { + const obj = await Multiownable.new(); + + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); + await obj.transferOwnershipWithHowMany([wallet3], 1, { from: wallet1 }); + await obj.transferOwnershipWithHowMany([wallet3], 1, { from: wallet2 }); + + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(1); + (await obj.ownersLength.call()).should.be.bignumber.equal(1); + (await obj.ownerAt.call(0)).should.be.equal(wallet3); + }); + + it('should transfer from 2 to 2', async function () { + const obj = await Multiownable.new(); + + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); + await obj.transferOwnershipWithHowMany([wallet3, wallet4], 2, { from: wallet1 }); + await obj.transferOwnershipWithHowMany([wallet3, wallet4], 2, { from: wallet2 }); + + (await obj.howManyOwnersDecide.call()).should.be.bignumber.equal(2); + (await obj.ownersLength.call()).should.be.bignumber.equal(2); + (await obj.ownerAt.call(0)).should.be.equal(wallet3); + (await obj.ownerAt.call(1)).should.be.equal(wallet4); + }); + }); + + describe('owners', async function () { + it('should manage operations array correctly for 3 owners after 1 owner', async function () { + const obj = await Multiownable.new(); + + await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet3], 3); + + (await obj.ownersLength.call()).should.be.bignumber.equal(3); + (await obj.ownerAt.call(0)).should.be.equal(wallet1); + (await obj.ownerAt.call(1)).should.be.equal(wallet2); + (await obj.ownerAt.call(2)).should.be.equal(wallet3); + (await obj.isOwner.call(_)).should.be.false; + (await obj.isOwner.call(wallet1)).should.be.true; + (await obj.isOwner.call(wallet2)).should.be.true; + (await obj.isOwner.call(wallet3)).should.be.true; + (await obj.allOwners.call()).should.be.deep.equal([ + '0x000000000000000000000000' + wallet1.substr(2), + '0x000000000000000000000000' + wallet2.substr(2), + '0x000000000000000000000000' + wallet3.substr(2), + ]); + }); + + it('should manage operations array correctly for 2 owners after 3 owners', async function () { + const obj = await Multiownable.new(); + + await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet3], 3); + await obj.transferOwnershipWithHowMany([wallet3, wallet2], 2, { from: wallet1 }); + await obj.transferOwnershipWithHowMany([wallet3, wallet2], 2, { from: wallet2 }); + await obj.transferOwnershipWithHowMany([wallet3, wallet2], 2, { from: wallet3 }); + + (await obj.ownersLength.call()).should.be.bignumber.equal(2); + (await obj.ownerAt.call(0)).should.be.equal(wallet3); + (await obj.ownerAt.call(1)).should.be.equal(wallet2); + (await obj.isOwner.call(_)).should.be.false; + (await obj.isOwner.call(wallet1)).should.be.false; + (await obj.isOwner.call(wallet2)).should.be.true; + (await obj.isOwner.call(wallet3)).should.be.true; + (await obj.allOwners.call()).should.be.deep.equal([ + '0x000000000000000000000000' + wallet3.substr(2), + '0x000000000000000000000000' + wallet2.substr(2), + ]); + }); + }); + + describe('operations', async function () { + it('should manage operations array correctly', async function () { + const obj = await Multiownable.new(); + + // Transfer ownership 1 => 1 + (await obj.operationsLength.call()).should.be.bignumber.equal(0); + await obj.transferOwnershipWithHowMany([wallet1], 1); + (await obj.operationsLength.call()).should.be.bignumber.equal(0); + + // Transfer ownership 1 => 2 + (await obj.operationsLength.call()).should.be.bignumber.equal(0); + await obj.transferOwnershipWithHowMany([wallet2, wallet3], 2, { from: wallet1 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(0); + + // Transfer ownership 2 => 2 + (await obj.operationsLength.call()).should.be.bignumber.equal(0); + await obj.transferOwnershipWithHowMany([wallet4, wallet5], 2, { from: wallet2 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(1); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + ]); + await obj.transferOwnershipWithHowMany([wallet4, wallet5], 2, { from: wallet3 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(0); + }); + + it('should allow to cancel pending operations', async function () { + const obj = await Multiownable.new(); + await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet3], 3); + + // First owner agree + await obj.transferOwnershipWithHowMany([wallet4], 1, { from: wallet1 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(1); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + ]); + + // First owner disagree + const operation1 = await obj.operationAt.call(0); + await obj.cancelOperation(operation1, { from: wallet1 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(0); + + // First and Second owners agree + await obj.transferOwnershipWithHowMany([wallet4], 1, { from: wallet1 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(1); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + ]); + await obj.transferOwnershipWithHowMany([wallet4], 1, { from: wallet2 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(1); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + ]); + + // Second owner disagree + const operation2 = await obj.operationAt.call(0); + await obj.cancelOperation(operation2, { from: wallet2 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(1); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + ]); + + // Third owner agree + await obj.transferOwnershipWithHowMany([wallet4], 1, { from: wallet3 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(1); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + ]); + + // Second owner agree + await obj.transferOwnershipWithHowMany([wallet4], 1, { from: wallet2 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(0); + }); + + it('should reset all pending operations when owners change', async function () { + const obj = await MultiownableImpl.new(); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); + + await obj.setValue(1, { from: wallet1 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(1); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + ]); + + await obj.transferOwnershipWithHowMany([wallet3], 1, { from: wallet1 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(2); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + await obj.operationAt.call(1), + ]); + + await obj.transferOwnershipWithHowMany([wallet3], 1, { from: wallet2 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(0); + }); + + it('should correctly perform last operation', async function () { + const obj = await MultiownableImpl.new(); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); + + await obj.setValue(1, { from: wallet1 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(1); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + ]); + + await obj.transferOwnershipWithHowMany([wallet3], 1, { from: wallet1 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(2); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + await obj.operationAt.call(1), + ]); + + await obj.transferOwnershipWithHowMany([wallet3], 1, { from: wallet2 }); + (await obj.ownerAt.call(0)).should.be.equal(wallet3); + }); + + it('should not be able to create more than 20 pending operations per owner', async function () { + const obj = await MultiownableImpl.new(); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); + + // Fill wallet1 pending operations list + for (let i = 0; i < 20; i++) { + await obj.setValue(i, { from: wallet1 }); + } + await obj.setValue(20, { from: wallet1 }).should.be.rejectedWith(EVMRevert); + (await obj.ownerOperationsLength.call(wallet1)).should.be.bignumber.equal(20); + (await obj.allOwnerOperations.call(wallet1)).should.be.deep.equal([ + await obj.ownerOperationsAt.call(wallet1, 0), + await obj.ownerOperationsAt.call(wallet1, 1), + await obj.ownerOperationsAt.call(wallet1, 2), + await obj.ownerOperationsAt.call(wallet1, 3), + await obj.ownerOperationsAt.call(wallet1, 4), + await obj.ownerOperationsAt.call(wallet1, 5), + await obj.ownerOperationsAt.call(wallet1, 6), + await obj.ownerOperationsAt.call(wallet1, 7), + await obj.ownerOperationsAt.call(wallet1, 8), + await obj.ownerOperationsAt.call(wallet1, 9), + await obj.ownerOperationsAt.call(wallet1, 10), + await obj.ownerOperationsAt.call(wallet1, 11), + await obj.ownerOperationsAt.call(wallet1, 12), + await obj.ownerOperationsAt.call(wallet1, 13), + await obj.ownerOperationsAt.call(wallet1, 14), + await obj.ownerOperationsAt.call(wallet1, 15), + await obj.ownerOperationsAt.call(wallet1, 16), + await obj.ownerOperationsAt.call(wallet1, 17), + await obj.ownerOperationsAt.call(wallet1, 18), + await obj.ownerOperationsAt.call(wallet1, 19), + ]); + + // Erase wallet1 pending operations list + for (let i = 20; i > 0; i--) { + const operation = await obj.ownerOperationsAt.call(wallet1, Math.random() % i); + await obj.cancelOperation(operation, { from: wallet1, gas: 2000000 }); + } + (await obj.ownerOperationsLength.call(wallet1)).should.be.bignumber.equal(0); + (await obj.allOwnerOperations.call(wallet1)).should.be.deep.equal([]); + }); }); it('should correctly perform not last operation', async function () { const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2]); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); await obj.setValue(1, { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); - - await obj.transferOwnership([wallet3], { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(2); + (await obj.operationsLength.call()).should.be.bignumber.equal(1); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + ]); + + await obj.transferOwnershipWithHowMany([wallet3], 1, { from: wallet1 }); + (await obj.operationsLength.call()).should.be.bignumber.equal(2); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + await obj.operationAt.call(1), + ]); await obj.setValue(1, { from: wallet2 }); (await obj.value.call()).should.be.bignumber.equal(1); @@ -313,32 +649,42 @@ contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, walle it('should handle multiple simultaneous operations correctly', async function () { const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2]); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); // wallet1 => 1 await obj.setValue(1, { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); + (await obj.operationsLength.call()).should.be.bignumber.equal(1); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + ]); // Check value (await obj.value.call()).should.be.bignumber.equal(0); // wallet2 => 2 await obj.setValue(2, { from: wallet2 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(2); + (await obj.operationsLength.call()).should.be.bignumber.equal(2); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + await obj.operationAt.call(1), + ]); // Check value (await obj.value.call()).should.be.bignumber.equal(0); // wallet1 => 2 await obj.setValue(2, { from: wallet1 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); + (await obj.operationsLength.call()).should.be.bignumber.equal(1); + (await obj.allOperations.call()).should.be.deep.equal([ + await obj.operationAt.call(0), + ]); // Check value (await obj.value.call()).should.be.bignumber.equal(2); // wallet2 => 1 await obj.setValue(1, { from: wallet2 }); - (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); + (await obj.operationsLength.call()).should.be.bignumber.equal(0); // Check value (await obj.value.call()).should.be.bignumber.equal(1); @@ -346,7 +692,7 @@ contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, walle it('should allow to call onlyAnyOwner methods properly', async function () { const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2]); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); // Not owners try to call await obj.setValueAny(1, { from: _ }).should.be.rejectedWith(EVMRevert); @@ -361,7 +707,7 @@ contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, walle it('should allow to call onlyManyOwners methods properly', async function () { const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2]); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); // Not owners try to call await obj.setValue(1, { from: _ }).should.be.rejectedWith(EVMRevert); @@ -388,7 +734,7 @@ contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, walle it('should allow to call onlySomeOwners(n) methods properly', async function () { const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2]); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); // Invalid arg await obj.setValueSome(1, 0, { from: _ }).should.be.rejectedWith(EVMRevert); @@ -407,30 +753,30 @@ contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, walle it('should not allow to cancel pending of another owner', async function () { const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2]); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); // First owner await obj.setValue(2, { from: wallet1 }).should.be.fulfilled; // Second owner - const operation = await obj.allOperations.call(0); - await obj.cancelPending(operation, { from: wallet2 }).should.be.rejectedWith(EVMRevert); + const operation = await obj.operationAt.call(0); + await obj.cancelOperation(operation, { from: wallet2 }).should.be.rejectedWith(EVMRevert); }); it('should not allow to transfer ownership to no one and to user 0', async function () { const obj = await Multiownable.new(); - await obj.transferOwnership([]).should.be.rejectedWith(EVMRevert); - await obj.transferOwnership([0]).should.be.rejectedWith(EVMRevert); - await obj.transferOwnership([0, wallet1]).should.be.rejectedWith(EVMRevert); - await obj.transferOwnership([wallet1, 0]).should.be.rejectedWith(EVMRevert); - await obj.transferOwnership([0, wallet1, wallet2]).should.be.rejectedWith(EVMRevert); - await obj.transferOwnership([wallet1, 0, wallet2]).should.be.rejectedWith(EVMRevert); - await obj.transferOwnership([wallet1, wallet2, 0]).should.be.rejectedWith(EVMRevert); + await obj.transferOwnershipWithHowMany([], 0).should.be.rejectedWith(EVMRevert); + await obj.transferOwnershipWithHowMany([0], 1).should.be.rejectedWith(EVMRevert); + await obj.transferOwnershipWithHowMany([0, wallet1], 2).should.be.rejectedWith(EVMRevert); + await obj.transferOwnershipWithHowMany([wallet1, 0], 2).should.be.rejectedWith(EVMRevert); + await obj.transferOwnershipWithHowMany([0, wallet1, wallet2], 3).should.be.rejectedWith(EVMRevert); + await obj.transferOwnershipWithHowMany([wallet1, 0, wallet2], 3).should.be.rejectedWith(EVMRevert); + await obj.transferOwnershipWithHowMany([wallet1, wallet2, 0], 3).should.be.rejectedWith(EVMRevert); }); it('should works for nested methods with onlyManyOwners modifier', async function () { const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2]); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); await obj.nestedFirst(100, { from: wallet1 }); await obj.nestedFirst(100, { from: wallet2 }); @@ -440,7 +786,7 @@ contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, walle it('should works for nested methods with onlyAnyOwners modifier', async function () { const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2]); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); await obj.nestedFirstAnyToAny(100, { from: wallet3 }).should.be.rejectedWith(EVMRevert); await obj.nestedFirstAnyToAny2(100, { from: wallet1 }).should.be.rejectedWith(EVMRevert); @@ -452,7 +798,7 @@ contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, walle it('should works for nested methods with onlyAllOwners modifier', async function () { const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2]); + await obj.transferOwnershipWithHowMany([wallet1, wallet2], 2); await obj.nestedFirstAllToAll(100, { from: wallet3 }).should.be.rejectedWith(EVMRevert); await obj.nestedFirstAllToAll2(100, { from: wallet1 }).should.be.fulfilled; @@ -465,7 +811,7 @@ contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, walle it('should works for nested methods with onlyManyOwners => onlySomeOwners modifier', async function () { const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2, wallet3]); + await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet3], 3); await obj.nestedFirstManyToSome(100, 1, { from: wallet1 }); await obj.nestedFirstManyToSome(100, 1, { from: wallet2 }); @@ -485,7 +831,7 @@ contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, walle it('should works for nested methods with onlyAnyOwners => onlySomeOwners modifier', async function () { const obj = await MultiownableImpl.new(); - await obj.transferOwnership([wallet1, wallet2, wallet3]); + await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet3], 3); // 1 => 1 await obj.nestedFirstAnyToSome(100, 1, { from: wallet1 }); @@ -503,30 +849,95 @@ contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, walle it('should not allow to transfer ownership to several equal users', async function () { const obj = await Multiownable.new(); - await obj.transferOwnership([wallet1, wallet1]).should.be.rejectedWith(EVMRevert); - await obj.transferOwnership([wallet1, wallet2, wallet1]).should.be.rejectedWith(EVMRevert); + await obj.transferOwnershipWithHowMany([wallet1, wallet1], 2).should.be.rejectedWith(EVMRevert); + await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet1], 3).should.be.rejectedWith(EVMRevert); }); - it('should not allow to transfer ownership to more than 256 owners', async function () { + it('should allow to transfer ownership to the exactly 64 owners', async function () { const obj = await Multiownable.new(); - await obj.transferOwnership([ - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - _, - ]).should.be.rejectedWith(EVMRevert); + await obj.transferOwnershipWithHowMany([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, + ], 1).should.be.fulfilled; + }); + + it('should handle owners adding errors', async function () { + const obj = await Multiownable.new(); + + await obj.addOwnersWithHowMany([wallet1], 0).should.be.rejectedWith(EVMRevert); + await obj.addOwnersWithHowMany([wallet1], 3).should.be.rejectedWith(EVMRevert); + await obj.addOwnersWithHowMany([wallet1, wallet2], 4).should.be.rejectedWith(EVMRevert); + await obj.addOwnersWithHowMany([wallet1, '0x0'], 1).should.be.rejectedWith(EVMRevert); + await obj.addOwnersWithHowMany([wallet1, wallet1], 1).should.be.rejectedWith(EVMRevert); + }); + + it('should allow to add more than 256 owners', async function () { + const obj = await Multiownable.new(); + + await obj.addOwnersWithHowMany([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, + ], 1).should.be.fulfilled; + (await obj.ownersLength.call()).should.be.bignumber.equal(1 + 64); + + await obj.addOwnersWithHowMany([ + 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, + 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, + 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, + 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, 0x80, + ], 1).should.be.fulfilled; + (await obj.ownersLength.call()).should.be.bignumber.equal(1 + 64 * 2); + + await obj.addOwnersWithHowMany([ + 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, + 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, + 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, + 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xC0, + ], 1); + (await obj.ownersLength.call()).should.be.bignumber.equal(1 + 64 * 3); + + await obj.addOwnersWithHowMany([ + 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, + 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, + 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, + 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, 0x100, + ], 1); + (await obj.ownersLength.call()).should.be.bignumber.equal(1 + 64 * 4); + + await obj.addOwnersWithHowMany([ + 0x141, 0x142, 0x143, 0x144, 0x145, 0x146, 0x147, 0x148, 0x149, 0x14A, 0x14B, 0x14C, 0x14D, 0x14E, 0x14F, 0x150, + 0x151, 0x152, 0x153, 0x154, 0x155, 0x156, 0x157, 0x158, 0x159, 0x15A, 0x15B, 0x15C, 0x15D, 0x15E, 0x15F, 0x160, + 0x161, 0x162, 0x163, 0x164, 0x165, 0x166, 0x167, 0x168, 0x169, 0x16A, 0x16B, 0x16C, 0x16D, 0x16E, 0x16F, 0x170, + 0x171, 0x172, 0x173, 0x174, 0x175, 0x176, 0x177, 0x178, 0x179, 0x17A, 0x17B, 0x17C, 0x17D, 0x17E, 0x17F, 0x180, + ], 1).should.be.fulfilled; + (await obj.ownersLength.call()).should.be.bignumber.equal(1 + 64 * 5); + + await obj.addOwnersWithHowMany([ + 0x101, 0x102, 0x103, 0x104, 0x105, 0x106, 0x107, 0x108, 0x109, 0x10A, 0x10B, 0x10C, 0x10D, 0x10E, 0x10F, 0x110, + 0x111, 0x112, 0x113, 0x114, 0x115, 0x116, 0x117, 0x118, 0x119, 0x11A, 0x11B, 0x11C, 0x11D, 0x11E, 0x11F, 0x120, + 0x121, 0x122, 0x123, 0x124, 0x125, 0x126, 0x127, 0x128, 0x129, 0x12A, 0x12B, 0x12C, 0x12D, 0x12E, 0x12F, 0x130, + 0x131, 0x132, 0x133, 0x134, 0x135, 0x136, 0x137, 0x138, 0x139, 0x13A, 0x13B, 0x13C, 0x13D, 0x13E, 0x13F, 0x140, + ], 1).should.be.fulfilled; + (await obj.ownersLength.call()).should.be.bignumber.equal(1 + 64 * 6); + + await obj.addOwnersWithHowMany([ + 0x1C1, 0x1C2, 0x1C3, 0x1C4, 0x1C5, 0x1C6, 0x1C7, 0x1C8, 0x1C9, 0x1CA, 0x1CB, 0x1CC, 0x1CD, 0x1CE, 0x1CF, 0x1D0, + 0x1D1, 0x1D2, 0x1D3, 0x1D4, 0x1D5, 0x1D6, 0x1D7, 0x1D8, 0x1D9, 0x1DA, 0x1DB, 0x1DC, 0x1DD, 0x1DE, 0x1DF, 0x1E0, + 0x1E1, 0x1E2, 0x1E3, 0x1E4, 0x1E5, 0x1E6, 0x1E7, 0x1E8, 0x1E9, 0x1EA, 0x1EB, 0x1EC, 0x1ED, 0x1EE, 0x1EF, 0x1F0, + 0x1F1, 0x1F2, 0x1F3, 0x1F4, 0x1F5, 0x1F6, 0x1F7, 0x1F8, 0x1F9, 0x1FA, 0x1FB, 0x1FC, 0x1FD, 0x1FE, 0x1FF, 0x200, + ], 1); + (await obj.ownersLength.call()).should.be.bignumber.equal(1 + 64 * 7); + + await obj.addOwnersWithHowMany([ + 0x181, 0x182, 0x183, 0x184, 0x185, 0x186, 0x187, 0x188, 0x189, 0x18A, 0x18B, 0x18C, 0x18D, 0x18E, 0x18F, 0x190, + 0x191, 0x192, 0x193, 0x194, 0x195, 0x196, 0x197, 0x198, 0x199, 0x19A, 0x19B, 0x19C, 0x19D, 0x19E, 0x19F, 0x1A0, + 0x1A1, 0x1A2, 0x1A3, 0x1A4, 0x1A5, 0x1A6, 0x1A7, 0x1A8, 0x1A9, 0x1AA, 0x1AB, 0x1AC, 0x1AD, 0x1AE, 0x1AF, 0x1B0, + 0x1B1, 0x1B2, 0x1B3, 0x1B4, 0x1B5, 0x1B6, 0x1B7, 0x1B8, 0x1B9, 0x1BA, 0x1BB, 0x1BC, 0x1BD, 0x1BE, 0x1BF, 0x1C0, + ], 1); + (await obj.ownersLength.call()).should.be.bignumber.equal(1 + 64 * 8); }); }); diff --git a/test/Set.js b/test/Set.js new file mode 100644 index 0000000..97375d6 --- /dev/null +++ b/test/Set.js @@ -0,0 +1,188 @@ +// @flow + +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(web3.BigNumber)) + .should(); + +const Set = artifacts.require('Set'); +const SetImpl = artifacts.require('SetImpl'); + +const value1 = 'abcdefghijklmnopqrstuvwxyz123456'; +const value2 = 'abcdefghijklmnopqrstuvwxyz123457'; +const value3 = 'abcdefghijklmnopqrstuvwxyz123458'; + +contract('Set', function ([_, wallet1, wallet2, wallet3, wallet4, wallet5]) { + before(async function () { + SetImpl.link('Set', (await Set.new()).address); + }); + + it('should init correctly', async function () { + const set = await SetImpl.new(); + + (await set.length.call()).should.be.bignumber.equal(0); + (await set.contains.call(value1)).should.be.false; + (await set.contains.call(value2)).should.be.false; + (await set.contains.call(value3)).should.be.false; + }); + + describe('add', async function () { + it('should add first item', async function () { + const set = await SetImpl.new(); + + await set.add(value1); + + (await set.length.call()).should.be.bignumber.equal(1); + (await set.contains.call(value1)).should.be.true; + (await set.contains.call(value2)).should.be.false; + (await set.contains.call(value3)).should.be.false; + web3.toAscii(await set.at.call(0)).should.be.equal(value1); + }); + + it('should add two different items', async function () { + const set = await SetImpl.new(); + + await set.add(value1); + await set.add(value2); + + (await set.length.call()).should.be.bignumber.equal(2); + (await set.contains.call(value1)).should.be.true; + (await set.contains.call(value2)).should.be.true; + (await set.contains.call(value3)).should.be.false; + web3.toAscii(await set.at.call(0)).should.be.equal(value1); + web3.toAscii(await set.at.call(1)).should.be.equal(value2); + }); + + it('should add three different items', async function () { + const set = await SetImpl.new(); + + await set.add(value1); + await set.add(value2); + await set.add(value3); + + (await set.length.call()).should.be.bignumber.equal(3); + (await set.contains.call(value1)).should.be.true; + (await set.contains.call(value2)).should.be.true; + (await set.contains.call(value3)).should.be.true; + web3.toAscii(await set.at.call(0)).should.be.equal(value1); + web3.toAscii(await set.at.call(1)).should.be.equal(value2); + web3.toAscii(await set.at.call(2)).should.be.equal(value3); + }); + + it('should not add existing item to 1 item', async function () { + const set = await SetImpl.new(); + + await set.add(value1); + await set.add(value1); + + (await set.length.call()).should.be.bignumber.equal(1); + (await set.contains.call(value1)).should.be.true; + (await set.contains.call(value2)).should.be.false; + (await set.contains.call(value3)).should.be.false; + web3.toAscii(await set.at.call(0)).should.be.equal(value1); + }); + + it('should not add existing item to 2 items (1)', async function () { + const set = await SetImpl.new(); + + await set.add(value1); + await set.add(value2); + await set.add(value1); + + (await set.length.call()).should.be.bignumber.equal(2); + (await set.contains.call(value1)).should.be.true; + (await set.contains.call(value2)).should.be.true; + (await set.contains.call(value3)).should.be.false; + web3.toAscii(await set.at.call(0)).should.be.equal(value1); + web3.toAscii(await set.at.call(1)).should.be.equal(value2); + }); + + it('should not add existing item to 2 items (2)', async function () { + const set = await SetImpl.new(); + + await set.add(value1); + await set.add(value2); + await set.add(value2); + + (await set.length.call()).should.be.bignumber.equal(2); + (await set.contains.call(value1)).should.be.true; + (await set.contains.call(value2)).should.be.true; + (await set.contains.call(value3)).should.be.false; + web3.toAscii(await set.at.call(0)).should.be.equal(value1); + web3.toAscii(await set.at.call(1)).should.be.equal(value2); + }); + }); + + describe('remove', async function () { + it('should delete 1 of 1 items', async function () { + const set = await SetImpl.new(); + + await set.add(value1); + await set.remove(value1); + + (await set.length.call()).should.be.bignumber.equal(0); + (await set.contains.call(value1)).should.be.false; + (await set.contains.call(value2)).should.be.false; + (await set.contains.call(value3)).should.be.false; + }); + + it('should delete first of 2 items', async function () { + const set = await SetImpl.new(); + + await set.add(value1); + await set.add(value2); + await set.remove(value1); + + (await set.length.call()).should.be.bignumber.equal(1); + (await set.contains.call(value1)).should.be.false; + (await set.contains.call(value2)).should.be.true; + (await set.contains.call(value3)).should.be.false; + web3.toAscii(await set.at.call(0)).should.be.equal(value2); + }); + + it('should delete last of 2 items', async function () { + const set = await SetImpl.new(); + + await set.add(value1); + await set.add(value2); + await set.remove(value2); + + (await set.length.call()).should.be.bignumber.equal(1); + (await set.contains.call(value1)).should.be.true; + (await set.contains.call(value2)).should.be.false; + (await set.contains.call(value3)).should.be.false; + web3.toAscii(await set.at.call(0)).should.be.equal(value1); + }); + + it('should delete mid of 3 items', async function () { + const set = await SetImpl.new(); + + await set.add(value1); + await set.add(value2); + await set.add(value3); + await set.remove(value2); + + (await set.length.call()).should.be.bignumber.equal(2); + (await set.contains.call(value1)).should.be.true; + (await set.contains.call(value2)).should.be.false; + (await set.contains.call(value3)).should.be.true; + web3.toAscii(await set.at.call(0)).should.be.equal(value1); + web3.toAscii(await set.at.call(1)).should.be.equal(value3); + }); + + it('should not delete not existing item', async function () { + const set = await SetImpl.new(); + + await set.add(value1); + await set.add(value2); + await set.remove(value3); + + (await set.length.call()).should.be.bignumber.equal(2); + (await set.contains.call(value1)).should.be.true; + (await set.contains.call(value2)).should.be.true; + (await set.contains.call(value3)).should.be.false; + web3.toAscii(await set.at.call(0)).should.be.equal(value1); + web3.toAscii(await set.at.call(1)).should.be.equal(value2); + }); + }); +}); diff --git a/test/impl/MultiownableImpl.sol b/test/impl/MultiownableImpl.sol index d6e79c7..6b4afe0 100644 --- a/test/impl/MultiownableImpl.sol +++ b/test/impl/MultiownableImpl.sol @@ -73,4 +73,10 @@ contract MultiownableImpl is Multiownable { value = _value; } + // + + function unsafeResignOwnership() public { + _removeOwner(msg.sender); + } + } \ No newline at end of file diff --git a/test/impl/SetImpl.sol b/test/impl/SetImpl.sol new file mode 100644 index 0000000..29604eb --- /dev/null +++ b/test/impl/SetImpl.sol @@ -0,0 +1,31 @@ +pragma solidity ^0.4.11; + +import { Set } from "../../contracts/Set.sol"; + + +contract SetImpl { + using Set for Set.Data; + + Set.Data data; + + function length() public view returns(uint) { + return data.length(); + } + + function at(uint index) public view returns(bytes32) { + return data.at(index); + } + + function contains(bytes32 item) public view returns(bool) { + return data.contains(item); + } + + function add(bytes32 item) public returns(bool) { + return data.add(item); + } + + function remove(bytes32 item) public returns(bool) { + return data.remove(item); + } + +} \ No newline at end of file