├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .jshintrc ├── .solcover.js ├── .soliumignore ├── .soliumrc.json ├── .travis.yml ├── README.md ├── contracts ├── Migrations.sol └── Multiownable.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package-lock.json ├── package.json ├── scripts ├── rpc.sh └── testrpc(win).bat ├── test ├── MultiAttack.js ├── Multiownable.js ├── helpers │ ├── EVMRevert.js │ ├── EVMThrow.js │ ├── advanceToBlock.js │ ├── assertJump.js │ ├── assertRevert.js │ ├── decodeLogs.js │ ├── ether.js │ ├── expectEvent.js │ ├── expectThrow.js │ ├── increaseTime.js │ ├── latestTime.js │ ├── merkleTree.js │ ├── sendTransaction.js │ ├── sign.js │ ├── toPromise.js │ └── transactionMined.js └── impl │ ├── MultiAttackable.sol │ ├── MultiAttacker.sol │ └── MultiownableImpl.sol └── truffle.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["flow", "env"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | truffle.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : [ 3 | "standard", 4 | "plugin:promise/recommended" 5 | ], 6 | "plugins": [ 7 | "promise" 8 | ], 9 | "env": { 10 | "browser" : true, 11 | "node" : true, 12 | "mocha" : true, 13 | "jest" : true 14 | }, 15 | "globals" : { 16 | "artifacts": false, 17 | "contract": false, 18 | "assert": false, 19 | "web3": false 20 | }, 21 | "rules": { 22 | 23 | // Strict mode 24 | "strict": [2, "global"], 25 | 26 | // Code style 27 | "indent": [2, 4], 28 | "quotes": [2, "single"], 29 | "semi": ["error", "always"], 30 | "space-before-function-paren": ["error", "always"], 31 | "no-use-before-define": 0, 32 | "no-unused-expressions": "off", 33 | "eqeqeq": [2, "smart"], 34 | "dot-notation": [2, {"allowKeywords": true, "allowPattern": ""}], 35 | "no-redeclare": [2, {"builtinGlobals": true}], 36 | "no-trailing-spaces": [2, { "skipBlankLines": true }], 37 | "eol-last": 1, 38 | "comma-spacing": [2, {"before": false, "after": true}], 39 | "camelcase": [2, {"properties": "always"}], 40 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 41 | "comma-dangle": [1, "always-multiline"], 42 | "no-dupe-args": 2, 43 | "no-dupe-keys": 2, 44 | "no-debugger": 0, 45 | "no-undef": 2, 46 | "object-curly-spacing": [2, "always"], 47 | "max-len": [2, 120, 2], 48 | "generator-star-spacing": ["error", "before"], 49 | "promise/avoid-new": 0, 50 | "promise/always-return": 0 51 | } 52 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | .DS_Store 4 | .node-xmlhttprequest-sync-* 5 | .idea/* 6 | 7 | coverage/ 8 | coverageEnv/ 9 | allFiredEvents 10 | coverage.json 11 | scTopics 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.). 3 | "browser": true, // Standard browser globals e.g. `window`, `document`. 4 | "camelcase": false, // Permit only camelcase for `var` and `object indexes`. 5 | "curly": true, // Require {} for every new block or scope. 6 | "devel": false, // Allow development statements e.g. `console.log();`. 7 | "eqeqeq": true, // Require triple equals i.e. `===`. 8 | "esnext": true, // Allow ES.next specific features such as `const` and `let`. 9 | "freeze": true, // Forbid overwriting prototypes of native objects such as Array, Date and so on. 10 | "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 11 | "indent": 2, // Specify indentation spacing 12 | "latedef": true, // Prohibit variable use before definition. 13 | "newcap": false, // Require capitalization of all constructor functions e.g. `new F()`. 14 | "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. 15 | "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. 16 | "noempty": true, // Prohibit use of empty blocks. 17 | "nonew": true, // Prohibits the use of constructor functions for side-effects 18 | "quotmark": "single", // Define quotes to string values. 19 | "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. 20 | "smarttabs": false, // Supress warnings about mixed tabs and spaces 21 | "strict": true, // Require `use strict` pragma in every file. 22 | "trailing": true, // Prohibit trailing whitespaces. 23 | "undef": true, // Require all non-global variables be declared before they are used. 24 | "unused": true, // Warn unused variables. 25 | 26 | "maxparams": 4, // Maximum number of parameters for a function 27 | "maxstatements": 15, // Maximum number of statements in a function 28 | "maxcomplexity": 10, // Cyclomatic complexity (http://en.wikipedia.org/wiki/Cyclomatic_complexity) 29 | "maxdepth": 4, // Maximum depth of nested control structures 30 | "maxlen": 120, // Maximum number of cols in a line 31 | "multistr": true, // Allow use of multiline EOL escaping 32 | "experimental": ["asyncawait", "asyncreqawait"], 33 | 34 | "predef": [ // Extra globals. 35 | "after", 36 | "afterEach", 37 | "before", 38 | "beforeEach", 39 | "define", 40 | "describe", 41 | "exports", 42 | "it", 43 | "web3", 44 | "artifacts", 45 | "contract", 46 | "assert", 47 | "should", 48 | "expect" 49 | ] 50 | } -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('babel-polyfill'); 3 | 4 | module.exports = { 5 | testrpcOptions: '-p 8555 -l 0xffffffff ' + 6 | ' --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200,1000000000000000000000000" ' + 7 | ' --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201,1000000000000000000000000" ' + 8 | ' --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202,1000000000000000000000000" ' + 9 | ' --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203,1000000000000000000000000" ' + 10 | ' --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204,1000000000000000000000000" ' + 11 | ' --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205,1000000000000000000000000" ' + 12 | ' --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206,1000000000000000000000000" ' + 13 | ' --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000" ' + 14 | ' --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000" ' + 15 | ' --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000"', 16 | copyNodeModules: true, 17 | norpc: false 18 | } 19 | -------------------------------------------------------------------------------- /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom-rules-filename": null, 3 | "rules": { 4 | "imports-on-top": true, 5 | "variable-declarations": true, 6 | "array-declarations": true, 7 | "operator-whitespace": true, 8 | "lbrace": true, 9 | "mixedcase": true, 10 | "camelcase": true, 11 | "uppercase": true, 12 | "no-empty-blocks": true, 13 | "no-unused-vars": true, 14 | "quotes": true, 15 | "blank-lines": true, 16 | "indentation": true, 17 | "whitespace": true, 18 | "deprecated-suicide": true, 19 | "pragma-on-top": true 20 | } 21 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # https://github.com/sc-forks/solidity-coverage/blob/master/docs/faq.md 3 | # 4 | 5 | sudo: required 6 | dist: trusty 7 | language: node_js 8 | node_js: 9 | - '8' 10 | install: 11 | - npm install -g truffle 12 | - npm install -g ganache-cli 13 | - npm install 14 | before_script: 15 | - scripts/rpc.sh > /dev/null & 16 | - sleep 5 17 | script: 18 | - npm run lint 19 | - npm run lint:sol 20 | - npm run test 21 | after_script: 22 | - npm run coverage && cat coverage/lcov.info | coveralls 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multiownable 2 | 3 | [![Build Status](https://travis-ci.org/bitclave/Multiownable.svg?branch=master)](https://travis-ci.org/bitclave/Multiownable) 4 | [![Coverage Status](https://coveralls.io/repos/github/bitclave/Multiownable/badge.svg)](https://coveralls.io/github/bitclave/Multiownable) 5 | 6 | BitClave implementation of Multiownable contract as improvement to OpenZeppelin Ownable contract 7 | 8 | # Installation 9 | 10 | 1. Install [truffle](http://truffleframework.com) globally with `npm install -g truffle` 11 | 2. Install [ganache-cli](https://github.com/trufflesuite/ganache-cli) globally with `npm install -g ganache-cli` 12 | 3. Install local packages with `npm install` 13 | 4. Run ganache in separate terminal `scripts/rpc.sh` 14 | 5. Run tests with `npm test` 15 | 16 | On macOS you also need to install watchman: `brew install watchman` 17 | 18 | # Features 19 | 20 | 1. Supports up to 256 simultaneous owners 21 | 2. Simple usage: add modifiers `onlyAnyOwner`, `onlyManyOwners`, `onlyAllOwners` and `onlySomeOwners(howMany)` 22 | 3. Supports multiple pending operations 23 | 4. Allows owners to cancel pending operations 24 | 5. Reset all pending operations on ownership transfering 25 | 26 | # Example of multisig wallet 27 | 28 | ```solidity 29 | contract SimplestMultiWallet is Multiownable { 30 | 31 | bool avoidReentrancy = false; 32 | 33 | function () public payable { 34 | } 35 | 36 | function transferTo(address to, uint256 amount) public onlyManyOwners { 37 | require(!avoidReentrancy); 38 | avoidReentrancy = true; 39 | to.transfer(amount); 40 | avoidReentrancy = false; 41 | } 42 | 43 | } 44 | ``` 45 | 46 | # Example of multisig wallet with ERC20 tokens support 47 | 48 | ```solidity 49 | contract SimplestTokensMultiWallet is Multiownable { 50 | 51 | bool avoidReentrancy = false; 52 | 53 | function () public payable { 54 | } 55 | 56 | function transferTo(address to, uint256 amount) public onlyManyOwners { 57 | require(!avoidReentrancy); 58 | avoidReentrancy = true; 59 | to.transfer(amount); 60 | avoidReentrancy = false; 61 | } 62 | 63 | function transferTokensTo(address token, address to, uint256 amount) public onlyManyOwners { 64 | require(!avoidReentrancy); 65 | avoidReentrancy = true; 66 | ERC20(token).transfer(to, amount); 67 | avoidReentrancy = false; 68 | } 69 | 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | 4 | contract Migrations { 5 | 6 | address public owner; 7 | uint public lastCompletedMigration; 8 | 9 | modifier restricted() { 10 | if (msg.sender == owner) { 11 | _; 12 | } 13 | } 14 | 15 | constructor() public { 16 | owner = msg.sender; 17 | } 18 | 19 | function setCompleted(uint completed) public restricted { 20 | lastCompletedMigration = completed; 21 | } 22 | 23 | function upgrade(address newAddress) public restricted { 24 | Migrations upgraded = Migrations(newAddress); 25 | upgraded.setCompleted(lastCompletedMigration); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /contracts/Multiownable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | 4 | contract Multiownable { 5 | 6 | // VARIABLES 7 | 8 | uint256 public ownersGeneration; 9 | uint256 public howManyOwnersDecide; 10 | address[] public owners; 11 | bytes32[] public allOperations; 12 | address internal insideCallSender; 13 | uint256 internal insideCallCount; 14 | 15 | // Reverse lookup tables for owners and allOperations 16 | mapping(address => uint) public ownersIndices; // Starts from 1 17 | mapping(bytes32 => uint) public allOperationsIndicies; 18 | 19 | // Owners voting mask per operations 20 | mapping(bytes32 => uint256) public votesMaskByOperation; 21 | mapping(bytes32 => uint256) public votesCountByOperation; 22 | 23 | // EVENTS 24 | 25 | event OwnershipTransferred(address[] previousOwners, uint howManyOwnersDecide, address[] newOwners, uint newHowManyOwnersDecide); 26 | event OperationCreated(bytes32 operation, uint howMany, uint ownersCount, address proposer); 27 | event OperationUpvoted(bytes32 operation, uint votes, uint howMany, uint ownersCount, address upvoter); 28 | event OperationPerformed(bytes32 operation, uint howMany, uint ownersCount, address performer); 29 | event OperationDownvoted(bytes32 operation, uint votes, uint ownersCount, address downvoter); 30 | event OperationCancelled(bytes32 operation, address lastCanceller); 31 | 32 | // ACCESSORS 33 | 34 | function isOwner(address wallet) public constant returns(bool) { 35 | return ownersIndices[wallet] > 0; 36 | } 37 | 38 | function ownersCount() public constant returns(uint) { 39 | return owners.length; 40 | } 41 | 42 | function allOperationsCount() public constant returns(uint) { 43 | return allOperations.length; 44 | } 45 | 46 | // MODIFIERS 47 | 48 | /** 49 | * @dev Allows to perform method by any of the owners 50 | */ 51 | modifier onlyAnyOwner { 52 | if (checkHowManyOwners(1)) { 53 | bool update = (insideCallSender == address(0)); 54 | if (update) { 55 | insideCallSender = msg.sender; 56 | insideCallCount = 1; 57 | } 58 | _; 59 | if (update) { 60 | insideCallSender = address(0); 61 | insideCallCount = 0; 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * @dev Allows to perform method only after many owners call it with the same arguments 68 | */ 69 | modifier onlyManyOwners { 70 | if (checkHowManyOwners(howManyOwnersDecide)) { 71 | bool update = (insideCallSender == address(0)); 72 | if (update) { 73 | insideCallSender = msg.sender; 74 | insideCallCount = howManyOwnersDecide; 75 | } 76 | _; 77 | if (update) { 78 | insideCallSender = address(0); 79 | insideCallCount = 0; 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * @dev Allows to perform method only after all owners call it with the same arguments 86 | */ 87 | modifier onlyAllOwners { 88 | if (checkHowManyOwners(owners.length)) { 89 | bool update = (insideCallSender == address(0)); 90 | if (update) { 91 | insideCallSender = msg.sender; 92 | insideCallCount = owners.length; 93 | } 94 | _; 95 | if (update) { 96 | insideCallSender = address(0); 97 | insideCallCount = 0; 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * @dev Allows to perform method only after some owners call it with the same arguments 104 | */ 105 | modifier onlySomeOwners(uint howMany) { 106 | require(howMany > 0, "onlySomeOwners: howMany argument is zero"); 107 | require(howMany <= owners.length, "onlySomeOwners: howMany argument exceeds the number of owners"); 108 | 109 | if (checkHowManyOwners(howMany)) { 110 | bool update = (insideCallSender == address(0)); 111 | if (update) { 112 | insideCallSender = msg.sender; 113 | insideCallCount = howMany; 114 | } 115 | _; 116 | if (update) { 117 | insideCallSender = address(0); 118 | insideCallCount = 0; 119 | } 120 | } 121 | } 122 | 123 | // CONSTRUCTOR 124 | 125 | constructor() public { 126 | owners.push(msg.sender); 127 | ownersIndices[msg.sender] = 1; 128 | howManyOwnersDecide = 1; 129 | } 130 | 131 | // INTERNAL METHODS 132 | 133 | /** 134 | * @dev onlyManyOwners modifier helper 135 | */ 136 | function checkHowManyOwners(uint howMany) internal returns(bool) { 137 | if (insideCallSender == msg.sender) { 138 | require(howMany <= insideCallCount, "checkHowManyOwners: nested owners modifier check require more owners"); 139 | return true; 140 | } 141 | 142 | uint ownerIndex = ownersIndices[msg.sender] - 1; 143 | require(ownerIndex < owners.length, "checkHowManyOwners: msg.sender is not an owner"); 144 | bytes32 operation = keccak256(msg.data, ownersGeneration); 145 | 146 | require((votesMaskByOperation[operation] & (2 ** ownerIndex)) == 0, "checkHowManyOwners: owner already voted for the operation"); 147 | votesMaskByOperation[operation] |= (2 ** ownerIndex); 148 | uint operationVotesCount = votesCountByOperation[operation] + 1; 149 | votesCountByOperation[operation] = operationVotesCount; 150 | if (operationVotesCount == 1) { 151 | allOperationsIndicies[operation] = allOperations.length; 152 | allOperations.push(operation); 153 | emit OperationCreated(operation, howMany, owners.length, msg.sender); 154 | } 155 | emit OperationUpvoted(operation, operationVotesCount, howMany, owners.length, msg.sender); 156 | 157 | // If enough owners confirmed the same operation 158 | if (votesCountByOperation[operation] == howMany) { 159 | deleteOperation(operation); 160 | emit OperationPerformed(operation, howMany, owners.length, msg.sender); 161 | return true; 162 | } 163 | 164 | return false; 165 | } 166 | 167 | /** 168 | * @dev Used to delete cancelled or performed operation 169 | * @param operation defines which operation to delete 170 | */ 171 | function deleteOperation(bytes32 operation) internal { 172 | uint index = allOperationsIndicies[operation]; 173 | if (index < allOperations.length - 1) { // Not last 174 | allOperations[index] = allOperations[allOperations.length - 1]; 175 | allOperationsIndicies[allOperations[index]] = index; 176 | } 177 | allOperations.length--; 178 | 179 | delete votesMaskByOperation[operation]; 180 | delete votesCountByOperation[operation]; 181 | delete allOperationsIndicies[operation]; 182 | } 183 | 184 | // PUBLIC METHODS 185 | 186 | /** 187 | * @dev Allows owners to change their mind by cacnelling votesMaskByOperation operations 188 | * @param operation defines which operation to delete 189 | */ 190 | function cancelPending(bytes32 operation) public onlyAnyOwner { 191 | uint ownerIndex = ownersIndices[msg.sender] - 1; 192 | require((votesMaskByOperation[operation] & (2 ** ownerIndex)) != 0, "cancelPending: operation not found for this user"); 193 | votesMaskByOperation[operation] &= ~(2 ** ownerIndex); 194 | uint operationVotesCount = votesCountByOperation[operation] - 1; 195 | votesCountByOperation[operation] = operationVotesCount; 196 | emit OperationDownvoted(operation, operationVotesCount, owners.length, msg.sender); 197 | if (operationVotesCount == 0) { 198 | deleteOperation(operation); 199 | emit OperationCancelled(operation, msg.sender); 200 | } 201 | } 202 | 203 | /** 204 | * @dev Allows owners to change ownership 205 | * @param newOwners defines array of addresses of new owners 206 | */ 207 | function transferOwnership(address[] newOwners) public { 208 | transferOwnershipWithHowMany(newOwners, newOwners.length); 209 | } 210 | 211 | /** 212 | * @dev Allows owners to change ownership 213 | * @param newOwners defines array of addresses of new owners 214 | * @param newHowManyOwnersDecide defines how many owners can decide 215 | */ 216 | function transferOwnershipWithHowMany(address[] newOwners, uint256 newHowManyOwnersDecide) public onlyManyOwners { 217 | require(newOwners.length > 0, "transferOwnershipWithHowMany: owners array is empty"); 218 | require(newOwners.length <= 256, "transferOwnershipWithHowMany: owners count is greater then 256"); 219 | require(newHowManyOwnersDecide > 0, "transferOwnershipWithHowMany: newHowManyOwnersDecide equal to 0"); 220 | require(newHowManyOwnersDecide <= newOwners.length, "transferOwnershipWithHowMany: newHowManyOwnersDecide exceeds the number of owners"); 221 | 222 | // Reset owners reverse lookup table 223 | for (uint j = 0; j < owners.length; j++) { 224 | delete ownersIndices[owners[j]]; 225 | } 226 | for (uint i = 0; i < newOwners.length; i++) { 227 | require(newOwners[i] != address(0), "transferOwnershipWithHowMany: owners array contains zero"); 228 | require(ownersIndices[newOwners[i]] == 0, "transferOwnershipWithHowMany: owners array contains duplicates"); 229 | ownersIndices[newOwners[i]] = i + 1; 230 | } 231 | 232 | emit OwnershipTransferred(owners, howManyOwnersDecide, newOwners, newHowManyOwnersDecide); 233 | owners = newOwners; 234 | howManyOwnersDecide = newHowManyOwnersDecide; 235 | allOperations.length = 0; 236 | ownersGeneration++; 237 | } 238 | 239 | } 240 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require('Migrations'); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const Multiownable = artifacts.require('Multiownable'); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Multiownable); 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Multiownable", 3 | "version": "1.0.0", 4 | "description": "Solidity Contracts for the BitClave CAT Token", 5 | "main": "y", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "devDependencies": { 10 | "babel-eslint": "^8.2.3", 11 | "babel-polyfill": "^6.26.0", 12 | "babel-preset-env": "^1.6.1", 13 | "babel-preset-flow": "^6.23.0", 14 | "babel-register": "^6.26.0", 15 | "chai": "^4.1.2", 16 | "chai-as-promised": "^7.1.1", 17 | "chai-bignumber": "^2.0.2", 18 | "coveralls": "^3.0.1", 19 | "eslint": "^4.19.1", 20 | "eslint-config-defaults": "^9.0.0", 21 | "eslint-config-standard": "^11.0.0", 22 | "eslint-plugin-import": "^2.11.0", 23 | "eslint-plugin-node": "^6.0.1", 24 | "eslint-plugin-promise": "^3.7.0", 25 | "eslint-plugin-react": "^7.7.0", 26 | "eslint-plugin-standard": "^3.1.0", 27 | "ethjs-abi": "^0.2.1", 28 | "ethereumjs-util": "^5.2.0", 29 | "ethereumjs-wallet": "^0.6.0", 30 | "ganache-core": "^2.1.0", 31 | "solidity-coverage": "^0.5.0", 32 | "solium": "^1.1.7", 33 | "truffle": "^4.1.8", 34 | "web3": "^0.19.1", 35 | "zeppelin-solidity": "^1.9.0" 36 | }, 37 | "scripts": { 38 | "test": "truffle test", 39 | "lint": "eslint .", 40 | "lint:fix": "eslint . --fix", 41 | "lint:sol": "solium -d .", 42 | "lint:sol:fix": "solium -d . --fix", 43 | "lint:all": "npm run lint && npm run lint:sol", 44 | "lint:all:fix": "npm run lint:fix && npm run lint:sol:fix", 45 | "coverage": "./node_modules/.bin/solidity-coverage", 46 | "rpc": "scripts/rpc.sh" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/bitclave/crowdsale.git" 51 | }, 52 | "keywords": [ 53 | "bitclave", 54 | "solidity", 55 | "truffle" 56 | ], 57 | "author": "BitClave", 58 | "license": "MIT", 59 | "bugs": { 60 | "url": "https://github.com/bitclave/Multiownable/issues" 61 | }, 62 | "homepage": "https://github.com/bitclave/Multiownable#readme" 63 | } 64 | -------------------------------------------------------------------------------- /scripts/rpc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # We define 10 accounts with balance 1M ether, needed for high-value tests. 4 | ganache-cli --port 9545 --gasLimit 8000000 \ 5 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200,1000000000000000000000000" \ 6 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201,1000000000000000000000000" \ 7 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202,1000000000000000000000000" \ 8 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203,1000000000000000000000000" \ 9 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204,1000000000000000000000000" \ 10 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205,1000000000000000000000000" \ 11 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206,1000000000000000000000000" \ 12 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000" \ 13 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000" \ 14 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000" 15 | -------------------------------------------------------------------------------- /scripts/testrpc(win).bat: -------------------------------------------------------------------------------- 1 | testrpc -l 6000000 --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200,1000000000000000000000000" --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201,1000000000000000000000000" --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202,1000000000000000000000000" --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203,1000000000000000000000000" --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204,1000000000000000000000000" --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205,1000000000000000000000000" --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206,1000000000000000000000000" --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000" --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000" --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000" 2 | -------------------------------------------------------------------------------- /test/MultiAttack.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import ether from './helpers/ether'; 3 | import EVMRevert from './helpers/EVMRevert'; 4 | 5 | require('chai') 6 | .use(require('chai-as-promised')) 7 | .use(require('chai-bignumber')(web3.BigNumber)) 8 | .should(); 9 | 10 | const MultiAttackable = artifacts.require('./impl/MultiAttackable.sol'); 11 | const MultiAttacker = artifacts.require('./impl/MultiAttacker.sol'); 12 | 13 | contract('MultiAttack', function ([_, wallet1, wallet2, wallet3, wallet4, wallet5]) { 14 | it('should handle reentracy attack', async function () { 15 | const victim = await MultiAttackable.new(); 16 | const hacker = await MultiAttacker.new(); 17 | 18 | // Prepare victim wallet 19 | await victim.transferOwnership([wallet1, wallet2]); 20 | await web3.eth.sendTransaction({ from: _, to: victim.address, value: ether(3) }); 21 | 22 | // Try reentrace attack 23 | await victim.transferTo(hacker.address, ether(1), { from: wallet1 }); 24 | await victim.transferTo(hacker.address, ether(1), { from: wallet2 }).should.be.rejectedWith(EVMRevert); 25 | 26 | (await web3.eth.getBalance(victim.address)).should.be.bignumber.equal(ether(3)); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/Multiownable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import EVMRevert from './helpers/EVMRevert'; 3 | 4 | require('chai') 5 | .use(require('chai-as-promised')) 6 | .use(require('chai-bignumber')(web3.BigNumber)) 7 | .should(); 8 | 9 | const Multiownable = artifacts.require('Multiownable.sol'); 10 | const MultiownableImpl = artifacts.require('./impl/MultiownableImpl.sol'); 11 | 12 | contract('Multiownable', function ([_, wallet1, wallet2, wallet3, wallet4, wallet5]) { 13 | it('should be initialized correctly', async function () { 14 | const obj = await Multiownable.new(); 15 | 16 | (await obj.owners.call(0)).should.be.equal(_); 17 | (await obj.ownersCount.call()).should.be.bignumber.equal(1); 18 | 19 | (await obj.isOwner.call(_)).should.be.true; 20 | (await obj.isOwner.call(wallet1)).should.be.false; 21 | (await obj.isOwner.call(wallet2)).should.be.false; 22 | (await obj.isOwner.call(wallet3)).should.be.false; 23 | (await obj.isOwner.call(wallet4)).should.be.false; 24 | (await obj.isOwner.call(wallet5)).should.be.false; 25 | }); 26 | 27 | it('should transfer ownership 1 => 1 correctly', async function () { 28 | const obj = await Multiownable.new(); 29 | 30 | await obj.transferOwnership([wallet1]); 31 | 32 | (await obj.owners.call(0)).should.be.equal(wallet1); 33 | (await obj.ownersCount.call()).should.be.bignumber.equal(1); 34 | 35 | (await obj.isOwner.call(_)).should.be.false; 36 | (await obj.isOwner.call(wallet1)).should.be.true; 37 | (await obj.isOwner.call(wallet2)).should.be.false; 38 | (await obj.isOwner.call(wallet3)).should.be.false; 39 | (await obj.isOwner.call(wallet4)).should.be.false; 40 | (await obj.isOwner.call(wallet5)).should.be.false; 41 | }); 42 | 43 | it('should transfer ownership 1 => 2 correctly', async function () { 44 | const obj = await Multiownable.new(); 45 | 46 | await obj.transferOwnership([wallet1, wallet2]); 47 | 48 | (await obj.owners.call(0)).should.be.equal(wallet1); 49 | (await obj.owners.call(1)).should.be.equal(wallet2); 50 | (await obj.ownersCount.call()).should.be.bignumber.equal(2); 51 | 52 | (await obj.isOwner.call(_)).should.be.false; 53 | (await obj.isOwner.call(wallet1)).should.be.true; 54 | (await obj.isOwner.call(wallet2)).should.be.true; 55 | (await obj.isOwner.call(wallet3)).should.be.false; 56 | (await obj.isOwner.call(wallet4)).should.be.false; 57 | (await obj.isOwner.call(wallet5)).should.be.false; 58 | }); 59 | 60 | it('should transfer ownership 1 => 3 correctly', async function () { 61 | const obj = await Multiownable.new(); 62 | 63 | await obj.transferOwnership([wallet1, wallet2, wallet3]); 64 | 65 | (await obj.owners.call(0)).should.be.equal(wallet1); 66 | (await obj.owners.call(1)).should.be.equal(wallet2); 67 | (await obj.owners.call(2)).should.be.equal(wallet3); 68 | (await obj.ownersCount.call()).should.be.bignumber.equal(3); 69 | 70 | (await obj.isOwner.call(_)).should.be.false; 71 | (await obj.isOwner.call(wallet1)).should.be.true; 72 | (await obj.isOwner.call(wallet2)).should.be.true; 73 | (await obj.isOwner.call(wallet3)).should.be.true; 74 | (await obj.isOwner.call(wallet4)).should.be.false; 75 | (await obj.isOwner.call(wallet5)).should.be.false; 76 | }); 77 | 78 | it('should transfer ownership 2 => 1 correctly', async function () { 79 | const obj = await Multiownable.new(); 80 | 81 | await obj.transferOwnership([wallet1, wallet2]); 82 | await obj.transferOwnership([wallet3], { from: wallet1 }); 83 | await obj.transferOwnership([wallet3], { from: wallet2 }); 84 | 85 | (await obj.owners.call(0)).should.be.equal(wallet3); 86 | (await obj.ownersCount.call()).should.be.bignumber.equal(1); 87 | 88 | (await obj.isOwner.call(_)).should.be.false; 89 | (await obj.isOwner.call(wallet1)).should.be.false; 90 | (await obj.isOwner.call(wallet2)).should.be.false; 91 | (await obj.isOwner.call(wallet3)).should.be.true; 92 | (await obj.isOwner.call(wallet4)).should.be.false; 93 | (await obj.isOwner.call(wallet5)).should.be.false; 94 | }); 95 | 96 | it('should transfer ownership 3 => 1 correctly', async function () { 97 | const obj = await Multiownable.new(); 98 | 99 | await obj.transferOwnership([wallet1, wallet2, wallet3]); 100 | await obj.transferOwnership([wallet4], { from: wallet1 }); 101 | await obj.transferOwnership([wallet4], { from: wallet2 }); 102 | await obj.transferOwnership([wallet4], { from: wallet3 }); 103 | 104 | (await obj.owners.call(0)).should.be.equal(wallet4); 105 | (await obj.ownersCount.call()).should.be.bignumber.equal(1); 106 | 107 | (await obj.isOwner.call(_)).should.be.false; 108 | (await obj.isOwner.call(wallet1)).should.be.false; 109 | (await obj.isOwner.call(wallet2)).should.be.false; 110 | (await obj.isOwner.call(wallet3)).should.be.false; 111 | (await obj.isOwner.call(wallet4)).should.be.true; 112 | (await obj.isOwner.call(wallet5)).should.be.false; 113 | }); 114 | 115 | it('should transfer ownership 2 => 2 correctly', async function () { 116 | const obj = await Multiownable.new(); 117 | 118 | await obj.transferOwnership([wallet1, wallet2]); 119 | await obj.transferOwnership([wallet3, wallet4], { from: wallet1 }); 120 | await obj.transferOwnership([wallet3, wallet4], { from: wallet2 }); 121 | 122 | (await obj.owners.call(0)).should.be.equal(wallet3); 123 | (await obj.owners.call(1)).should.be.equal(wallet4); 124 | (await obj.ownersCount.call()).should.be.bignumber.equal(2); 125 | 126 | (await obj.isOwner.call(_)).should.be.false; 127 | (await obj.isOwner.call(wallet1)).should.be.false; 128 | (await obj.isOwner.call(wallet2)).should.be.false; 129 | (await obj.isOwner.call(wallet3)).should.be.true; 130 | (await obj.isOwner.call(wallet4)).should.be.true; 131 | (await obj.isOwner.call(wallet5)).should.be.false; 132 | }); 133 | 134 | it('should transfer ownership 2 => 3 correctly', async function () { 135 | const obj = await Multiownable.new(); 136 | 137 | await obj.transferOwnership([wallet1, wallet2]); 138 | await obj.transferOwnership([wallet3, wallet4, wallet5], { from: wallet1 }); 139 | await obj.transferOwnership([wallet3, wallet4, wallet5], { from: wallet2 }); 140 | 141 | (await obj.owners.call(0)).should.be.equal(wallet3); 142 | (await obj.owners.call(1)).should.be.equal(wallet4); 143 | (await obj.owners.call(2)).should.be.equal(wallet5); 144 | (await obj.ownersCount.call()).should.be.bignumber.equal(3); 145 | 146 | (await obj.isOwner.call(_)).should.be.false; 147 | (await obj.isOwner.call(wallet1)).should.be.false; 148 | (await obj.isOwner.call(wallet2)).should.be.false; 149 | (await obj.isOwner.call(wallet3)).should.be.true; 150 | (await obj.isOwner.call(wallet4)).should.be.true; 151 | (await obj.isOwner.call(wallet5)).should.be.true; 152 | }); 153 | 154 | it('should transfer ownership 3 => 2 correctly', async function () { 155 | const obj = await Multiownable.new(); 156 | 157 | await obj.transferOwnership([wallet1, wallet2, wallet3]); 158 | await obj.transferOwnership([wallet4, wallet5], { from: wallet1 }); 159 | await obj.transferOwnership([wallet4, wallet5], { from: wallet2 }); 160 | await obj.transferOwnership([wallet4, wallet5], { from: wallet3 }); 161 | 162 | (await obj.owners.call(0)).should.be.equal(wallet4); 163 | (await obj.owners.call(1)).should.be.equal(wallet5); 164 | (await obj.ownersCount.call()).should.be.bignumber.equal(2); 165 | 166 | (await obj.isOwner.call(_)).should.be.false; 167 | (await obj.isOwner.call(wallet1)).should.be.false; 168 | (await obj.isOwner.call(wallet2)).should.be.false; 169 | (await obj.isOwner.call(wallet3)).should.be.false; 170 | (await obj.isOwner.call(wallet4)).should.be.true; 171 | (await obj.isOwner.call(wallet5)).should.be.true; 172 | }); 173 | 174 | it('should transfer ownership 1,2 of 3 => 2 correctly', async function () { 175 | const obj = await Multiownable.new(); 176 | 177 | await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet3], 2); 178 | await obj.transferOwnership([wallet4, wallet5], { from: wallet1 }); 179 | await obj.transferOwnership([wallet4, wallet5], { from: wallet2 }); 180 | 181 | (await obj.owners.call(0)).should.be.equal(wallet4); 182 | (await obj.owners.call(1)).should.be.equal(wallet5); 183 | (await obj.ownersCount.call()).should.be.bignumber.equal(2); 184 | }); 185 | 186 | it('should transfer ownership 2,3 of 3 => 2 correctly', async function () { 187 | const obj = await Multiownable.new(); 188 | 189 | await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet3], 2); 190 | await obj.transferOwnership([wallet4, wallet5], { from: wallet2 }); 191 | await obj.transferOwnership([wallet4, wallet5], { from: wallet3 }); 192 | 193 | (await obj.owners.call(0)).should.be.equal(wallet4); 194 | (await obj.owners.call(1)).should.be.equal(wallet5); 195 | (await obj.ownersCount.call()).should.be.bignumber.equal(2); 196 | }); 197 | 198 | it('should transfer ownership 1,3 of 3 => 2 correctly', async function () { 199 | const obj = await Multiownable.new(); 200 | 201 | await obj.transferOwnershipWithHowMany([wallet1, wallet2, wallet3], 2); 202 | await obj.transferOwnership([wallet4, wallet5], { from: wallet1 }); 203 | await obj.transferOwnership([wallet4, wallet5], { from: wallet3 }); 204 | 205 | (await obj.owners.call(0)).should.be.equal(wallet4); 206 | (await obj.owners.call(1)).should.be.equal(wallet5); 207 | (await obj.ownersCount.call()).should.be.bignumber.equal(2); 208 | }); 209 | 210 | it('should not transfer ownership with wrong how many argument', async function () { 211 | const obj = await Multiownable.new(); 212 | 213 | await obj.transferOwnershipWithHowMany([wallet1], 0).should.be.rejectedWith(EVMRevert); 214 | await obj.transferOwnershipWithHowMany([wallet1, wallet2], 3).should.be.rejectedWith(EVMRevert); 215 | await obj.transferOwnershipWithHowMany([wallet1, wallet2], 4).should.be.rejectedWith(EVMRevert); 216 | }); 217 | 218 | it('should correctly manage allOperations array', async function () { 219 | const obj = await Multiownable.new(); 220 | 221 | // Transfer ownership 1 => 1 222 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); 223 | await obj.transferOwnership([wallet1]); 224 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); 225 | 226 | // Transfer ownership 1 => 2 227 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); 228 | await obj.transferOwnership([wallet2, wallet3], { from: wallet1 }); 229 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); 230 | 231 | // Transfer ownership 2 => 2 232 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); 233 | await obj.transferOwnership([wallet4, wallet5], { from: wallet2 }); 234 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); 235 | await obj.transferOwnership([wallet4, wallet5], { from: wallet3 }); 236 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); 237 | }); 238 | 239 | it('should allow to cancel pending operations', async function () { 240 | const obj = await Multiownable.new(); 241 | await obj.transferOwnership([wallet1, wallet2, wallet3]); 242 | 243 | // First owner agree 244 | await obj.transferOwnership([wallet4], { from: wallet1 }); 245 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); 246 | 247 | // First owner disagree 248 | const operation1 = await obj.allOperations.call(0); 249 | await obj.cancelPending(operation1, { from: wallet1 }); 250 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); 251 | 252 | // First and Second owners agree 253 | await obj.transferOwnership([wallet4], { from: wallet1 }); 254 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); 255 | await obj.transferOwnership([wallet4], { from: wallet2 }); 256 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); 257 | 258 | // Second owner disagree 259 | const operation2 = await obj.allOperations.call(0); 260 | await obj.cancelPending(operation2, { from: wallet2 }); 261 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); 262 | 263 | // Third owner agree 264 | await obj.transferOwnership([wallet4], { from: wallet3 }); 265 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); 266 | 267 | // Second owner agree 268 | await obj.transferOwnership([wallet4], { from: wallet2 }); 269 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); 270 | }); 271 | 272 | it('should reset all pending operations when owners change', async function () { 273 | const obj = await MultiownableImpl.new(); 274 | await obj.transferOwnership([wallet1, wallet2]); 275 | 276 | await obj.setValue(1, { from: wallet1 }); 277 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); 278 | 279 | await obj.transferOwnership([wallet3], { from: wallet1 }); 280 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(2); 281 | 282 | await obj.transferOwnership([wallet3], { from: wallet2 }); 283 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); 284 | }); 285 | 286 | it('should correctly perform last operation', async function () { 287 | const obj = await MultiownableImpl.new(); 288 | await obj.transferOwnership([wallet1, wallet2]); 289 | 290 | await obj.setValue(1, { from: wallet1 }); 291 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); 292 | 293 | await obj.transferOwnership([wallet3], { from: wallet1 }); 294 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(2); 295 | 296 | await obj.transferOwnership([wallet3], { from: wallet2 }); 297 | (await obj.owners.call(0)).should.be.equal(wallet3); 298 | }); 299 | 300 | it('should correctly perform not last operation', async function () { 301 | const obj = await MultiownableImpl.new(); 302 | await obj.transferOwnership([wallet1, wallet2]); 303 | 304 | await obj.setValue(1, { from: wallet1 }); 305 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); 306 | 307 | await obj.transferOwnership([wallet3], { from: wallet1 }); 308 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(2); 309 | 310 | await obj.setValue(1, { from: wallet2 }); 311 | (await obj.value.call()).should.be.bignumber.equal(1); 312 | }); 313 | 314 | it('should handle multiple simultaneous operations correctly', async function () { 315 | const obj = await MultiownableImpl.new(); 316 | await obj.transferOwnership([wallet1, wallet2]); 317 | 318 | // wallet1 => 1 319 | await obj.setValue(1, { from: wallet1 }); 320 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); 321 | 322 | // Check value 323 | (await obj.value.call()).should.be.bignumber.equal(0); 324 | 325 | // wallet2 => 2 326 | await obj.setValue(2, { from: wallet2 }); 327 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(2); 328 | 329 | // Check value 330 | (await obj.value.call()).should.be.bignumber.equal(0); 331 | 332 | // wallet1 => 2 333 | await obj.setValue(2, { from: wallet1 }); 334 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(1); 335 | 336 | // Check value 337 | (await obj.value.call()).should.be.bignumber.equal(2); 338 | 339 | // wallet2 => 1 340 | await obj.setValue(1, { from: wallet2 }); 341 | (await obj.allOperationsCount.call()).should.be.bignumber.equal(0); 342 | 343 | // Check value 344 | (await obj.value.call()).should.be.bignumber.equal(1); 345 | }); 346 | 347 | it('should allow to call onlyAnyOwner methods properly', async function () { 348 | const obj = await MultiownableImpl.new(); 349 | await obj.transferOwnership([wallet1, wallet2]); 350 | 351 | // Not owners try to call 352 | await obj.setValueAny(1, { from: _ }).should.be.rejectedWith(EVMRevert); 353 | await obj.setValueAny(1, { from: wallet3 }).should.be.rejectedWith(EVMRevert); 354 | 355 | // Owners try to call 356 | await obj.setValueAny(2, { from: wallet1 }).should.be.fulfilled; 357 | (await obj.value.call()).should.be.bignumber.equal(2); 358 | await obj.setValueAny(3, { from: wallet2 }).should.be.fulfilled; 359 | (await obj.value.call()).should.be.bignumber.equal(3); 360 | }); 361 | 362 | it('should allow to call onlyManyOwners methods properly', async function () { 363 | const obj = await MultiownableImpl.new(); 364 | await obj.transferOwnership([wallet1, wallet2]); 365 | 366 | // Not owners try to call 367 | await obj.setValue(1, { from: _ }).should.be.rejectedWith(EVMRevert); 368 | await obj.setValue(1, { from: wallet3 }).should.be.rejectedWith(EVMRevert); 369 | 370 | // Single owners try to call twice 371 | await obj.setValue(2, { from: wallet1 }).should.be.fulfilled; 372 | await obj.setValue(2, { from: wallet1 }).should.be.rejectedWith(EVMRevert); 373 | }); 374 | 375 | it('should allow to call onlyAllOwners methods properly', async function () { 376 | const obj = await MultiownableImpl.new(); 377 | await obj.transferOwnershipWithHowMany([wallet1, wallet2], 1); 378 | 379 | // Not owners try to call 380 | await obj.setValueAll(1, { from: _ }).should.be.rejectedWith(EVMRevert); 381 | await obj.setValueAll(1, { from: wallet3 }).should.be.rejectedWith(EVMRevert); 382 | 383 | // Single owners try to call twice 384 | await obj.setValueAll(2, { from: wallet1 }).should.be.fulfilled; 385 | await obj.setValueAll(2, { from: wallet2 }).should.be.fulfilled; 386 | (await obj.value.call()).should.be.bignumber.equal(2); 387 | }); 388 | 389 | it('should allow to call onlySomeOwners(n) methods properly', async function () { 390 | const obj = await MultiownableImpl.new(); 391 | await obj.transferOwnership([wallet1, wallet2]); 392 | 393 | // Invalid arg 394 | await obj.setValueSome(1, 0, { from: _ }).should.be.rejectedWith(EVMRevert); 395 | await obj.setValueSome(1, 3, { from: _ }).should.be.rejectedWith(EVMRevert); 396 | 397 | // Not owners try to call 398 | await obj.setValueSome(1, 1, { from: _ }).should.be.rejectedWith(EVMRevert); 399 | await obj.setValueSome(1, 1, { from: wallet3 }).should.be.rejectedWith(EVMRevert); 400 | 401 | // Owners try to call 402 | await obj.setValueSome(5, 2, { from: wallet1 }).should.be.fulfilled; 403 | (await obj.value.call()).should.be.bignumber.equal(0); 404 | await obj.setValueSome(5, 2, { from: wallet2 }).should.be.fulfilled; 405 | (await obj.value.call()).should.be.bignumber.equal(5); 406 | }); 407 | 408 | it('should not allow to cancel pending of another owner', async function () { 409 | const obj = await MultiownableImpl.new(); 410 | await obj.transferOwnership([wallet1, wallet2]); 411 | 412 | // First owner 413 | await obj.setValue(2, { from: wallet1 }).should.be.fulfilled; 414 | 415 | // Second owner 416 | const operation = await obj.allOperations.call(0); 417 | await obj.cancelPending(operation, { from: wallet2 }).should.be.rejectedWith(EVMRevert); 418 | }); 419 | 420 | it('should not allow to transfer ownership to no one and to user 0', async function () { 421 | const obj = await Multiownable.new(); 422 | await obj.transferOwnership([]).should.be.rejectedWith(EVMRevert); 423 | await obj.transferOwnership([0]).should.be.rejectedWith(EVMRevert); 424 | await obj.transferOwnership([0, wallet1]).should.be.rejectedWith(EVMRevert); 425 | await obj.transferOwnership([wallet1, 0]).should.be.rejectedWith(EVMRevert); 426 | await obj.transferOwnership([0, wallet1, wallet2]).should.be.rejectedWith(EVMRevert); 427 | await obj.transferOwnership([wallet1, 0, wallet2]).should.be.rejectedWith(EVMRevert); 428 | await obj.transferOwnership([wallet1, wallet2, 0]).should.be.rejectedWith(EVMRevert); 429 | }); 430 | 431 | it('should works for nested methods with onlyManyOwners modifier', async function () { 432 | const obj = await MultiownableImpl.new(); 433 | await obj.transferOwnership([wallet1, wallet2]); 434 | 435 | await obj.nestedFirst(100, { from: wallet1 }); 436 | await obj.nestedFirst(100, { from: wallet2 }); 437 | 438 | (await obj.value.call()).should.be.bignumber.equal(100); 439 | }); 440 | 441 | it('should works for nested methods with onlyAnyOwners modifier', async function () { 442 | const obj = await MultiownableImpl.new(); 443 | await obj.transferOwnership([wallet1, wallet2]); 444 | 445 | await obj.nestedFirstAnyToAny(100, { from: wallet3 }).should.be.rejectedWith(EVMRevert); 446 | await obj.nestedFirstAnyToAny2(100, { from: wallet1 }).should.be.rejectedWith(EVMRevert); 447 | 448 | await obj.nestedFirstAnyToAny(100, { from: wallet1 }); 449 | await obj.nestedFirstAnyToAny(100, { from: wallet2 }); 450 | (await obj.value.call()).should.be.bignumber.equal(100); 451 | }); 452 | 453 | it('should works for nested methods with onlyAllOwners modifier', async function () { 454 | const obj = await MultiownableImpl.new(); 455 | await obj.transferOwnership([wallet1, wallet2]); 456 | 457 | await obj.nestedFirstAllToAll(100, { from: wallet3 }).should.be.rejectedWith(EVMRevert); 458 | await obj.nestedFirstAllToAll2(100, { from: wallet1 }).should.be.fulfilled; 459 | await obj.nestedFirstAllToAll2(100, { from: wallet2 }).should.be.rejectedWith(EVMRevert); 460 | 461 | await obj.nestedFirstAllToAll(100, { from: wallet1 }); 462 | await obj.nestedFirstAllToAll(100, { from: wallet2 }); 463 | (await obj.value.call()).should.be.bignumber.equal(100); 464 | }); 465 | 466 | it('should works for nested methods with onlyManyOwners => onlySomeOwners modifier', async function () { 467 | const obj = await MultiownableImpl.new(); 468 | await obj.transferOwnership([wallet1, wallet2, wallet3]); 469 | 470 | await obj.nestedFirstManyToSome(100, 1, { from: wallet1 }); 471 | await obj.nestedFirstManyToSome(100, 1, { from: wallet2 }); 472 | await obj.nestedFirstManyToSome(100, 1, { from: wallet3 }); 473 | (await obj.value.call()).should.be.bignumber.equal(100); 474 | 475 | await obj.nestedFirstManyToSome(200, 2, { from: wallet1 }); 476 | await obj.nestedFirstManyToSome(200, 2, { from: wallet2 }); 477 | await obj.nestedFirstManyToSome(200, 2, { from: wallet3 }); 478 | (await obj.value.call()).should.be.bignumber.equal(200); 479 | 480 | await obj.nestedFirstManyToSome(300, 3, { from: wallet1 }); 481 | await obj.nestedFirstManyToSome(300, 3, { from: wallet2 }); 482 | await obj.nestedFirstManyToSome(300, 3, { from: wallet3 }); 483 | (await obj.value.call()).should.be.bignumber.equal(300); 484 | }); 485 | 486 | it('should works for nested methods with onlyAnyOwners => onlySomeOwners modifier', async function () { 487 | const obj = await MultiownableImpl.new(); 488 | await obj.transferOwnership([wallet1, wallet2, wallet3]); 489 | 490 | // 1 => 1 491 | await obj.nestedFirstAnyToSome(100, 1, { from: wallet1 }); 492 | (await obj.value.call()).should.be.bignumber.equal(100); 493 | await obj.nestedFirstAnyToSome(200, 1, { from: wallet2 }); 494 | (await obj.value.call()).should.be.bignumber.equal(200); 495 | await obj.nestedFirstAnyToSome(300, 1, { from: wallet3 }); 496 | (await obj.value.call()).should.be.bignumber.equal(300); 497 | 498 | // 1 => 2 499 | await obj.nestedFirstAnyToSome(100, 2, { from: wallet1 }).should.be.rejectedWith(EVMRevert); 500 | await obj.nestedFirstAnyToSome(200, 2, { from: wallet2 }).should.be.rejectedWith(EVMRevert); 501 | await obj.nestedFirstAnyToSome(300, 2, { from: wallet3 }).should.be.rejectedWith(EVMRevert); 502 | }); 503 | 504 | it('should not allow to transfer ownership to several equal users', async function () { 505 | const obj = await Multiownable.new(); 506 | await obj.transferOwnership([wallet1, wallet1]).should.be.rejectedWith(EVMRevert); 507 | await obj.transferOwnership([wallet1, wallet2, wallet1]).should.be.rejectedWith(EVMRevert); 508 | }); 509 | 510 | it('should not allow to transfer ownership to more than 256 owners', async function () { 511 | const obj = await Multiownable.new(); 512 | await obj.transferOwnership([ 513 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 514 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 515 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 516 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 517 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 518 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 519 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 520 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 521 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 522 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 523 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 524 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 525 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 526 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 527 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 528 | _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 529 | _, 530 | ]).should.be.rejectedWith(EVMRevert); 531 | }); 532 | }); 533 | -------------------------------------------------------------------------------- /test/helpers/EVMRevert.js: -------------------------------------------------------------------------------- 1 | export default 'revert'; 2 | -------------------------------------------------------------------------------- /test/helpers/EVMThrow.js: -------------------------------------------------------------------------------- 1 | export default 'invalid opcode'; 2 | -------------------------------------------------------------------------------- /test/helpers/advanceToBlock.js: -------------------------------------------------------------------------------- 1 | export function advanceBlock () { 2 | return new Promise((resolve, reject) => { 3 | web3.currentProvider.sendAsync({ 4 | jsonrpc: '2.0', 5 | method: 'evm_mine', 6 | id: Date.now(), 7 | }, (err, res) => { 8 | return err ? reject(err) : resolve(res); 9 | }); 10 | }); 11 | } 12 | 13 | // Advances the block number so that the last mined block is `number`. 14 | export default async function advanceToBlock (number) { 15 | if (web3.eth.blockNumber > number) { 16 | throw Error(`block number ${number} is in the past (current is ${web3.eth.blockNumber})`); 17 | } 18 | 19 | while (web3.eth.blockNumber < number) { 20 | await advanceBlock(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/helpers/assertJump.js: -------------------------------------------------------------------------------- 1 | export default async promise => { 2 | try { 3 | await promise; 4 | assert.fail('Expected invalid opcode not received'); 5 | } catch (error) { 6 | const invalidOpcodeReceived = error.message.search('invalid opcode') >= 0; 7 | assert(invalidOpcodeReceived, `Expected "invalid opcode", got ${error} instead`); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /test/helpers/assertRevert.js: -------------------------------------------------------------------------------- 1 | export default async promise => { 2 | try { 3 | await promise; 4 | assert.fail('Expected revert not received'); 5 | } catch (error) { 6 | const revertFound = error.message.search('revert') >= 0; 7 | assert(revertFound, `Expected "revert", got ${error} instead`); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /test/helpers/decodeLogs.js: -------------------------------------------------------------------------------- 1 | const SolidityEvent = require('web3/lib/web3/event.js'); 2 | 3 | export default function decodeLogs (logs, contract, address) { 4 | return logs.map(log => { 5 | const event = new SolidityEvent(null, contract.events[log.topics[0]], address); 6 | return event.decode(log); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /test/helpers/ether.js: -------------------------------------------------------------------------------- 1 | export default function ether (n) { 2 | return new web3.BigNumber(web3.toWei(n, 'ether')); 3 | } 4 | -------------------------------------------------------------------------------- /test/helpers/expectEvent.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert; 2 | 3 | const inLogs = async (logs, eventName) => { 4 | const event = logs.find(e => e.event === eventName); 5 | assert.exists(event); 6 | return event; 7 | }; 8 | 9 | const inTransaction = async (tx, eventName) => { 10 | const { logs } = await tx; 11 | return inLogs(logs, eventName); 12 | }; 13 | 14 | module.exports = { 15 | inLogs, 16 | inTransaction, 17 | }; 18 | -------------------------------------------------------------------------------- /test/helpers/expectThrow.js: -------------------------------------------------------------------------------- 1 | export default async promise => { 2 | try { 3 | await promise; 4 | } catch (error) { 5 | // TODO: Check jump destination to destinguish between a throw 6 | // and an actual invalid jump. 7 | const invalidOpcode = error.message.search('invalid opcode') >= 0; 8 | // TODO: When we contract A calls contract B, and B throws, instead 9 | // of an 'invalid jump', we get an 'out of gas' error. How do 10 | // we distinguish this from an actual out of gas event? (The 11 | // ganache log actually show an 'invalid jump' event.) 12 | const outOfGas = error.message.search('out of gas') >= 0; 13 | const revert = error.message.search('revert') >= 0; 14 | assert( 15 | invalidOpcode || outOfGas || revert, 16 | 'Expected throw, got \'' + error + '\' instead', 17 | ); 18 | return; 19 | } 20 | assert.fail('Expected throw not received'); 21 | }; 22 | -------------------------------------------------------------------------------- /test/helpers/increaseTime.js: -------------------------------------------------------------------------------- 1 | import latestTime from './latestTime'; 2 | 3 | // Increases ganache time by the passed duration in seconds 4 | export default function increaseTime (duration) { 5 | const id = Date.now(); 6 | 7 | return new Promise((resolve, reject) => { 8 | web3.currentProvider.sendAsync({ 9 | jsonrpc: '2.0', 10 | method: 'evm_increaseTime', 11 | params: [duration], 12 | id: id, 13 | }, err1 => { 14 | if (err1) return reject(err1); 15 | 16 | web3.currentProvider.sendAsync({ 17 | jsonrpc: '2.0', 18 | method: 'evm_mine', 19 | id: id + 1, 20 | }, (err2, res) => { 21 | return err2 ? reject(err2) : resolve(res); 22 | }); 23 | }); 24 | }); 25 | } 26 | 27 | /** 28 | * Beware that due to the need of calling two separate ganache methods and rpc calls overhead 29 | * it's hard to increase time precisely to a target point so design your test to tolerate 30 | * small fluctuations from time to time. 31 | * 32 | * @param target time in seconds 33 | */ 34 | export function increaseTimeTo (target) { 35 | let now = latestTime(); 36 | if (target < now) throw Error(`Cannot increase current time(${now}) to a moment in the past(${target})`); 37 | let diff = target - now; 38 | return increaseTime(diff); 39 | } 40 | 41 | export const duration = { 42 | seconds: function (val) { return val; }, 43 | minutes: function (val) { return val * this.seconds(60); }, 44 | hours: function (val) { return val * this.minutes(60); }, 45 | days: function (val) { return val * this.hours(24); }, 46 | weeks: function (val) { return val * this.days(7); }, 47 | years: function (val) { return val * this.days(365); }, 48 | }; 49 | -------------------------------------------------------------------------------- /test/helpers/latestTime.js: -------------------------------------------------------------------------------- 1 | // Returns the time of the last mined block in seconds 2 | export default function latestTime () { 3 | return web3.eth.getBlock('latest').timestamp; 4 | } 5 | -------------------------------------------------------------------------------- /test/helpers/merkleTree.js: -------------------------------------------------------------------------------- 1 | import { sha3, bufferToHex } from 'ethereumjs-util'; 2 | 3 | export default class MerkleTree { 4 | constructor (elements) { 5 | // Filter empty strings and hash elements 6 | this.elements = elements.filter(el => el).map(el => sha3(el)); 7 | 8 | // Deduplicate elements 9 | this.elements = this.bufDedup(this.elements); 10 | // Sort elements 11 | this.elements.sort(Buffer.compare); 12 | 13 | // Create layers 14 | this.layers = this.getLayers(this.elements); 15 | } 16 | 17 | getLayers (elements) { 18 | if (elements.length === 0) { 19 | return [['']]; 20 | } 21 | 22 | const layers = []; 23 | layers.push(elements); 24 | 25 | // Get next layer until we reach the root 26 | while (layers[layers.length - 1].length > 1) { 27 | layers.push(this.getNextLayer(layers[layers.length - 1])); 28 | } 29 | 30 | return layers; 31 | } 32 | 33 | getNextLayer (elements) { 34 | return elements.reduce((layer, el, idx, arr) => { 35 | if (idx % 2 === 0) { 36 | // Hash the current element with its pair element 37 | layer.push(this.combinedHash(el, arr[idx + 1])); 38 | } 39 | 40 | return layer; 41 | }, []); 42 | } 43 | 44 | combinedHash (first, second) { 45 | if (!first) { return second; } 46 | if (!second) { return first; } 47 | 48 | return sha3(this.sortAndConcat(first, second)); 49 | } 50 | 51 | getRoot () { 52 | return this.layers[this.layers.length - 1][0]; 53 | } 54 | 55 | getHexRoot () { 56 | return bufferToHex(this.getRoot()); 57 | } 58 | 59 | getProof (el) { 60 | let idx = this.bufIndexOf(el, this.elements); 61 | 62 | if (idx === -1) { 63 | throw new Error('Element does not exist in Merkle tree'); 64 | } 65 | 66 | return this.layers.reduce((proof, layer) => { 67 | const pairElement = this.getPairElement(idx, layer); 68 | 69 | if (pairElement) { 70 | proof.push(pairElement); 71 | } 72 | 73 | idx = Math.floor(idx / 2); 74 | 75 | return proof; 76 | }, []); 77 | } 78 | 79 | getHexProof (el) { 80 | const proof = this.getProof(el); 81 | 82 | return this.bufArrToHexArr(proof); 83 | } 84 | 85 | getPairElement (idx, layer) { 86 | const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1; 87 | 88 | if (pairIdx < layer.length) { 89 | return layer[pairIdx]; 90 | } else { 91 | return null; 92 | } 93 | } 94 | 95 | bufIndexOf (el, arr) { 96 | let hash; 97 | 98 | // Convert element to 32 byte hash if it is not one already 99 | if (el.length !== 32 || !Buffer.isBuffer(el)) { 100 | hash = sha3(el); 101 | } else { 102 | hash = el; 103 | } 104 | 105 | for (let i = 0; i < arr.length; i++) { 106 | if (hash.equals(arr[i])) { 107 | return i; 108 | } 109 | } 110 | 111 | return -1; 112 | } 113 | 114 | bufDedup (elements) { 115 | return elements.filter((el, idx) => { 116 | return this.bufIndexOf(el, elements) === idx; 117 | }); 118 | } 119 | 120 | bufArrToHexArr (arr) { 121 | if (arr.some(el => !Buffer.isBuffer(el))) { 122 | throw new Error('Array is not an array of buffers'); 123 | } 124 | 125 | return arr.map(el => '0x' + el.toString('hex')); 126 | } 127 | 128 | sortAndConcat (...args) { 129 | return Buffer.concat([...args].sort(Buffer.compare)); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/helpers/sendTransaction.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const ethjsABI = require('ethjs-abi'); 3 | 4 | export function findMethod (abi, name, args) { 5 | for (var i = 0; i < abi.length; i++) { 6 | const methodArgs = _.map(abi[i].inputs, 'type').join(','); 7 | if ((abi[i].name === name) && (methodArgs === args)) { 8 | return abi[i]; 9 | } 10 | } 11 | } 12 | 13 | export default function sendTransaction (target, name, argsTypes, argsValues, opts) { 14 | const abiMethod = findMethod(target.abi, name, argsTypes); 15 | const encodedData = ethjsABI.encodeMethod(abiMethod, argsValues); 16 | return target.sendTransaction(Object.assign({ data: encodedData }, opts)); 17 | } 18 | -------------------------------------------------------------------------------- /test/helpers/sign.js: -------------------------------------------------------------------------------- 1 | import utils from 'ethereumjs-util'; 2 | 3 | /** 4 | * Hash and add same prefix to the hash that ganache use. 5 | * @param {string} message the plaintext/ascii/original message 6 | * @return {string} the hash of the message, prefixed, and then hashed again 7 | */ 8 | export const hashMessage = (message) => { 9 | const messageHex = Buffer.from(utils.sha3(message).toString('hex'), 'hex'); 10 | const prefix = utils.toBuffer('\u0019Ethereum Signed Message:\n' + messageHex.length.toString()); 11 | return utils.bufferToHex(utils.sha3(Buffer.concat([prefix, messageHex]))); 12 | }; 13 | 14 | // signs message using web3 (auto-applies prefix) 15 | export const signMessage = (signer, message = '', options = {}) => { 16 | return web3.eth.sign(signer, web3.sha3(message, options)); 17 | }; 18 | 19 | // signs hex string using web3 (auto-applies prefix) 20 | export const signHex = (signer, message = '') => { 21 | return signMessage(signer, message, { encoding: 'hex' }); 22 | }; 23 | -------------------------------------------------------------------------------- /test/helpers/toPromise.js: -------------------------------------------------------------------------------- 1 | export default func => 2 | (...args) => 3 | new Promise((resolve, reject) => 4 | func(...args, (error, data) => error ? reject(error) : resolve(data))); 5 | -------------------------------------------------------------------------------- /test/helpers/transactionMined.js: -------------------------------------------------------------------------------- 1 | 2 | // from https://gist.github.com/xavierlepretre/88682e871f4ad07be4534ae560692ee6 3 | module.export = web3.eth.transactionMined = function (txnHash, interval) { 4 | var transactionReceiptAsync; 5 | interval = interval || 500; 6 | transactionReceiptAsync = function (txnHash, resolve, reject) { 7 | try { 8 | var receipt = web3.eth.getTransactionReceipt(txnHash); 9 | if (receipt === null) { 10 | setTimeout(function () { 11 | transactionReceiptAsync(txnHash, resolve, reject); 12 | }, interval); 13 | } else { 14 | resolve(receipt); 15 | } 16 | } catch (e) { 17 | reject(e); 18 | } 19 | }; 20 | 21 | if (Array.isArray(txnHash)) { 22 | var promises = []; 23 | txnHash.forEach(function (oneTxHash) { 24 | promises.push( 25 | web3.eth.getTransactionReceiptMined(oneTxHash, interval)); 26 | }); 27 | return Promise.all(promises); 28 | } else { 29 | return new Promise(function (resolve, reject) { 30 | transactionReceiptAsync(txnHash, resolve, reject); 31 | }); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /test/impl/MultiAttackable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | import "../../contracts/Multiownable.sol"; 4 | 5 | 6 | contract MultiAttackable is Multiownable { 7 | 8 | function() public payable { 9 | } 10 | 11 | function transferTo(address to, uint256 amount) public onlyManyOwners { 12 | require(to.call.value(amount)()); 13 | //to.transfer(amount); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /test/impl/MultiAttacker.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | import "./MultiAttackable.sol"; 4 | 5 | 6 | contract MultiAttacker { 7 | 8 | bool avoidRecursionDuringAttack = false; 9 | 10 | function () public payable { 11 | if (!avoidRecursionDuringAttack) { 12 | avoidRecursionDuringAttack = true; 13 | MultiAttackable(msg.sender).transferTo(this, 2 ether); 14 | avoidRecursionDuringAttack = false; 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /test/impl/MultiownableImpl.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | import "../../contracts/Multiownable.sol"; 4 | 5 | 6 | contract MultiownableImpl is Multiownable { 7 | 8 | uint public value; 9 | 10 | function setValue(uint _value) public onlyManyOwners { 11 | value = _value; 12 | } 13 | 14 | function setValueAny(uint _value) public onlyAnyOwner { 15 | value = _value; 16 | } 17 | 18 | function setValueAll(uint _value) public onlyAllOwners { 19 | value = _value; 20 | } 21 | 22 | function setValueSome(uint _value, uint howMany) public onlySomeOwners(howMany) { 23 | value = _value; 24 | } 25 | 26 | function nestedFirst(uint _value) public onlyManyOwners { 27 | nestedSecond(_value); 28 | } 29 | 30 | function nestedSecond(uint _value) public onlyManyOwners { 31 | value = _value; 32 | } 33 | 34 | // 35 | 36 | function nestedFirstAllToAll(uint _value) public onlyAllOwners { 37 | nestedSecondAllToAll(_value); 38 | } 39 | 40 | function nestedFirstAllToAll2(uint _value) public onlyAllOwners { 41 | this.nestedSecondAllToAll(_value); // this. 42 | } 43 | 44 | function nestedSecondAllToAll(uint _value) public onlyAllOwners { 45 | value = _value; 46 | } 47 | 48 | // 49 | 50 | function nestedFirstAnyToAny(uint _value) public onlyAnyOwner { 51 | nestedSecondAnyToAny(_value); 52 | } 53 | 54 | function nestedFirstAnyToAny2(uint _value) public onlyAnyOwner { 55 | this.nestedSecondAnyToAny(_value); // this. 56 | } 57 | 58 | function nestedSecondAnyToAny(uint _value) public onlyAnyOwner { 59 | value = _value; 60 | } 61 | 62 | // 63 | 64 | function nestedFirstManyToSome(uint _value, uint howMany) public onlyManyOwners { 65 | nestedSecondSome(_value, howMany); 66 | } 67 | 68 | function nestedFirstAnyToSome(uint _value, uint howMany) public onlyAnyOwner { 69 | nestedSecondSome(_value, howMany); 70 | } 71 | 72 | function nestedSecondSome(uint _value, uint howMany) public onlySomeOwners(howMany) { 73 | value = _value; 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('babel-polyfill'); 3 | 4 | module.exports = { 5 | networks: { 6 | development: { 7 | host: 'localhost', 8 | port: 9545, 9 | network_id: '*', 10 | gas: 8000000, 11 | }, 12 | coverage: { 13 | host: 'localhost', 14 | port: 8555, 15 | network_id: '*', 16 | gas: 0xffffffff, 17 | }, 18 | }, 19 | }; 20 | --------------------------------------------------------------------------------