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' + } + } +}