From 1994d5ce31c54f4b9273d4e09b4b6d8f8c09fde5 Mon Sep 17 00:00:00 2001 From: Ravindra Kumar Date: Mon, 11 Mar 2019 18:00:45 +0530 Subject: [PATCH] Smart contract initial setup --- .babelrc | 3 + .circleci/config.yml | 37 ++++ .env.sample | 2 + .eslintignore | 4 + .eslintrc.js | 102 +++++++++++ .gitattributes | 1 + .gitignore | 106 ++++++++++++ .prettierignore | 1 + .prettierrc | 5 + .solcover.js | 6 + .soliumignore | 2 + .soliumrc.json | 27 +++ AddressRegistry.sol | 28 ---- CONTRIBUTING.md | 1 + LICENSE | 21 +++ README.md | 61 ++++++- .../LogicRegistry.sol | 31 ++-- contracts/Migrations.sol | 27 +++ contracts/Ownable.sol | 39 +++++ .../ProxyLogics}/default.sol | 12 +- .../ProxyRegistry.sol | 8 +- UserProxy.sol => contracts/UserProxy.sol | 70 ++++---- migrations/1_initial_migration.js | 5 + migrations/2_contract_migrations.js | 5 + mocha-smart-contracts-config.json | 10 ++ package.json | 68 ++++++++ test/Ownable.test.js | 31 ++++ test/helpers/general.js | 158 ++++++++++++++++++ truffle-box.json | 18 ++ truffle.js | 56 +++++++ 30 files changed, 852 insertions(+), 93 deletions(-) create mode 100644 .babelrc create mode 100644 .circleci/config.yml create mode 100644 .env.sample create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .solcover.js create mode 100644 .soliumignore create mode 100644 .soliumrc.json delete mode 100644 AddressRegistry.sol create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE rename LogicRegistry.sol => contracts/LogicRegistry.sol (63%) create mode 100644 contracts/Migrations.sol create mode 100644 contracts/Ownable.sol rename {ProxyLogics => contracts/ProxyLogics}/default.sol (89%) rename ProxyRegistry.sol => contracts/ProxyRegistry.sol (96%) rename UserProxy.sol => contracts/UserProxy.sol (82%) create mode 100644 migrations/1_initial_migration.js create mode 100644 migrations/2_contract_migrations.js create mode 100644 mocha-smart-contracts-config.json create mode 100644 package.json create mode 100644 test/Ownable.test.js create mode 100644 test/helpers/general.js create mode 100755 truffle-box.json create mode 100644 truffle.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..3ea7a2d --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["env","es2015", "stage-2", "stage-3"] +} diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..46f98bb --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,37 @@ +# Javascript Node CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-javascript/ for more details +# +version: 2 +jobs: + build: + docker: + # specify the version you desire here + - image: circleci/node:8.4.0 + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/mongo:3.4.4 + + working_directory: ~/repo + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "package.json" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: npm install + + - save_cache: + paths: + - node_modules + key: v1-dependencies-{{ checksum "package.json" }} + + # run tests! + - run: npm run test-ci \ No newline at end of file diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..98aee42 --- /dev/null +++ b/.env.sample @@ -0,0 +1,2 @@ +RINKEBY_PRIVATE_KEY="df7ebe6c9601adf4e911faac9da547686e6453a11cf13264d895fc2979a6bec2" +ROPSTEN_PRIVATE_KEY="192f175c2f5e5a9437fdbc12043404763f96ccbcd6fc32b1d61dbb61e14e6f34" \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..47d952c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +tmp/** +build/** +node_modules/** +contracts/** diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..6d2fc1d --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,102 @@ +module.exports = { + parser: 'babel-eslint', + parserOptions: { + ecmaFeatures: { + generators: true, + experimentalObjectRestSpread: true + }, + sourceType: 'module', + allowImportExportEverywhere: false + }, + extends: [ + 'eslint:recommended', + 'plugin:import/errors', + 'plugin:import/warnings', + 'plugin:promise/recommended', + 'plugin:security/recommended' + ], + plugins: [ + 'compat', + 'prettier', + 'promise', + 'security' + ], + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.json', '.css'], + paths: './src' + } + }, + polyfills: ['fetch', 'promises'] + }, + env: { + node: true, + }, + globals: { + __DEV__: true, + __dirname: true, + after: true, + afterAll: true, + afterEach: true, + artifacts: true, + assert: true, + before: true, + beforeAll: true, + beforeEach: true, + console: true, + contract: true, + describe: true, + expect: true, + fetch: true, + global: true, + it: true, + module: true, + process: true, + Promise: true, + require: true, + setTimeout: true, + test: true, + xdescribe: true, + xit: true, + web3: true + }, + rules: { + 'compat/compat': 'error', + 'import/first': 'error', + 'import/no-anonymous-default-export': 'error', + 'import/no-unassigned-import': 'error', + 'import/prefer-default-export': 'error', + 'import/no-named-as-default': 'off', + 'import/no-unresolved': 'error', + 'prettier/prettier': [ + 'error', + { + semi: false, + singleQuote: true, + trailingComma: 'none' + } + ], + 'promise/avoid-new': 'off', + 'security/detect-object-injection': 'off', + 'arrow-body-style': 'off', + 'lines-between-class-members': ['error', 'always'], + 'no-console': ['warn', { allow: ['assert'] }], + 'no-shadow': 'error', + 'no-var': 'error', + + 'padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: 'class', next: '*' }, + { blankLine: 'always', prev: 'do', next: '*' }, + { blankLine: 'always', prev: '*', next: 'export' }, + { blankLine: 'always', prev: 'for', next: '*' }, + { blankLine: 'always', prev: 'if', next: '*' }, + { blankLine: 'always', prev: 'switch', next: '*' }, + { blankLine: 'always', prev: 'try', next: '*' }, + { blankLine: 'always', prev: 'while', next: '*' }, + { blankLine: 'always', prev: 'with', next: '*' } + ], + 'prefer-const': 'error' + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7cc88f0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sol linguist-language=Solidity \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8f79c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,106 @@ + +# Created by https://www.gitignore.io/api/solidity,soliditytruffle + +### Solidity ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +### SolidityTruffle ### +# depedencies +node_modules + +# testing + +# production +build +build_webpack + +# misc +.DS_Store +npm-debug.log +.truffle-solidity-loader +.vagrant/** +blockchain/geth/** +blockchain/keystore/** +blockchain/history + +#truffle +.tern-port +yarn.lock +package-lock.json + + +# End of https://www.gitignore.io/api/solidity,soliditytruffle + +test-results/ \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ec6d3cd --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +package.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..49955e2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "none" +} diff --git a/.solcover.js b/.solcover.js new file mode 100644 index 0000000..669e290 --- /dev/null +++ b/.solcover.js @@ -0,0 +1,6 @@ +module.exports = { + port: 9545, + testrpcOptions: + '-p 9545 -m "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat"', + copyNodeModules: false +} diff --git a/.soliumignore b/.soliumignore new file mode 100644 index 0000000..3394412 --- /dev/null +++ b/.soliumignore @@ -0,0 +1,2 @@ +node_modules +contracts/Migrations.sol \ No newline at end of file diff --git a/.soliumrc.json b/.soliumrc.json new file mode 100644 index 0000000..6313fb8 --- /dev/null +++ b/.soliumrc.json @@ -0,0 +1,27 @@ +{ + "extends": "solium:all", + "plugins": ["security"], + "rules": { + "arg-overflow": "error", + "array-declarations": "error", + "blank-lines": "error", + "camelcase": "error", + "comma-whitespace": "error", + "deprecated-suicide": "error", + "function-whitespace": "error", + "imports-on-top": "error", + "indentation": ["error", 4], + "lbrace": "error", + "mixedcase": "error", + "no-empty-blocks": "error", + "no-unused-vars": "error", + "operator-whitespace": "error", + "pragma-on-top": "error", + "quotes": ["error", "double"], + "security/no-inline-assembly": "off", + "semicolon-whitespace": "error", + "uppercase": "off", + "variable-declarations": "error", + "whitespace": "error" + } +} diff --git a/AddressRegistry.sol b/AddressRegistry.sol deleted file mode 100644 index 122e6ae..0000000 --- a/AddressRegistry.sol +++ /dev/null @@ -1,28 +0,0 @@ -pragma solidity ^0.4.23; - - -contract AddressRegistry { - - event AddressSet(string name, address addr); - mapping(bytes32 => address) registry; - - constructor() public { - registry[keccak256(abi.encodePacked("admin"))] = msg.sender; - registry[keccak256(abi.encodePacked("owner"))] = msg.sender; - } - - function getAddr(string memory name) public view returns(address) { - return registry[keccak256(abi.encodePacked(name))]; - } - - function setAddr(string memory name, address addr) public { - require( - msg.sender == getAddr("admin") || - msg.sender == getAddr("owner"), - "Permission Denied" - ); - registry[keccak256(abi.encodePacked(name))] = addr; - emit AddressSet(name, addr); - } - -} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dfb6aea --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +Contributors Guide diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5f83974 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Ravindra Kumar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 4a23c86..011beb8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,59 @@ -# InstaProxyContract -InstaDApp Proxy Contract. +# InstaDApp V2 Contracts + +Smart contracts comprising the business logic of the InstaDApp. + +## This project uses: +- [Truffle v5](https://truffleframework.com/) +- [Ganache](https://truffleframework.com/ganache) +- [Solium](https://github.com/duaraghav8/Solium) +- [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-solidity) +- [Travis CI](https://travis-ci.org/InstaDApp/InstaContract-v2) and [Circle CI](https://circleci.com/gh/InstaDApp/InstaContract-v2) +- [Coveralls](https://coveralls.io/github/InstaDApp/InstaContract-v2?branch=master) + +## Installation + +1. Install Truffle and Ganache CLI globally. + +```javascript +npm install -g truffle@beta +npm install -g ganache-cli +``` + +2. Create a `.env` file in the root directory and add your private key. + +## Commands: + +``` +Compile contracts: truffle compile +Migrate contracts: truffle migrate +Test contracts: truffle test +Run eslint: npm run lint +Run solium: npm run solium +Run solidity-coverage: npm run coverage +Run lint, solium, and truffle test: npm run test +``` + +## License +``` +MIT License + +Copyright (c) 2019 InstaDApp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/LogicRegistry.sol b/contracts/LogicRegistry.sol similarity index 63% rename from LogicRegistry.sol rename to contracts/LogicRegistry.sol index 16eaeef..efa202f 100644 --- a/LogicRegistry.sol +++ b/contracts/LogicRegistry.sol @@ -1,43 +1,40 @@ -pragma solidity ^0.4.23; - +pragma solidity ^0.5.0; interface AddrRegistry { - function getAddr(string calldata name) external view returns(address); + function getAddr(string calldata) external view returns (address); } + contract AddressRegistry { address public registry; modifier onlyAdmin() { - require( - msg.sender == getAddress("admin"), - "Permission Denied" - ); + require(msg.sender == getAddress("admin"), "Permission Denied"); _; } - function getAddress(string memory name) internal view returns(address) { + function getAddress(string memory name) internal view returns (address) { AddrRegistry addrReg = AddrRegistry(registry); return addrReg.getAddr(name); } } -contract LogicRegistry is AddressRegistry { +contract LogicRegistry is AddressRegistry { event DefaultLogicSet(address logicAddr); event LogicSet(address logicAddr, bool isLogic); - mapping(address => bool) public DefaultLogicProxies; - mapping(address => bool) public LogicProxies; + mapping(address => bool) public defaultLogicProxies; + mapping(address => bool) public logicProxies; constructor(address registry_) public { registry = registry_; } - function getLogic(address logicAddr) public view returns(bool) { - if (DefaultLogicProxies[logicAddr]) { + function getLogic(address logicAddr) public view returns (bool) { + if (defaultLogicProxies[logicAddr]) { return true; - } else if (LogicProxies[logicAddr]) { + } else if (logicProxies[logicAddr]) { return true; } else { return false; @@ -46,14 +43,14 @@ contract LogicRegistry is AddressRegistry { function setLogic(address logicAddr, bool isLogic) public onlyAdmin { require(msg.sender == getAddress("admin"), "Permission Denied"); - LogicProxies[logicAddr] = true; + logicProxies[logicAddr] = true; emit LogicSet(logicAddr, isLogic); } function setDefaultLogic(address logicAddr) public onlyAdmin { require(msg.sender == getAddress("admin"), "Permission Denied"); - DefaultLogicProxies[logicAddr] = true; + defaultLogicProxies[logicAddr] = true; emit DefaultLogicSet(logicAddr); } -} \ No newline at end of file +} diff --git a/contracts/Migrations.sol b/contracts/Migrations.sol new file mode 100644 index 0000000..483c891 --- /dev/null +++ b/contracts/Migrations.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.5.0; + +/* solium-disable mixedcase */ +contract Migrations { + address public owner; + uint public last_completed_migration; + + modifier restricted() { + if (msg.sender == owner) _; + } + + constructor() public { + owner = msg.sender; + } + + function setCompleted(uint completed) public restricted { + last_completed_migration = completed; + } + + function upgrade(address _newAddress) public restricted { + Migrations upgraded = Migrations(_newAddress); + upgraded.setCompleted(last_completed_migration); + } +} + +/* solium-enable mixedcase */ + diff --git a/contracts/Ownable.sol b/contracts/Ownable.sol new file mode 100644 index 0000000..7b6bff5 --- /dev/null +++ b/contracts/Ownable.sol @@ -0,0 +1,39 @@ +pragma solidity ^0.5.0; + + +/** + * @title Ownable + * @dev The Ownable contract has an owner address, and provides basic authorization control + * functions, this simplifies the implementation of "user permissions". + */ +contract Ownable { + address public owner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev The Ownable constructor sets the original `owner` of the contract to the sender + * account. + */ + constructor() public { + owner = msg.sender; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(msg.sender == owner, "Only owner accessible"); + _; + } + + /** + * @dev Allows the current owner to transfer control of the contract to a newOwner. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) public onlyOwner { + require(newOwner != address(0), "Address not equal to zero"); + emit OwnershipTransferred(owner, newOwner); + owner = newOwner; + } +} diff --git a/ProxyLogics/default.sol b/contracts/ProxyLogics/default.sol similarity index 89% rename from ProxyLogics/default.sol rename to contracts/ProxyLogics/default.sol index d5f22b0..9a5df53 100644 --- a/ProxyLogics/default.sol +++ b/contracts/ProxyLogics/default.sol @@ -1,5 +1,4 @@ -pragma solidity ^0.4.23; - +pragma solidity ^0.5.0; interface IERC20 { function transfer(address to, uint256 value) external returns (bool); @@ -9,15 +8,19 @@ interface ICDP { function give(bytes32 cup, address guy) external; } -contract ProxyTest { +contract ProxyTest { event LogTransferETH(address dest, uint amount); event LogTransferERC20(address token, address dest, uint amount); event LogTransferCDP(address dest, uint num); function transferETH(address dest, uint amount) public payable { dest.transfer(amount); - emit LogTransferETH(dest, amount); + + emit LogTransferETH( + dest, + amount + ); } function transferERC20(address tokenAddr, address dest, address amount) public { @@ -31,5 +34,4 @@ contract ProxyTest { loanMaster.give(bytes32(num), dest); emit LogTransferCDP(dest, num); } - } \ No newline at end of file diff --git a/ProxyRegistry.sol b/contracts/ProxyRegistry.sol similarity index 96% rename from ProxyRegistry.sol rename to contracts/ProxyRegistry.sol index cf289fc..0586a24 100644 --- a/ProxyRegistry.sol +++ b/contracts/ProxyRegistry.sol @@ -1,9 +1,8 @@ -pragma solidity ^0.4.23; - +pragma solidity ^0.5.0; import "./UserProxy.sol"; -// ProxyRegistry + contract ProxyRegistry { event Created(address indexed sender, address indexed owner, address proxy); mapping(address => UserProxy) public proxies; @@ -26,9 +25,10 @@ contract ProxyRegistry { proxies[owner] == UserProxy(0) || proxies[owner].owner() != owner, "multiple-proxy-per-user-not-allowed" ); // Not allow new proxy if the user already has one and remains being the owner + proxy = new UserProxy(logicProxyAddr, activeDays); emit Created(msg.sender, owner, address(proxy)); proxy.setOwner(owner); proxies[owner] = proxy; } -} \ No newline at end of file +} diff --git a/UserProxy.sol b/contracts/UserProxy.sol similarity index 82% rename from UserProxy.sol rename to contracts/UserProxy.sol index 803cad1..1244ace 100644 --- a/UserProxy.sol +++ b/contracts/UserProxy.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.5.0; library SafeMath { @@ -9,11 +9,11 @@ library SafeMath { } } -contract UserAuth { +contract UserAuth { using SafeMath for uint; using SafeMath for uint256; - + event LogSetOwner(address indexed owner, bool isGuardian); event LogSetGuardian(address indexed guardian); @@ -33,16 +33,6 @@ contract UserAuth { _; } - function isAuth(address src) internal view returns (bool) { - if (src == address(this)) { - return true; - } else if (src == owner) { - return true; - } else { - return false; - } - } - function setOwner(address owner_) public auth { owner = owner_; emit LogSetOwner(owner, false); @@ -61,18 +51,20 @@ contract UserAuth { emit LogSetGuardian(guardian_); } + function isAuth(address src) internal view returns (bool) { + if (src == address(this)) { + return true; + } else if (src == owner) { + return true; + } else { + return false; + } + } } contract UserNote { - event LogNote( - bytes4 indexed sig, - address indexed guy, - bytes32 indexed foo, - bytes32 indexed bar, - uint wad, - bytes fax - ) anonymous; + event LogNote(bytes4 indexed sig, address indexed guy, bytes32 indexed foo, bytes32 indexed bar, uint wad, bytes fax); modifier note { bytes32 foo; @@ -81,31 +73,41 @@ contract UserNote { foo := calldataload(4) bar := calldataload(36) } - emit LogNote(msg.sig, msg.sender, foo, bar, msg.value, msg.data); + emit LogNote( + msg.sig, + msg.sender, + foo, + bar, + msg.value, + msg.data + ); _; } } + interface LogicRegistry { - function getLogic(address logicAddr) external view returns(bool); + function getLogic(address logicAddr) external view returns (bool); } + // checking if the logic proxy is authorised contract UserLogic { address public logicProxyAddr; - function isAuthorisedLogic(address logicAddr) internal view returns(bool) { + function isAuthorisedLogic(address logicAddr) internal view returns (bool) { LogicRegistry logicProxy = LogicRegistry(logicProxyAddr); return logicProxy.getLogic(logicAddr); } } + + // UserProxy // Allows code execution using a persistant identity This can be very // useful to execute a sequence of atomic actions. Since the owner of // the proxy can be changed, this allows for dynamic ownership models // i.e. a multisig contract UserProxy is UserAuth, UserNote, UserLogic { - constructor(address logicProxyAddr_, uint activePeriod_) public { logicProxyAddr = logicProxyAddr_; lastActivity = block.timestamp; @@ -114,13 +116,7 @@ contract UserProxy is UserAuth, UserNote, UserLogic { function() external payable {} - function execute(address _target, bytes memory _data) - public - auth - note - payable - returns (bytes memory response) - { + function execute(address _target, bytes memory _data) public payable auth note returns (bytes memory response) { require(_target != address(0), "user-proxy-target-address-required"); require(isAuthorisedLogic(_target), "logic-proxy-address-not-allowed"); lastActivity = block.timestamp; @@ -135,10 +131,10 @@ contract UserProxy is UserAuth, UserNote, UserLogic { returndatacopy(add(response, 0x20), 0, size) switch iszero(succeeded) - case 1 { - // throw if delegatecall failed - revert(add(response, 0x20), size) - } + case 1 { + // throw if delegatecall failed + revert(add(response, 0x20), size) + } } } -} \ No newline at end of file +} diff --git a/migrations/1_initial_migration.js b/migrations/1_initial_migration.js new file mode 100644 index 0000000..100bd2c --- /dev/null +++ b/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +const Migrations = artifacts.require('./Migrations.sol') + +module.exports = async deployer => { + await deployer.deploy(Migrations) +} diff --git a/migrations/2_contract_migrations.js b/migrations/2_contract_migrations.js new file mode 100644 index 0000000..a3be069 --- /dev/null +++ b/migrations/2_contract_migrations.js @@ -0,0 +1,5 @@ +const ownableFactory = artifacts.require('Ownable.sol') + +module.exports = async deployer => { + await deployer.deploy(ownableFactory) +} diff --git a/mocha-smart-contracts-config.json b/mocha-smart-contracts-config.json new file mode 100644 index 0000000..8e7acad --- /dev/null +++ b/mocha-smart-contracts-config.json @@ -0,0 +1,10 @@ +{ + "reporterEnabled": "mocha-junit-reporter, eth-gas-reporter", + "mochaJunitReporterReporterOptions": { + "mochaFile": "./test-results/test-contract-results.xml" + }, + "reporterOptions": { + "currency": "USD", + "gasPrice": 21 + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..1d5acb3 --- /dev/null +++ b/package.json @@ -0,0 +1,68 @@ +{ + "name": "smart-contract-starter", + "description": "Boilerplate for your next Smart Contract, made simple.", + "version": "0.0.1", + "author": "Ravindra Kumar ", + "license": "MIT", + "main": "truffle.js", + "directories": { + "test": "test" + }, + "scripts": { + "ganache": "ganache-cli -e 300 -p 9545 -m 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat' --accounts 30 > /dev/null &", + "stop": "sudo kill `sudo lsof -t -i:9545`", + "test": "npm run ganache sleep 5 && npm run lint ./ && npm run solium && truffle test && npm run stop", + "test:gas-reporter": "GAS_REPORTER=true npm run test", + "test-ci": "GAS_REPORTER=true npm run ganache sleep 5 && npm run lint ./ && npm run solium && truffle test", + "coverage": "./node_modules/.bin/solidity-coverage", + "lint": "eslint ./test", + "lint:fix": "eslint ./ --fix", + "solium": "solium -d contracts/", + "solium:fix": "solium -d contracts/ --fix", + "build": "npm run clean:contracts && truffle compile" + }, + "dependencies": { + "bn.js": "^4.11.8", + "dotenv": "^6.2.0", + "ethereumjs-wallet": "^0.6.3", + "npm-check-updates": "^2.15.0", + "openzeppelin-solidity": "^2.0.0", + "truffle": "^5.0.0", + "truffle-hdwallet-provider": "^1.0.1", + "web3": "^1.0.0-beta.37", + "webpack": "^4.28.1" + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-eslint": "10.0.1", + "babel-polyfill": "^6.26.0", + "babel-preset-env": "^1.7.0", + "babel-preset-es2015": "6.24.1", + "babel-preset-stage-2": "6.24.1", + "babel-preset-stage-3": "6.24.1", + "babel-register": "6.26.0", + "chai": "4.2.0", + "chai-as-promised": "7.1.1", + "chai-bignumber": "3.0.0", + "coveralls": "3.0.2", + "eslint": "5.10.0", + "eslint-config-prettier": "^3.3.0", + "eslint-config-standard": "^12.0.0", + "eslint-plugin-babel": "^5.3.0", + "eslint-plugin-compat": "^2.6.3", + "eslint-plugin-import": "2.14.0", + "eslint-plugin-node": "8.0.0", + "eslint-plugin-prettier": "^3.0.0", + "eslint-plugin-promise": "4.0.1", + "eslint-plugin-security": "^1.4.0", + "eslint-plugin-standard": "^4.0.0", + "eth-gas-reporter": "^0.1.12", + "ganache-cli": "^6.2.5", + "mocha-junit-reporter": "^1.18.0", + "mocha-multi-reporters": "^1.1.7", + "prettier": "^1.16.4", + "prettier-plugin-solidity-refactor": "^1.0.0-alpha.14", + "solidity-coverage": "0.5.11", + "solium": "1.1.8" + } +} diff --git a/test/Ownable.test.js b/test/Ownable.test.js new file mode 100644 index 0000000..6a401fe --- /dev/null +++ b/test/Ownable.test.js @@ -0,0 +1,31 @@ +const { assertRevert } = require('./helpers/general') + +const Ownable = artifacts.require('Ownable') + +contract('Ownable', accounts => { + let ownable + + beforeEach(async () => { + ownable = await Ownable.new() + }) + + it('should have an owner', async () => { + const owner = await ownable.owner() + assert.isTrue(owner !== 0) + }) + + it('changes owner after transfer', async () => { + const other = accounts[1] + await ownable.transferOwnership(other) + const owner = await ownable.owner() + + assert.isTrue(owner === other) + }) + + it('should prevent non-owners from transfering', async () => { + const other = accounts[2] + const owner = await ownable.owner.call() + assert.isTrue(owner !== other) + await assertRevert(ownable.transferOwnership(other, { from: other })) + }) +}) diff --git a/test/helpers/general.js b/test/helpers/general.js new file mode 100644 index 0000000..57cba27 --- /dev/null +++ b/test/helpers/general.js @@ -0,0 +1,158 @@ +const { BN } = web3.utils + +const decimals18 = new BN(10).pow(new BN(18)) +const bigZero = new BN(0) +const addressZero = `0x${'0'.repeat(40)}` +const bytes32Zero = '0x' + '00'.repeat(32) +const gasPrice = new BN(5e9) + +const assertRevert = async promise => { + try { + await promise + assert.fail('Expected revert not received') + } catch (error) { + const revertFound = error.message.search('revert') >= 0 + assert(revertFound, `Expected "revert", got ${error} instead`) + } +} + +const assertJump = async promise => { + try { + await promise + assert.fail('Expected invalid opcode not received') + } catch (error) { + const invalidOpcodeReceived = error.message.search('invalid opcode') >= 0 + assert( + invalidOpcodeReceived, + `Expected "invalid opcode", got ${error} instead` + ) + } +} + +const assertThrow = async promise => { + try { + await promise + } catch (error) { + // TODO: Check jump destination to destinguish between a throw + // and an actual invalid jump. + const invalidOpcode = error.message.search('invalid opcode') >= 0 + // TODO: When we contract A calls contract B, and B throws, instead + // of an 'invalid jump', we get an 'out of gas' error. How do + // we distinguish this from an actual out of gas event? (The + // testrpc log actually show an 'invalid jump' event.) + const outOfGas = error.message.search('out of gas') >= 0 + const revert = error.message.search('revert') >= 0 + const exception = + error.message.search( + 'VM Exception while processing transaction: revert' + ) >= 0 + assert( + invalidOpcode || exception || outOfGas || revert, + "Expected throw, got '" + error + "' instead" + ) + return + } + + assert.fail('Expected throw not received') +} + +const waitForEvent = (contract, event, optTimeout) => + new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + clearTimeout(timeout) + return reject(new Error('Timeout waiting for contractEvent')) + }, optTimeout || 5000) + + const eventEmitter = contract.contract.events[event]() + eventEmitter + .on('data', data => { + eventEmitter.unsubscribe() + clearTimeout(timeout) + resolve(data) + }) + .on('changed', data => { + clearTimeout() + eventEmitter.unsubscribe() + resolve(data) + }) + .on('error', err => { + eventEmitter.unsubscribe() + reject(err) + }) + }) + +const areInRange = (num1, num2, range) => { + const bigNum1 = new BN(num1.toString()) + const bigNum2 = new BN(num2.toString()) + const bigRange = new BN(range.toString()) + + if (bigNum1.equals(bigNum2)) { + return true + } + + const larger = bigNum1.gt(bigNum2) ? bigNum1 : bigNum2 + const smaller = bigNum1.lt(bigNum2) ? bigNum1 : bigNum2 + + return larger.sub(smaller).lt(bigRange) +} + +const getNowInSeconds = () => new BN(Date.now()).div(1000).floor(0) + +const trimBytes32Array = bytes32Array => + bytes32Array.filter(bytes32 => bytes32 != bytes32Zero) + +const getEtherBalance = address => { + return new Promise((resolve, reject) => { + web3.eth.getBalance(address, (err, res) => { + if (err) reject(err) + + resolve(res) + }) + }) +} + +const getTxInfo = txHash => { + if (typeof txHash === 'object') { + return txHash.receipt + } + + return new Promise((resolve, reject) => { + web3.eth.getTransactionReceipt(txHash, (err, res) => { + if (err) { + reject(err) + } + + resolve(res) + }) + }) +} + +const sendTransaction = args => { + return new Promise(function(resolve, reject) { + web3.eth.sendTransaction(args, (err, res) => { + if (err) { + reject(err) + } else { + resolve(res) + } + }) + }) +} + +module.exports = { + decimals18, + bigZero, + addressZero, + bytes32Zero, + gasPrice, + assertRevert, + assertJump, + assertThrow, + waitForEvent, + areInRange, + getNowInSeconds, + trimBytes32Array, + getEtherBalance, + getTxInfo, + sendTransaction +} diff --git a/truffle-box.json b/truffle-box.json new file mode 100755 index 0000000..8d5b698 --- /dev/null +++ b/truffle-box.json @@ -0,0 +1,18 @@ +{ + "ignore": [ + "README.md", + "package-lock.json" + ], + "commands": { + "Compile contracts": "truffle compile", + "Migrate contracts": "truffle migrate", + "Test contracts": "truffle test", + "Run eslint": "npm run lint", + "Run solium": "npm run solium", + "Run solidity-coverage": "npm run coverage", + "Run lint, solium, and truffle test": "npm run test" + }, + "hooks": { + "post-unpack": "npm install" + } +} \ No newline at end of file diff --git a/truffle.js b/truffle.js new file mode 100644 index 0000000..86d4281 --- /dev/null +++ b/truffle.js @@ -0,0 +1,56 @@ +require('dotenv').config() +const HDWalletProvider = require('truffle-hdwallet-provider') + +const rinkebyWallet = + 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat' +const rinkebyProvider = new HDWalletProvider( + rinkebyWallet, + 'https://rinkeby.infura.io/' +) + +const ropstenWallet = + 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat' +const ropstenProvider = new HDWalletProvider( + ropstenWallet, + 'https://ropsten.infura.io/' +) + +module.exports = { + migrations_directory: './migrations', + networks: { + test: { + host: 'localhost', + port: 9545, + network_id: '*', + gas: 6.5e6, + gasPrice: 5e9, + websockets: true + }, + ropsten: { + network_id: 3, + gas: 6.5e6, + gasPrice: 5e9, + provider: () => ropstenProvider + }, + rinkeby: { + network_id: 4, + gas: 6.5e6, + gasPrice: 5e9, + provider: () => rinkebyProvider + } + }, + solc: { + optimizer: { + enabled: true, + runs: 500 + } + }, + mocha: { + reporter: 'mocha-multi-reporters', + useColors: true, + enableTimeouts: false, + reporterOptions: { + configFile: './mocha-smart-contracts-config.json' + } + } +}