From 3503b9f2dda138fa1921c81d97a2950f53f4a386 Mon Sep 17 00:00:00 2001 From: Kevin Siegler Date: Fri, 24 Apr 2020 16:02:41 -0500 Subject: [PATCH] chore(template): add full experimental template --- templates/experimental/.eslintrc.js | 65 +++ templates/experimental/.prettierrc | 13 + templates/experimental/.solcover.js | 5 + templates/experimental/.soliumignore | 0 templates/experimental/.soliumrc.json | 23 + templates/experimental/README.md | 83 ++++ templates/experimental/arapp.json | 22 + .../experimental/contracts/BaseCache.sol | 140 ++++++ .../experimental/contracts/BaseOEApps.sol | 219 +++++++++ .../experimental/contracts/BaseTemplate.sol | 423 ++++++++++++++++++ .../contracts/OpenEnterpriseTemplate.sol | 252 +++++++++++ .../contracts/test/TestImports.sol | 15 + templates/experimental/manifest.json | 3 + templates/experimental/migrations/.keep | 0 templates/experimental/package.json | 74 +++ templates/experimental/public/.gitkeep | 0 templates/experimental/scripts/create-dao.js | 35 ++ templates/experimental/scripts/deploy.js | 18 + templates/experimental/scripts/recover-dao.js | 114 +++++ templates/experimental/temp/helpers/apps.js | 39 ++ templates/experimental/temp/helpers/events.js | 36 ++ templates/experimental/temp/lib/OEDeployer.js | 47 ++ .../temp/lib/TemplatesDeployer.js | 216 +++++++++ templates/experimental/temp/lib/arapp-file.js | 49 ++ templates/experimental/temp/lib/ens.js | 31 ++ templates/experimental/temp/lib/network.js | 29 ++ .../temp/scripts/deploy-standardBounties.js | 44 ++ .../temp/scripts/deploy-template.js | 31 ++ .../experimental/temp/scripts/new-dao.js | 73 +++ .../experimental/temp/scripts/test-ganache.sh | 69 +++ .../test/OpenEnterpriseTemplate.test.js | 334 ++++++++++++++ templates/experimental/truffle-config.js | 14 + 32 files changed, 2516 insertions(+) create mode 100644 templates/experimental/.eslintrc.js create mode 100644 templates/experimental/.prettierrc create mode 100644 templates/experimental/.solcover.js create mode 100644 templates/experimental/.soliumignore create mode 100644 templates/experimental/.soliumrc.json create mode 100644 templates/experimental/README.md create mode 100644 templates/experimental/arapp.json create mode 100644 templates/experimental/contracts/BaseCache.sol create mode 100644 templates/experimental/contracts/BaseOEApps.sol create mode 100644 templates/experimental/contracts/BaseTemplate.sol create mode 100644 templates/experimental/contracts/OpenEnterpriseTemplate.sol create mode 100644 templates/experimental/contracts/test/TestImports.sol create mode 100644 templates/experimental/manifest.json create mode 100644 templates/experimental/migrations/.keep create mode 100644 templates/experimental/package.json create mode 100644 templates/experimental/public/.gitkeep create mode 100644 templates/experimental/scripts/create-dao.js create mode 100644 templates/experimental/scripts/deploy.js create mode 100644 templates/experimental/scripts/recover-dao.js create mode 100644 templates/experimental/temp/helpers/apps.js create mode 100644 templates/experimental/temp/helpers/events.js create mode 100644 templates/experimental/temp/lib/OEDeployer.js create mode 100644 templates/experimental/temp/lib/TemplatesDeployer.js create mode 100644 templates/experimental/temp/lib/arapp-file.js create mode 100644 templates/experimental/temp/lib/ens.js create mode 100644 templates/experimental/temp/lib/network.js create mode 100644 templates/experimental/temp/scripts/deploy-standardBounties.js create mode 100644 templates/experimental/temp/scripts/deploy-template.js create mode 100644 templates/experimental/temp/scripts/new-dao.js create mode 100755 templates/experimental/temp/scripts/test-ganache.sh create mode 100644 templates/experimental/test/OpenEnterpriseTemplate.test.js create mode 100644 templates/experimental/truffle-config.js diff --git a/templates/experimental/.eslintrc.js b/templates/experimental/.eslintrc.js new file mode 100644 index 000000000..963c07057 --- /dev/null +++ b/templates/experimental/.eslintrc.js @@ -0,0 +1,65 @@ +module.exports = { + env: { + browser: true, + es6: true, + node: true, + commonjs: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:import/errors', + 'plugin:react/recommended', + 'plugin:jsx-a11y/recommended', + ], + parser: 'babel-eslint', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, + sourceType: 'module', + }, + plugins: ['react'], + rules: { + indent: ['error', 2], + 'linebreak-style': ['error', 'unix'], + quotes: ['error', 'single'], + 'react/no-typos': 1, + semi: ['error', 'never'], + 'array-bracket-spacing': [ + 'error', + 'always', + { + objectsInArrays: false, + arraysInArrays: false, + singleValue: false, + }, + ], + 'func-style': ['warn', 'declaration', { allowArrowFunctions: true }], + 'object-curly-spacing': ['error', 'always'], + // 'import/no-unused-modules': [ + // 'warn', + // { + // unusedExports: true, + // missingExports: true, + // ignoreExports: [], + // }, + // ], + 'no-undef': 'error', + 'no-unused-vars': [ + 'warn', + { vars: 'all', args: 'after-used', ignoreRestSiblings: false }, + ], + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'react/jsx-uses-react': 'warn', + 'react/jsx-uses-vars': 'warn', + 'react/jsx-filename-extension': 'off', + 'react/no-unused-prop-types': 'warn', + 'sort-imports': ['warn', { ignoreDeclarationSort: true }], + }, + settings: { + react: { + version: 'detect', + }, + }, +} diff --git a/templates/experimental/.prettierrc b/templates/experimental/.prettierrc new file mode 100644 index 000000000..ba5a8d1eb --- /dev/null +++ b/templates/experimental/.prettierrc @@ -0,0 +1,13 @@ +{ + "trailingComma": "es5", + "semi": false, + "singleQuote": true, + "overrides": [ + { + "files": ".babelrc", + "options": { + "parser": "json" + } + } + ] +} diff --git a/templates/experimental/.solcover.js b/templates/experimental/.solcover.js new file mode 100644 index 000000000..e9f35a38f --- /dev/null +++ b/templates/experimental/.solcover.js @@ -0,0 +1,5 @@ +module.exports = { + norpc: true, + skipFiles: ['test'], + deepSkip: true +} diff --git a/templates/experimental/.soliumignore b/templates/experimental/.soliumignore new file mode 100644 index 000000000..e69de29bb diff --git a/templates/experimental/.soliumrc.json b/templates/experimental/.soliumrc.json new file mode 100644 index 000000000..e72ccdf67 --- /dev/null +++ b/templates/experimental/.soliumrc.json @@ -0,0 +1,23 @@ +{ + "extends": "solium:all", + "rules": { + "imports-on-top": ["error"], + "variable-declarations": ["error"], + "array-declarations": ["error"], + "operator-whitespace": ["error"], + "lbrace": ["error"], + "mixedcase": 0, + "camelcase": ["error"], + "uppercase": 0, + "no-empty-blocks": ["error"], + "no-unused-vars": ["error"], + "quotes": ["error"], + "indentation": ["error", 4], + "whitespace": ["error"], + "deprecated-suicide": ["error"], + "arg-overflow": ["error", 8], + "pragma-on-top": ["error"], + "security/enforce-explicit-visibility": ["error"], + "error-reason": ["error"] + } +} diff --git a/templates/experimental/README.md b/templates/experimental/README.md new file mode 100644 index 000000000..dcd352c1c --- /dev/null +++ b/templates/experimental/README.md @@ -0,0 +1,83 @@ +# Open Enterprise DEV template + +The Open Enterprise template includes the collection of Aragon apps that enable organizations to curate issues, collectively budget, and design custom reward and bounty programs. + +## Usage + +Create a new token for the Open Enterprise entity: + +``` +template.newToken(name, symbol) +``` + +- `name`: Name for the token used in the organization +- `symbol`: Symbol for the token used in the organization + +Create a new Open Enterprise entity: + +``` +template.newInstance(name, members, votingSettings, financePeriod, useAgentAsVault) +``` + +- `id`: Id for org, will assign `[id].aragonid.eth` +- `members`: Array of member addresses (1 token will be minted for each member) +- `votingSettings`: Array of `[supportRequired, minAcceptanceQuorum, voteDuration]` to set up the voting app of the organization +- `financePeriod`: Initial duration for accounting periods, it can be set to zero in order to use the default of 30 days. +- `useAgentAsVault`: Use an Agent app as a more advanced form of Vault app + +Alternatively, create a new Open Enterprise entity with a Payroll app: + +``` +template.newInstance(name, members, votingSettings, financePeriod, useAgentAsVault, payrollSettings) +``` + +- `payrollSettings`: Array of `[address denominationToken , IFeed priceFeed, uint64 rateExpiryTime, address employeeManager (set to voting if 0x0)]` for the Payroll app + +## Deploying templates + +After deploying ENS, APM and AragonID, just run: + +``` +npm run deploy:rinkeby +``` + +The network details will be automatically selected by the `arapp.json`'s environments. + +## Permissions + +| App | Permission | Grantee | Manager | +|-------------------|-----------------------|---------------|---------| +| Kernel | APP_MANAGER | Voting | Voting | +| ACL | CREATE_PERMISSIONS | Voting | Voting | +| EVMScriptRegistry | REGISTRY_MANAGER | Voting | Voting | +| EVMScriptRegistry | REGISTRY_ADD_EXECUTOR | Voting | Voting | +| Voting | CREATE_VOTES | Token Manager | Voting | +| Voting | MODIFY_QUORUM | Voting | Voting | +| Voting | MODIFY_SUPPORT | Voting | Voting | +| Agent or Vault | TRANSFER | Finance | Voting | +| Finance | CREATE_PAYMENTS | Voting | Voting | +| Finance | EXECUTE_PAYMENTS | Voting | Voting | +| Finance | MANAGE_PAYMENTS | Voting | Voting | +| Token Manager | MINT | Voting | Voting | +| Token Manager | BURN | Voting | Voting | + +### Additional permissions if the Agent app is installed + +| App | Permission | Grantee | Manager | +|-------------------|-----------------------|---------------|---------| +| Agent | RUN_SCRIPT | Voting | Voting | +| Agent | EXECUTE | Voting | Voting | + +### Additional permissions if the Payroll app is installed + +| App | Permission | Grantee | Manager | +|---------------------|----------------------------|---------------------|---------------| +| Finance | CREATE_PAYMENTS | Payroll | Voting | +| Payroll | ADD_BONUS_ROLE | EOA or Voting | Voting | +| Payroll | ADD_EMPLOYEE_ROLE | EOA or Voting | Voting | +| Payroll | ADD_REIMBURSEMENT_ROLE | EOA or Voting | Voting | +| Payroll | TERMINATE_EMPLOYEE_ROLE | EOA or Voting | Voting | +| Payroll | SET_EMPLOYEE_SALARY_ROLE | EOA or voting | Voting | +| Payroll | MODIFY_PRICE_FEED_ROLE | Voting | Voting | +| Payroll | MODIFY_RATE_EXPIRY_ROLE | Voting | Voting | +| Payroll | MANAGE_ALLOWED_TOKENS_ROLE | Voting | Voting | diff --git a/templates/experimental/arapp.json b/templates/experimental/arapp.json new file mode 100644 index 000000000..15c9f9974 --- /dev/null +++ b/templates/experimental/arapp.json @@ -0,0 +1,22 @@ +{ + "environments": { + "default": { + "appName": "open-enterprise-template.aragonpm.eth", + "network": "rpc", + "registry": "0x5f6f7e8cc7346a11ca2def8f827b7a0b612c56a1" + }, + "dev": { + "appName": "oe-dev-template.aragonpm.eth", + "network": "rpc", + "registry": "0x5f6f7e8cc7346a11ca2def8f827b7a0b612c56a1" + }, + "rinkeby": { + "appName": "open-enterprise-template-experimental.open.aragonpm.eth", + "network": "rpc", + "registry": "0x98Df287B6C145399Aaa709692c8D308357bC085D", + "wsRPC": "wss://rinkeby.eth.aragon.network/ws" + } + }, + "roles": [], + "path": "contracts/DevTemplate.sol" +} diff --git a/templates/experimental/contracts/BaseCache.sol b/templates/experimental/contracts/BaseCache.sol new file mode 100644 index 000000000..e6b17519e --- /dev/null +++ b/templates/experimental/contracts/BaseCache.sol @@ -0,0 +1,140 @@ +pragma solidity 0.4.24; + +import "./BaseTemplate.sol"; +import { DotVoting } from "@autarklabs/apps-dot-voting/contracts/DotVoting.sol"; + + +contract BaseCache is BaseTemplate { + // string constant private ERROR_MISSING_BASE_CACHE = "TEMPLATE_MISSING_BASE_CACHE"; + + struct InstalledBase { + ACL acl; + Kernel dao; + Vault vault; + } + + struct InstalledTokens { + MiniMeToken token1; + MiniMeToken token2; + } + + struct InstalledTokenManagers { + TokenManager tokenManager1; + TokenManager tokenManager2; + WhitelistOracle whitelist; + } + + struct InstalledVotingApps { + DotVoting dotVoting; + Voting voting; + bool secondaryDot; + bool secondaryVoting; + } + + mapping (address => InstalledBase) internal baseCache; + mapping (address => InstalledTokens) internal tokensCache; + mapping (address => InstalledTokenManagers) internal tokenManagersCache; + mapping (address => InstalledVotingApps) internal votingAppsCache; + + constructor(address[5] _deployedSetupContracts) + BaseTemplate( + DAOFactory(_deployedSetupContracts[0]), + ENS(_deployedSetupContracts[1]), + MiniMeTokenFactory(_deployedSetupContracts[2]), + IFIFSResolvingRegistrar(_deployedSetupContracts[3]) + ) public {} + + function _cacheBase( + ACL _acl, + Kernel _dao, + Vault _vault, + address _owner + ) internal + { + InstalledBase storage baseInstance = baseCache[_owner]; + baseInstance.acl = _acl; + baseInstance.dao = _dao; + baseInstance.vault = _vault; + } + + function _cacheTokens( + MiniMeToken _token1, + MiniMeToken _token2, + address _owner + ) internal + { + InstalledTokens storage tokensInstance = tokensCache[_owner]; + tokensInstance.token1 = _token1; + tokensInstance.token2 = _token2; + } + + function _cacheTokenManagers( + TokenManager _tokenManager1, + TokenManager _tokenManager2, + WhitelistOracle _whitelist, + address _owner + ) internal + { + InstalledTokenManagers storage tokenManagersInstance = tokenManagersCache[_owner]; + tokenManagersInstance.tokenManager1 = _tokenManager1; + tokenManagersInstance.tokenManager2 = _tokenManager2; + tokenManagersInstance.whitelist = _whitelist; + } + + function _cacheVotingApps( + DotVoting _dotVoting, + Voting _voting, + bool _secondaryDot, + bool _secondaryVoting, + address _owner + ) internal + { + InstalledVotingApps storage votingAppsInstance = votingAppsCache[_owner]; + votingAppsInstance.dotVoting = _dotVoting; + votingAppsInstance.voting = _voting; + votingAppsInstance.secondaryDot = _secondaryDot; + votingAppsInstance.secondaryVoting = _secondaryVoting; + } + + function _popBaseCache(address _owner) internal returns (ACL, Kernel, Vault) { + // require(baseCache[_owner] != address(0), ERROR_MISSING_BASE_CACHE); + + InstalledBase storage baseInstance = baseCache[_owner]; + ACL acl = baseInstance.acl; + Kernel dao = baseInstance.dao; + Vault vault = baseInstance.vault; + + delete baseCache[_owner]; + return (acl, dao, vault); + } + + function _popTokensCache(address _owner) internal returns (MiniMeToken, MiniMeToken) { + InstalledTokens storage tokensInstance = tokensCache[_owner]; + MiniMeToken token1 = tokensInstance.token1; + MiniMeToken token2 = tokensInstance.token2; + + delete tokensCache[_owner]; + return (token1, token2); + } + + function _popTokenManagersCache(address _owner) internal returns (TokenManager, TokenManager, WhitelistOracle) { + InstalledTokenManagers storage tokenManagersInstance = tokenManagersCache[_owner]; + TokenManager tokenManager1 = tokenManagersInstance.tokenManager1; + TokenManager tokenManager2 = tokenManagersInstance.tokenManager2; + WhitelistOracle whitelist = tokenManagersInstance.whitelist; + + delete tokenManagersCache[_owner]; + return (tokenManager1, tokenManager2, whitelist); + } + + function _popVotingAppsCache(address _owner) internal returns (DotVoting, Voting, bool, bool) { + InstalledVotingApps storage votingAppsInstance = votingAppsCache[_owner]; + DotVoting dotVoting = votingAppsInstance.dotVoting; + Voting voting = votingAppsInstance.voting; + bool secondaryDot = votingAppsInstance.secondaryDot; + bool secondaryVoting = votingAppsInstance.secondaryVoting; + + delete votingAppsCache[_owner]; + return (dotVoting, voting, secondaryDot, secondaryVoting); + } +} diff --git a/templates/experimental/contracts/BaseOEApps.sol b/templates/experimental/contracts/BaseOEApps.sol new file mode 100644 index 000000000..0775c88b0 --- /dev/null +++ b/templates/experimental/contracts/BaseOEApps.sol @@ -0,0 +1,219 @@ +pragma solidity 0.4.24; + +import "@aragon/templates-shared/contracts/TokenCache.sol"; +import { About } from "@autarklabs/aragon-about/contracts/About.sol"; + +import "@autarklabs/apps-address-book/contracts/AddressBook.sol"; +import "@autarklabs/apps-allocations/contracts/Allocations.sol"; +import "@autarklabs/apps-discussions/contracts/DiscussionApp.sol"; +import { DotVoting } from "@autarklabs/apps-dot-voting/contracts/DotVoting.sol"; +import "@autarklabs/apps-projects/contracts/Projects.sol"; +import "@autarklabs/apps-rewards/contracts/Rewards.sol"; + +import "./BaseCache.sol"; + + +contract BaseOEApps is BaseCache, TokenCache { + // /* Hardcoded constant to save gas + //bytes32 constant internal ABOUT_APP_ID = apmNamehash("about.hatch"); // about.hatch.aragonpm.eth; + //bytes32 constant internal ADDRESS_BOOK_APP_ID = apmNamehash("address-book-experimental.open"); // address-book-experimental.open.aragonpm.eth + //bytes32 constant internal ALLOCATIONS_APP_ID = apmNamehash("allocations-experimental.open"); // allocations-experimental.open.aragonpm.eth + //bytes32 constant internal DISCUSSIONS_APP_ID = apmNamehash("discussions-experimental.open"); // discussions-experimental.open.aragonpm.eth + //bytes32 constant internal DOT_VOTING_APP_ID = apmNamehash("dot-voting-experimental.open"); // dot-voting-experimental.open.aragonpm.eth + //bytes32 constant internal PROJECTS_APP_ID = apmNamehash("projects-experimental.open"); // projects-experimental.open.aragonpm.eth + //bytes32 constant internal REWARDS_APP_ID = apmNamehash("rewards-experimental.open"); // rewards-experimental.open.aragonpm.eth; + // */ + // TODO: Move to HatchAPM // Main APM ? + bytes32 constant internal ABOUT_APP_ID = 0xc5dc24db02e4fa7866752fadcd38b600d9a7b04d03e06a5469937aaf56f76d8e; + bytes32 constant internal ADDRESS_BOOK_APP_ID = 0x298b5513a5cf1ac34532a6987252b67eebd0e5f4f8d58ea8d523209d4e445902; + bytes32 constant internal ALLOCATIONS_APP_ID = 0xd7f56e56cbe3d08cfabcc728099a172992966500b195767749de465bf3eb9fc6; + bytes32 constant internal DISCUSSIONS_APP_ID = 0x36ed2b69c7261556794cbbfdfff77470091d1f97a13064941ccb6a2c578ecc3d; + bytes32 constant internal DOT_VOTING_APP_ID = 0x6936893855b61c8676719f11ebde6fb8089a6d613d50b064786d543bb3799a00; + bytes32 constant internal PROJECTS_APP_ID = 0x8b2262358894dc727b6cc30678462dc32cf2ea77c6cffad012d21034e923fb7e; + bytes32 constant internal REWARDS_APP_ID = 0xd211eecce26d0278e7acc6f33fefa9b655e28e81cc899f333abe7d8e30deae80; + + string constant private ERROR_BOUNTIES_NOT_CONTRACT = "BOUNTIES_REGISTRY_NOT_CONTRACT"; + address constant internal ANY_ENTITY = address(-1); + Bounties internal bountiesRegistry; + address[] private whiteListed = [address(0), address(0), address(0)]; + + /** + * @dev Constructor for Open Enterprise Apps DAO + * @param _deployedSetupContracts Array of [DaoFactory, ENS, MiniMeTokenFactory, AragonID, StandardBounties] + * required pre-deployed contracts to set up the organization + */ + constructor(address[5] _deployedSetupContracts) + BaseCache(_deployedSetupContracts) + // internal // TODO: This makes the contract abstract + public + { + _ensureAragonIdIsValid(_deployedSetupContracts[3]); + _ensureMiniMeFactoryIsValid(_deployedSetupContracts[2]); + require(isContract(address(_deployedSetupContracts[4])), ERROR_BOUNTIES_NOT_CONTRACT); + + bountiesRegistry = Bounties(_deployedSetupContracts[4]); + whiteListed[1] = address(bountiesRegistry); + } + +/* ABOUT */ + + function _installAboutApp(Kernel _dao) internal returns (About) { + bytes memory initializeData = abi.encodeWithSelector(About(0).initialize.selector); + return About(_installNonDefaultApp(_dao, ABOUT_APP_ID, initializeData)); + } + + function _createAboutPermissions(ACL _acl, Kernel _dao, address _grantee, address _manager) internal returns (About) { + About about = _installAboutApp(_dao); + _acl.createPermission(_grantee, _dao, about.UPDATE_CONTENT(), _manager); + } + +/* ADDRESS-BOOK */ + + function _installAddressBookApp(Kernel _dao) internal returns (AddressBook) { + bytes memory initializeData = abi.encodeWithSelector(AddressBook(0).initialize.selector); + return AddressBook(_installNonDefaultApp(_dao, ADDRESS_BOOK_APP_ID, initializeData)); + } + + function _createAddressBookPermissions(ACL _acl, AddressBook _addressBook, address _grantee, address _manager) internal { + _acl.createPermission(_grantee, _addressBook, _addressBook.ADD_ENTRY_ROLE(), _manager); + _acl.createPermission(_grantee, _addressBook, _addressBook.REMOVE_ENTRY_ROLE(), _manager); + _acl.createPermission(_grantee, _addressBook, _addressBook.UPDATE_ENTRY_ROLE(), _manager); + } + +/* ALLOCATIONS */ + + function _installAllocationsApp(Kernel _dao, Vault _vault, uint64 _periodDuration) internal returns (Allocations) { + bytes memory initializeData = abi.encodeWithSelector(Allocations(0).initialize.selector, _vault, _periodDuration); + return Allocations(_installNonDefaultApp(_dao, ALLOCATIONS_APP_ID, initializeData)); + } + + function _createAllocationsPermissions( + ACL _acl, + Allocations _allocations, + address _createAllocationsGrantee, + address _createAccountsGrantee, + address _manager + ) + internal + { + _acl.createPermission(_createAccountsGrantee, _allocations, _allocations.CREATE_ACCOUNT_ROLE(), _manager); + _acl.createPermission(_createAccountsGrantee, _allocations, _allocations.CHANGE_BUDGETS_ROLE(), _manager); + _acl.createPermission(_createAllocationsGrantee, _allocations, _allocations.CREATE_ALLOCATION_ROLE(), _manager); + _acl.createPermission(ANY_ENTITY, _allocations, _allocations.EXECUTE_ALLOCATION_ROLE(), _manager); + _acl.createPermission(ANY_ENTITY, _allocations, _allocations.EXECUTE_PAYOUT_ROLE(), _manager); + } + +/* DOT-VOTING */ + + /** + * @param _dotVotingSettings Array of [minQuorum, candidateSupportPct, voteDuration] to set up the dot voting app of the organization + **/ + function _installDotVotingApp(Kernel _dao, MiniMeToken _token, uint64[3] memory _dotVotingSettings) internal returns (DotVoting) { + return _installDotVotingApp(_dao, _token, _dotVotingSettings[0], _dotVotingSettings[1], _dotVotingSettings[2]); + } + + function _installDotVotingApp( + Kernel _dao, + MiniMeToken _token, + uint64 _quorum, + uint64 _support, + uint64 _duration + ) + internal returns (DotVoting) + { + bytes memory initializeData = abi.encodeWithSelector(DotVoting(0).initialize.selector, _token, _quorum, _support, _duration); + return DotVoting(_installNonDefaultApp(_dao, DOT_VOTING_APP_ID, initializeData)); + } + + function _createDotVotingPermissions( + ACL _acl, + DotVoting _dotVoting, + address _grantee, + address _manager + ) + internal + { + _acl.createPermission(_grantee, _dotVoting, _dotVoting.ROLE_CREATE_VOTES(), _manager); + } + +/* DISCUSSIONS */ + + function _installDiscussionsApp(Kernel _dao) internal returns (DiscussionApp) { + bytes memory initializeData = abi.encodeWithSelector(DiscussionApp(0).initialize.selector); + return DiscussionApp(_installNonDefaultApp(_dao, DISCUSSIONS_APP_ID, initializeData)); + } + + function _createDiscussionsPermissions(ACL _acl, DiscussionApp _discussions, address _grantee, address _manager) internal { + _acl.createPermission(_grantee, _discussions, _discussions.EMPTY_ROLE(), _manager); + } + +/* PROJECTS */ + + function _installProjectsApp(Kernel _dao, Vault _vault) internal returns (Projects) { + bytes memory initializeData = abi.encodeWithSelector(Projects(0).initialize.selector, bountiesRegistry, _vault); + return Projects(_installNonDefaultApp(_dao, PROJECTS_APP_ID, initializeData)); + } + + function _createProjectsPermissions( + ACL _acl, + Projects _projects, + address _curator, + address _grantee, + address _manager + ) + internal + { + _acl.createPermission(_curator, _projects, _projects.CURATE_ISSUES_ROLE(), _manager); + _acl.createPermission(_grantee, _projects, _projects.FUND_ISSUES_ROLE(), _manager); + _acl.createPermission(_grantee, _projects, _projects.REMOVE_ISSUES_ROLE(), _manager); + _acl.createPermission(_grantee, _projects, _projects.FUND_OPEN_ISSUES_ROLE(), _manager); + _acl.createPermission(_grantee, _projects, _projects.UPDATE_BOUNTIES_ROLE(), _manager); + _acl.createPermission(_grantee, _projects, _projects.ADD_REPO_ROLE(), _manager); + _acl.createPermission(_grantee, _projects, _projects.CHANGE_SETTINGS_ROLE(), _manager); + _acl.createPermission(_grantee, _projects, _projects.REMOVE_REPO_ROLE(), _manager); + _acl.createPermission(_grantee, _projects, _projects.REVIEW_APPLICATION_ROLE(), _manager); + _acl.createPermission(_grantee, _projects, _projects.WORK_REVIEW_ROLE(), _manager); + } + +/* REWARDS */ + + function _installRewardsApp(Kernel _dao, Vault _vault) internal returns (Rewards) { + bytes memory initializeData = abi.encodeWithSelector(Rewards(0).initialize.selector, _vault); + return Rewards(_installNonDefaultApp(_dao, REWARDS_APP_ID, initializeData)); + } + + function _createRewardsPermissions( + ACL _acl, + Rewards _rewards, + address _grantee, + address _manager + ) + internal + { + _acl.createPermission(_grantee, _rewards, _rewards.ADD_REWARD_ROLE(), _manager); + } + +/* WHITELIST-ORACLE */ + + function _initializeWhitelistOracleApp(WhitelistOracle _whitelist, address _vault, address _finance) internal { + whiteListed[0] = _vault; + whiteListed[2] = _finance; + _whitelist.initialize(whiteListed); + } + +/* OPEN ENTERPRISE SPECIFIC VAULT PERMISSIONS */ + + function _grantVaultPermissions(ACL _acl, Vault _vault, Allocations _allocations, Projects _projects, Rewards _rewards) internal { + _acl.grantPermission(_allocations, _vault, _vault.TRANSFER_ROLE()); + _acl.grantPermission(_projects, _vault, _vault.TRANSFER_ROLE()); + _acl.grantPermission(_rewards, _vault, _vault.TRANSFER_ROLE()); + } + + /** + * @dev Overloaded from BaseTemplate to remove granted permissions, not needed for Open Enterprise + */ + function _transferPermissionFromTemplate(ACL _acl, address _app, bytes32 _permission, address _manager) internal { + _acl.revokePermission(address(this), _app, _permission); + _acl.setPermissionManager(_manager, _app, _permission); + } +} diff --git a/templates/experimental/contracts/BaseTemplate.sol b/templates/experimental/contracts/BaseTemplate.sol new file mode 100644 index 000000000..a30f88535 --- /dev/null +++ b/templates/experimental/contracts/BaseTemplate.sol @@ -0,0 +1,423 @@ +pragma solidity 0.4.24; + +import "@aragon/apps-agent/contracts/Agent.sol"; +import "@aragon/apps-finance/contracts/Finance.sol"; +import "@aragon/apps-payroll/contracts/Payroll.sol"; +import "@aragon/apps-shared-minime/contracts/MiniMeToken.sol"; +import "@aragon/apps-survey/contracts/Survey.sol"; +import "@aragon/apps-vault/contracts/Vault.sol"; +import "@aragon/apps-voting/contracts/Voting.sol"; + +import "@aragon/id/contracts/IFIFSResolvingRegistrar.sol"; +import "@aragon/os/contracts/acl/ACL.sol"; +import "@aragon/os/contracts/apm/APMNamehash.sol"; +import "@aragon/os/contracts/apm/Repo.sol"; +import "@aragon/os/contracts/common/IsContract.sol"; +import "@aragon/os/contracts/common/Uint256Helpers.sol"; +import "@aragon/os/contracts/factory/DAOFactory.sol"; +import "@aragon/os/contracts/kernel/Kernel.sol"; +import "@aragon/os/contracts/lib/ens/ENS.sol"; +import "@aragon/os/contracts/lib/ens/PublicResolver.sol"; + +// Custom Autark Apps +import "@autarklabs/apps-token-manager-custom/contracts/TokenManager.sol"; +import "@autarklabs/apps-whitelist-oracle/contracts/WhitelistOracle.sol"; + + +contract BaseTemplate is APMNamehash, IsContract { + using Uint256Helpers for uint256; + +// Hard-coded constants to save gas + // bytes32 constant internal AGENT_APP_ID = apmNamehash("agent"); // agent.aragonpm.eth + // bytes32 constant internal FINANCE_APP_ID = apmNamehash("finance"); // finance.aragonpm.eth + // bytes32 constant internal PAYROLL_APP_ID = apmNamehash("payroll"); // payroll.aragonpm.eth + // bytes32 constant internal SURVEY_APP_ID = apmNamehash("survey"); // survey.aragonpm.eth + // bytes32 constant internal TOKEN_MANAGER_APP_ID = apmNamehash("token-manager.hatch"); // token-manager.hatch.aragonpm.eth + // bytes32 constant internal VAULT_APP_ID = apmNamehash("vault"); // vault.aragonpm.eth + // bytes32 constant internal VOTING_APP_ID = apmNamehash("voting"); // voting.aragonpm.eth + // bytes32 constant internal WHITELIST_ORACLE_APP_ID = apmNamehash("whitelist-oracle.hatch"); // whitelist-oracle.hatch.aragonpm.eth + bytes32 constant internal AGENT_APP_ID = 0x9ac98dc5f995bf0211ed589ef022719d1487e5cb2bab505676f0d084c07cf89a; + bytes32 constant internal FINANCE_APP_ID = 0xbf8491150dafc5dcaee5b861414dca922de09ccffa344964ae167212e8c673ae; + bytes32 constant internal PAYROLL_APP_ID = 0x463f596a96d808cb28b5d080181e4a398bc793df2c222f6445189eb801001991; + bytes32 constant internal SURVEY_APP_ID = 0x030b2ab880b88e228f2da5a3d19a2a31bc10dbf91fb1143776a6de489389471e; + bytes32 constant internal TOKEN_MANAGER_APP_ID = 0xc568f11b5218b4d75fdc69c471ebdcffcb59025cc9119abfb35ed6d0efcbc4ff; + bytes32 constant internal VAULT_APP_ID = 0x7e852e0fcfce6551c13800f1e7476f982525c2b5277ba14b24339c68416336d1; + bytes32 constant internal VOTING_APP_ID = 0x9fa3927f639745e587912d4b0fea7ef9013bf93fb907d29faeab57417ba6e1d4; + bytes32 constant internal WHITELIST_ORACLE_APP_ID = 0x32ceb944f61770acf9d24fe42fd7ad630d08049a3b80b1475b120ab23569ba92; + + string constant private ERROR_ARAGON_ID_NOT_CONTRACT = "TEMPLATE_ARAGON_ID_NOT_CONTRACT"; + string constant private ERROR_ARAGON_ID_NOT_PROVIDED = "TEMPLATE_ARAGON_ID_NOT_PROVIDED"; + string constant private ERROR_CANNOT_CAST_VALUE_TO_ADDRESS = "TEMPLATE_CANNOT_CAST_VALUE_TO_ADDRESS"; + string constant private ERROR_DAO_FACTORY_NOT_CONTRACT = "TEMPLATE_DAO_FAC_NOT_CONTRACT"; + string constant private ERROR_ENS_NOT_CONTRACT = "TEMPLATE_ENS_NOT_CONTRACT"; + string constant private ERROR_INVALID_ID = "TEMPLATE_INVALID_ID"; + string constant private ERROR_MINIME_FACTORY_NOT_CONTRACT = "TEMPLATE_MINIME_FAC_NOT_CONTRACT"; + string constant private ERROR_MINIME_FACTORY_NOT_PROVIDED = "TEMPLATE_MINIME_FAC_NOT_PROVIDED"; + + DAOFactory internal daoFactory; + ENS internal ens; + IFIFSResolvingRegistrar internal aragonID; + MiniMeTokenFactory internal miniMeFactory; + + event DeployDao(address dao); + event DeployToken(address token); + event InstalledApp(address appProxy, bytes32 appId); + event SetupDao(address dao); + + constructor(DAOFactory _daoFactory, ENS _ens, MiniMeTokenFactory _miniMeFactory, IFIFSResolvingRegistrar _aragonID) public { + require(isContract(address(_daoFactory)), ERROR_DAO_FACTORY_NOT_CONTRACT); + require(isContract(address(_ens)), ERROR_ENS_NOT_CONTRACT); + + aragonID = _aragonID; + daoFactory = _daoFactory; + ens = _ens; + miniMeFactory = _miniMeFactory; + } + + /** + * @dev Create a DAO using the DAO Factory and grant the template root permissions so it has full + * control during setup. Once the DAO setup has finished, it is recommended to call the + * `_transferRootPermissionsFromTemplateAndFinalizeDAO()` helper to transfer the root + * permissions to the end entity in control of the organization. + */ + function _createDAO() internal returns (Kernel dao, ACL acl) { + dao = daoFactory.newDAO(this); + emit DeployDao(address(dao)); + acl = ACL(dao.acl()); + _createPermissionForTemplate(acl, dao, dao.APP_MANAGER_ROLE()); + } + + /* ACL */ + + function _createPermissions(ACL _acl, address[] memory _grantees, address _app, bytes32 _permission, address _manager) internal { + _acl.createPermission(_grantees[0], _app, _permission, address(this)); + for (uint256 i = 1; i < _grantees.length; i++) { + _acl.grantPermission(_grantees[i], _app, _permission); + } + _acl.revokePermission(address(this), _app, _permission); + _acl.setPermissionManager(_manager, _app, _permission); + } + + function _createPermissionForTemplate(ACL _acl, address _app, bytes32 _permission) internal { + _acl.createPermission(address(this), _app, _permission, address(this)); + } + + function _removePermissionFromTemplate(ACL _acl, address _app, bytes32 _permission) internal { + _acl.revokePermission(address(this), _app, _permission); + _acl.removePermissionManager(_app, _permission); + } + + function _transferRootPermissionsFromTemplateAndFinalizeDAO(Kernel _dao, address _to) internal { + _transferRootPermissionsFromTemplateAndFinalizeDAO(_dao, _to, _to); + } + + function _transferRootPermissionsFromTemplateAndFinalizeDAO(Kernel _dao, address _to, address _manager) internal { + ACL _acl = ACL(_dao.acl()); + _transferPermissionFromTemplate(_acl, _dao, _to, _dao.APP_MANAGER_ROLE(), _manager); + _transferPermissionFromTemplate(_acl, _acl, _to, _acl.CREATE_PERMISSIONS_ROLE(), _manager); + emit SetupDao(_dao); + } + + function _transferPermissionFromTemplate(ACL _acl, address _app, address _to, bytes32 _permission, address _manager) internal { + _acl.grantPermission(_to, _app, _permission); + _acl.revokePermission(address(this), _app, _permission); + _acl.setPermissionManager(_manager, _app, _permission); + } + + /* AGENT */ + + function _installDefaultAgentApp(Kernel _dao) internal returns (Agent) { + bytes memory initializeData = abi.encodeWithSelector(Agent(0).initialize.selector); + Agent agent = Agent(_installDefaultApp(_dao, AGENT_APP_ID, initializeData)); + // We assume that installing the Agent app as a default app means the DAO should have its + // Vault replaced by the Agent. Thus, we also set the DAO´s recovery app to the Agent. + _dao.setRecoveryVaultAppId(AGENT_APP_ID); + return agent; + } + + function _installNonDefaultAgentApp(Kernel _dao) internal returns (Agent) { + bytes memory initializeData = abi.encodeWithSelector(Agent(0).initialize.selector); + return Agent(_installNonDefaultApp(_dao, AGENT_APP_ID, initializeData)); + } + + function _createAgentPermissions(ACL _acl, Agent _agent, address _grantee, address _manager) internal { + _acl.createPermission(_grantee, _agent, _agent.EXECUTE_ROLE(), _manager); + _acl.createPermission(_grantee, _agent, _agent.RUN_SCRIPT_ROLE(), _manager); + } + + /* VAULT */ + + function _installVaultApp(Kernel _dao) internal returns (Vault) { + bytes memory initializeData = abi.encodeWithSelector(Vault(0).initialize.selector); + return Vault(_installDefaultApp(_dao, VAULT_APP_ID, initializeData)); + } + + function _createVaultPermissions(ACL _acl, Vault _vault, address _grantee, address _manager) internal { + _acl.createPermission(_grantee, _vault, _vault.TRANSFER_ROLE(), _manager); + } + + /* VOTING */ + + function _installVotingApp(Kernel _dao, MiniMeToken _token, uint64[3] memory _votingSettings) internal returns (Voting) { + return _installVotingApp(_dao, _token, _votingSettings[0], _votingSettings[1], _votingSettings[2]); + } + + function _installVotingApp( + Kernel _dao, + MiniMeToken _token, + uint64 _support, + uint64 _acceptance, + uint64 _duration + ) + internal returns (Voting) + { + bytes memory initializeData = abi.encodeWithSelector(Voting(0).initialize.selector, _token, _support, _acceptance, _duration); + return Voting(_installNonDefaultApp(_dao, VOTING_APP_ID, initializeData)); + } + + function _createVotingPermissions( + ACL _acl, + Voting _voting, + address _settingsGrantee, + address _createVotesGrantee, + address _manager + ) + internal + { + _acl.createPermission(_settingsGrantee, _voting, _voting.MODIFY_QUORUM_ROLE(), _manager); + _acl.createPermission(_settingsGrantee, _voting, _voting.MODIFY_SUPPORT_ROLE(), _manager); + _acl.createPermission(_createVotesGrantee, _voting, _voting.CREATE_VOTES_ROLE(), _manager); + } + + /* SURVEY */ + + function _installSurveyApp(Kernel _dao, MiniMeToken _token, uint64 _minParticipationPct, uint64 _surveyTime) internal returns (Survey) { + bytes memory initializeData = abi.encodeWithSelector(Survey(0).initialize.selector, _token, _minParticipationPct, _surveyTime); + return Survey(_installNonDefaultApp(_dao, SURVEY_APP_ID, initializeData)); + } + + function _createSurveyPermissions(ACL _acl, Survey _survey, address _grantee, address _manager) internal { + _acl.createPermission(_grantee, _survey, _survey.CREATE_SURVEYS_ROLE(), _manager); + _acl.createPermission(_grantee, _survey, _survey.MODIFY_PARTICIPATION_ROLE(), _manager); + } + + /* PAYROLL */ + + function _installPayrollApp( + Kernel _dao, + Finance _finance, + address _denominationToken, + IFeed _priceFeed, + uint64 _rateExpiryTime + ) + internal returns (Payroll) + { + bytes memory initializeData = abi.encodeWithSelector( + Payroll(0).initialize.selector, + _finance, + _denominationToken, + _priceFeed, + _rateExpiryTime + ); + return Payroll(_installNonDefaultApp(_dao, PAYROLL_APP_ID, initializeData)); + } + + /** + * @dev Internal function to configure payroll permissions. Note that we allow defining different managers for + * payroll since it may be useful to have one control the payroll settings (rate expiration, price feed, + * and allowed tokens), and another one to control the employee functionality (bonuses, salaries, + * reimbursements, employees, etc). + * @param _acl ACL instance being configured + * @param _acl Payroll app being configured + * @param _employeeManager Address that will receive permissions to handle employee payroll functionality + * @param _settingsManager Address that will receive permissions to manage payroll settings + * @param _permissionsManager Address that will be the ACL manager for the payroll permissions + */ + function _createPayrollPermissions( + ACL _acl, + Payroll _payroll, + address _employeeManager, + address _settingsManager, + address _permissionsManager + ) + internal + { + _acl.createPermission(_employeeManager, _payroll, _payroll.ADD_BONUS_ROLE(), _permissionsManager); + _acl.createPermission(_employeeManager, _payroll, _payroll.ADD_EMPLOYEE_ROLE(), _permissionsManager); + _acl.createPermission(_employeeManager, _payroll, _payroll.ADD_REIMBURSEMENT_ROLE(), _permissionsManager); + _acl.createPermission(_employeeManager, _payroll, _payroll.TERMINATE_EMPLOYEE_ROLE(), _permissionsManager); + _acl.createPermission(_employeeManager, _payroll, _payroll.SET_EMPLOYEE_SALARY_ROLE(), _permissionsManager); + + _acl.createPermission(_settingsManager, _payroll, _payroll.MODIFY_PRICE_FEED_ROLE(), _permissionsManager); + _acl.createPermission(_settingsManager, _payroll, _payroll.MODIFY_RATE_EXPIRY_ROLE(), _permissionsManager); + _acl.createPermission(_settingsManager, _payroll, _payroll.MANAGE_ALLOWED_TOKENS_ROLE(), _permissionsManager); + } + + function _unwrapPayrollSettings( + uint256[4] memory _payrollSettings + ) + internal pure returns (address denominationToken, IFeed priceFeed, uint64 rateExpiryTime, address employeeManager) + { + denominationToken = _toAddress(_payrollSettings[0]); + priceFeed = IFeed(_toAddress(_payrollSettings[1])); + rateExpiryTime = _payrollSettings[2].toUint64(); + employeeManager = _toAddress(_payrollSettings[3]); + } + + /* FINANCE */ + + function _installFinanceApp(Kernel _dao, Vault _vault, uint64 _periodDuration) internal returns (Finance) { + bytes memory initializeData = abi.encodeWithSelector(Finance(0).initialize.selector, _vault, _periodDuration); + return Finance(_installNonDefaultApp(_dao, FINANCE_APP_ID, initializeData)); + } + + function _createFinancePermissions(ACL _acl, Finance _finance, address _grantee, address _manager) internal { + _acl.createPermission(_grantee, _finance, _finance.EXECUTE_PAYMENTS_ROLE(), _manager); + _acl.createPermission(_grantee, _finance, _finance.MANAGE_PAYMENTS_ROLE(), _manager); + } + + function _createFinanceCreatePaymentsPermission(ACL _acl, Finance _finance, address _grantee, address _manager) internal { + _acl.createPermission(_grantee, _finance, _finance.CREATE_PAYMENTS_ROLE(), _manager); + } + + function _grantCreatePaymentPermission(ACL _acl, Finance _finance, address _to) internal { + _acl.grantPermission(_to, _finance, _finance.CREATE_PAYMENTS_ROLE()); + } + + function _transferCreatePaymentManagerFromTemplate(ACL _acl, Finance _finance, address _manager) internal { + _acl.setPermissionManager(_manager, _finance, _finance.CREATE_PAYMENTS_ROLE()); + } + + /* TOKEN MANAGER */ + + function _installTokenManagerApp( + Kernel _dao, + MiniMeToken _token, + bool _transferable, + uint256 _maxAccountTokens + ) + internal returns (TokenManager) + { + TokenManager tokenManager = TokenManager(_installNonDefaultApp(_dao, TOKEN_MANAGER_APP_ID)); + _token.changeController(tokenManager); + tokenManager.initialize(_token, _transferable, _maxAccountTokens); + return tokenManager; + } + + function _createTokenManagerPermissions(ACL _acl, TokenManager _tokenManager, address _grantee, address _manager) internal { + _acl.createPermission(_grantee, _tokenManager, _tokenManager.MINT_ROLE(), _manager); + _acl.createPermission(_grantee, _tokenManager, _tokenManager.BURN_ROLE(), _manager); + } + + function _mintTokens(ACL _acl, TokenManager _tokenManager, address[] memory _holders, uint256[] memory _stakes) internal { + _createPermissionForTemplate(_acl, _tokenManager, _tokenManager.MINT_ROLE()); + for (uint256 i = 0; i < _holders.length; i++) { + _tokenManager.mint(_holders[i], _stakes[i]); + } + _removePermissionFromTemplate(_acl, _tokenManager, _tokenManager.MINT_ROLE()); + } + + function _mintTokens(ACL _acl, TokenManager _tokenManager, address[] memory _holders, uint256 _stake) internal { + _createPermissionForTemplate(_acl, _tokenManager, _tokenManager.MINT_ROLE()); + for (uint256 i = 0; i < _holders.length; i++) { + _tokenManager.mint(_holders[i], _stake); + } + _removePermissionFromTemplate(_acl, _tokenManager, _tokenManager.MINT_ROLE()); + } + + function _mintTokens(ACL _acl, TokenManager _tokenManager, address _holder, uint256 _stake) internal { + _createPermissionForTemplate(_acl, _tokenManager, _tokenManager.MINT_ROLE()); + _tokenManager.mint(_holder, _stake); + _removePermissionFromTemplate(_acl, _tokenManager, _tokenManager.MINT_ROLE()); + } + + function _setOracle(ACL _acl, TokenManager _tokenManager, address _whitelistOracle) internal { + _createPermissionForTemplate(_acl, _tokenManager, _tokenManager.SET_ORACLE()); + _tokenManager.setOracle(_whitelistOracle); + _removePermissionFromTemplate(_acl, _tokenManager, _tokenManager.SET_ORACLE()); + } + + /* WHITELIST ORACLE */ + + function _installWhitelistOracleApp(Kernel _dao) internal returns (WhitelistOracle) { + return WhitelistOracle(_installNonDefaultApp(_dao, WHITELIST_ORACLE_APP_ID)); + } + + function _createWhitelistPermissions(ACL _acl, WhitelistOracle _whitelist, address _grantee, address _manager) internal { + _acl.createPermission(_grantee, _whitelist, _whitelist.ADD_SENDER_ROLE(), _manager); + _acl.createPermission(_grantee, _whitelist, _whitelist.REMOVE_SENDER_ROLE(), _manager); + } + + /* EVM SCRIPTS */ + + function _createEvmScriptsRegistryPermissions(ACL _acl, address _grantee, address _manager) internal { + EVMScriptRegistry registry = EVMScriptRegistry(_acl.getEVMScriptRegistry()); + _acl.createPermission(_grantee, registry, registry.REGISTRY_MANAGER_ROLE(), _manager); + _acl.createPermission(_grantee, registry, registry.REGISTRY_ADD_EXECUTOR_ROLE(), _manager); + } + + /* APPS */ + + function _installNonDefaultApp(Kernel _dao, bytes32 _appId) internal returns (address) { + return _installNonDefaultApp(_dao, _appId, new bytes(0)); + } + + function _installNonDefaultApp(Kernel _dao, bytes32 _appId, bytes memory _initializeData) internal returns (address) { + return _installApp(_dao, _appId, _initializeData, false); + } + + function _installDefaultApp(Kernel _dao, bytes32 _appId) internal returns (address) { + return _installDefaultApp(_dao, _appId, new bytes(0)); + } + + function _installDefaultApp(Kernel _dao, bytes32 _appId, bytes memory _initializeData) internal returns (address) { + return _installApp(_dao, _appId, _initializeData, true); + } + + function _installApp(Kernel _dao, bytes32 _appId, bytes memory _initializeData, bool _setDefault) internal returns (address) { + address latestBaseAppAddress = _latestVersionAppBase(_appId); + address instance = address(_dao.newAppInstance(_appId, latestBaseAppAddress, _initializeData, _setDefault)); + emit InstalledApp(instance, _appId); + return instance; + } + + function _latestVersionAppBase(bytes32 _appId) internal view returns (address base) { + Repo repo = Repo(PublicResolver(ens.resolver(_appId)).addr(_appId)); + (,base,) = repo.getLatest(); + } + + /* TOKEN */ + + function _createToken(string memory _name, string memory _symbol, uint8 _decimals) internal returns (MiniMeToken) { + require(address(miniMeFactory) != address(0), ERROR_MINIME_FACTORY_NOT_PROVIDED); + MiniMeToken token = miniMeFactory.createCloneToken(MiniMeToken(address(0)), 0, _name, _decimals, _symbol, true); + emit DeployToken(address(token)); + return token; + } + + function _ensureMiniMeFactoryIsValid(address _miniMeFactory) internal view { + require(isContract(address(_miniMeFactory)), ERROR_MINIME_FACTORY_NOT_CONTRACT); + } + + /* IDS */ + + function _validateId(string memory _id) internal pure { + require(bytes(_id).length > 0, ERROR_INVALID_ID); + } + + function _registerID(string memory _name, address _owner) internal { + require(address(aragonID) != address(0), ERROR_ARAGON_ID_NOT_PROVIDED); + aragonID.register(keccak256(abi.encodePacked(_name)), _owner); + } + + function _ensureAragonIdIsValid(address _aragonID) internal view { + require(isContract(address(_aragonID)), ERROR_ARAGON_ID_NOT_CONTRACT); + } + + /* HELPERS */ + + function _toAddress(uint256 _value) private pure returns (address) { + require(_value <= uint160(-1), ERROR_CANNOT_CAST_VALUE_TO_ADDRESS); + return address(_value); + } +} diff --git a/templates/experimental/contracts/OpenEnterpriseTemplate.sol b/templates/experimental/contracts/OpenEnterpriseTemplate.sol new file mode 100644 index 000000000..1bd1988f4 --- /dev/null +++ b/templates/experimental/contracts/OpenEnterpriseTemplate.sol @@ -0,0 +1,252 @@ +pragma solidity 0.4.24; + +import "./BaseOEApps.sol"; + + +contract OpenEnterpriseTemplate is BaseOEApps { + string constant private ERROR_MISSING_MEMBERS = "OPEN_ENTERPRISE_MISSING_MEMBERS"; + string constant private ERROR_BAD_VOTE_SETTINGS = "OPEN_ENTERPRISE_BAD_VOTE_SETTINGS"; + string constant private ERROR_BAD_DOT_VOTE_SETTINGS = "OPEN_ENTERPRISE_BAD_DOT_VOTE_SETTINGS"; + string constant private ERROR_BAD_MEMBERS_STAKES_LEN = "OPEN_ENTERPRISE_BAD_MEMBER_STAKES_LEN"; + + uint64 constant private DEFAULT_PERIOD = uint64(30 days); + uint8 constant private TOKEN_DECIMALS = uint8(18); + uint256 constant private UNLIMITED_TOKENS = uint256(0); + uint256 constant private ONE_TOKEN = uint256(1e18); + + /** + * @dev Constructor for Open Enterprise Apps DAO + * @param _deployedSetupContracts Array of [DaoFactory, ENS, MiniMeTokenFactory, AragonID, StandardBounties] + * required pre-deployed contracts to set up the organization + */ + constructor(address[5] _deployedSetupContracts) BaseOEApps(_deployedSetupContracts) public {} + + /** + * @dev Create a new MiniMe token and deploy a Open Enterprise DAO. + * @param _id String with the name for org, will assign `[id].aragonid.eth` + */ + function newTokensAndInstance( + string _id, + string _name1, + string _symbol1, + string _name2, + string _symbol2, + uint64[6] _votingSettings, + bool[2] _votingBools, + bool _useAgentAsVault + ) + external + { + (MiniMeToken token1, MiniMeToken token2) = newTokens(_name1, _symbol1, _name2, _symbol2); + _newInstance(_id, _votingSettings, _votingBools, token1, token2, _useAgentAsVault); + } + + /** + * @dev Install and configure TokenManager apps for previously created tokens + * @param _tokenBools Array of [fixedStake1?, transferable1?, fixedStake2?, transferable2?] + * related to the tokens settings + */ + function newTokenManagers( + address[] _members1, + uint256[] _stakes1, + address[] _members2, + uint256[] _stakes2, + bool[4] _tokenBools + ) public + { + _validateTokenSettings(_members1, _stakes1); + _validateTokenSettings(_members2, _stakes2); + (ACL acl, Kernel dao, Vault agentOrVault) = _popBaseCache(msg.sender); + (MiniMeToken token1, MiniMeToken token2) = _popTokensCache(msg.sender); + TokenManager tokenManager2 = TokenManager(0); + WhitelistOracle whitelist = WhitelistOracle(0); + + // Token 1 setup + TokenManager tokenManager1 = _installTokenManagerApp( + dao, + token1, + true, // it always needs to be transferable + _tokenBools[0] ? ONE_TOKEN : UNLIMITED_TOKENS + ); + + // Token 1 quasi-transferable + if (!_tokenBools[1]) { // token1 transferable? + if (address(whitelist) == address(0)) { + whitelist = _installWhitelistOracleApp(dao); + _setOracle(acl, tokenManager1, whitelist); + } + } + + // If token2 is set + if (address(token2) != address(0)) { + tokenManager2 = _installTokenManagerApp(dao, token2, true, _tokenBools[2] ? ONE_TOKEN : UNLIMITED_TOKENS); + _mintTokens(acl, tokenManager2, _members2, _stakes2); + if (!_tokenBools[3]) { // token2 transferable? + whitelist = _installWhitelistOracleApp(dao); + _setOracle(acl, tokenManager2, whitelist); + } + } + + _mintTokens(acl, tokenManager1, _members1, _stakes1); + _cacheBase(acl, dao, agentOrVault, msg.sender); + _cacheTokenManagers(tokenManager1, tokenManager2, whitelist, msg.sender); + } + + function finalizeDao( + uint64[2] _periods, + bool _useDiscussions + ) public + { + (ACL acl, Kernel dao, Vault agentOrVault) = _popBaseCache(msg.sender); + AddressBook addressBook = _installAddressBookApp(dao); + Allocations allocations = _installAllocationsApp(dao, agentOrVault, _periods[0] == 0 ? DEFAULT_PERIOD : _periods[0]); + Finance finance = _installFinanceApp(dao, agentOrVault, _periods[1] == 0 ? DEFAULT_PERIOD : _periods[1]); + Projects projects = _installProjectsApp(dao, agentOrVault); + Rewards rewards = _installRewardsApp(dao, agentOrVault); + DiscussionApp discussions = DiscussionApp(0); + if (_useDiscussions) { + discussions = _installDiscussionsApp(dao); + } + _setupPermissions( + acl, + dao, + addressBook, + allocations, + discussions, + finance, + projects, + rewards, + agentOrVault + ); + } + + /** + * @dev Create a new MiniMe token and cache it for the user + * @param _name1 String with the name for the primary token used by share holders in the organization + * @param _name2 String with the name for the reputation token used in the organization + * @param _symbol1 String with the symbol for the primary token used by share holders in the organization + * @param _symbol2 String with the symbol for the reputation token used in the organization + */ + function newTokens( + string _name1, + string _symbol1, + string _name2, + string _symbol2 + ) + public returns (MiniMeToken, MiniMeToken) + { + MiniMeToken token1 = _createToken(_name1, _symbol1, TOKEN_DECIMALS); + MiniMeToken token2 = MiniMeToken(0); + if (keccak256(abi.encodePacked(_symbol2)) != keccak256(abi.encodePacked(""))) { + token2 = _createToken(_name2, _symbol2, TOKEN_DECIMALS); + } + _cacheTokens(token1, token2, msg.sender); + + return (token1, token2); + } + + /** + * @dev Deploy a Open Enterprise DAO using a previously cached MiniMe token + * @param _id String with the name for org, will assign `[id].aragonid.eth` + * @param _votingBools Array of 2 booleans to select reference for apps: + * [DotVotingToken, VotingToken] true will select token2 as controller + */ + function _newInstance( + string _id, + uint64[6] memory _votingSettings, + bool[2] memory _votingBools, + MiniMeToken _token1, + MiniMeToken _token2, + bool _useAgentAsVault + ) + internal + { + _validateId(_id); + _validateVotingSettings(_votingSettings); + + (Kernel dao, ACL acl) = _createDAO(); + Vault agentOrVault = _useAgentAsVault ? _installDefaultAgentApp(dao) : _installVaultApp(dao); + DotVoting dotVoting = _installDotVotingApp( + dao, + _votingBools[0] ? _token2 : _token1, + _votingSettings[0], + _votingSettings[1], + _votingSettings[2] + ); + Voting voting = _installVotingApp(dao, _votingBools[1] ? _token2 : _token1, _votingSettings[3], _votingSettings[4], _votingSettings[5]); + + if (_useAgentAsVault) { + _createAgentPermissions(acl, Agent(agentOrVault), voting, voting); + } + _cacheVotingApps(dotVoting, voting, _votingBools[0], _votingBools[1], msg.sender); + _cacheBase(acl, dao, agentOrVault, msg.sender); + _registerID(_id, dao); + } + + function _setupPermissions( + ACL _acl, + Kernel _dao, + AddressBook _addressBook, + Allocations _allocations, + DiscussionApp _discussions, + Finance _finance, + Projects _projects, + Rewards _rewards, + Vault _agentOrVault + ) internal + { + _setupTokenPermissions(_acl, _dao, _finance, _agentOrVault); + (DotVoting dotVoting, Voting voting, , ) = _popVotingAppsCache(msg.sender); + + if (address(_discussions) != address(0)) { + _createDiscussionsPermissions(_acl, _discussions, ANY_ENTITY, voting); + } + _createAboutPermissions(_acl, _dao, voting, voting); + _createAddressBookPermissions(_acl, _addressBook, voting, voting); + _createAllocationsPermissions(_acl, _allocations, dotVoting, voting, voting); + _createEvmScriptsRegistryPermissions(_acl, voting, voting); + _createFinancePermissions(_acl, _finance, voting, voting); + _createFinanceCreatePaymentsPermission(_acl, _finance, voting, voting); + _createProjectsPermissions(_acl, _projects, dotVoting, voting, voting); + _createRewardsPermissions(_acl, _rewards, voting, voting); + _createVaultPermissions(_acl, _agentOrVault, _finance, address(this)); + _grantVaultPermissions(_acl, _agentOrVault, _allocations, _projects, _rewards); + + //Return permissions from the template + //_transferCreatePaymentManagerFromTemplate(_acl, _finance, voting); + _transferPermissionFromTemplate(_acl, _agentOrVault, _agentOrVault.TRANSFER_ROLE(), voting); + _transferRootPermissionsFromTemplateAndFinalizeDAO(_dao, voting); + } + + function _setupTokenPermissions( + ACL _acl, + Kernel _dao, + Finance _finance, + Vault _vault + ) internal + { + (DotVoting dotVoting, Voting voting, bool secondaryDot, bool secondaryVoting) = _popVotingAppsCache(msg.sender); + (TokenManager tokenManager1, TokenManager tokenManager2, WhitelistOracle whitelist) = _popTokenManagersCache(msg.sender); + if (address(tokenManager2) != address(0)) { + _createTokenManagerPermissions(_acl, tokenManager2, voting, voting); + } + if (address(whitelist) != address(0)) { + _initializeWhitelistOracleApp(whitelist, _vault, _finance); + _createWhitelistPermissions(_acl, whitelist, voting, voting); + } + _createDotVotingPermissions(_acl, dotVoting, secondaryDot ? tokenManager2 : tokenManager1, voting); + _createTokenManagerPermissions(_acl, tokenManager1, voting, voting); + _createVotingPermissions(_acl, voting, voting, secondaryVoting ? tokenManager2 : tokenManager1, voting); + + _cacheVotingApps(dotVoting, voting, secondaryDot, secondaryVoting, msg.sender); + } + + function _validateVotingSettings(uint64[6] memory _votingSettings) private pure { + require(_votingSettings.length == 6, ERROR_BAD_VOTE_SETTINGS); + } + + function _validateTokenSettings(address[] memory _members, uint256[] memory _stakes) private pure { + require(_members.length > 0, ERROR_MISSING_MEMBERS); + require(_members.length == _stakes.length, ERROR_BAD_MEMBERS_STAKES_LEN); + } +} diff --git a/templates/experimental/contracts/test/TestImports.sol b/templates/experimental/contracts/test/TestImports.sol new file mode 100644 index 000000000..05960790c --- /dev/null +++ b/templates/experimental/contracts/test/TestImports.sol @@ -0,0 +1,15 @@ +pragma solidity 0.4.24; + +import "@aragon/os/contracts/factory/ENSFactory.sol"; +import "@aragon/os/contracts/factory/APMRegistryFactory.sol"; +import "@aragon/os/contracts/factory/EVMScriptRegistryFactory.sol"; +import "@aragon/id/contracts/FIFSResolvingRegistrar.sol"; +import "@aragon/templates-shared/contracts/Migrations.sol"; + + +// HACK to workaround truffle artifact loading on dependencies +contract TestImports { + constructor() public { + // solium-disable-previous-line no-empty-blocks + } +} diff --git a/templates/experimental/manifest.json b/templates/experimental/manifest.json new file mode 100644 index 000000000..5a0dc2976 --- /dev/null +++ b/templates/experimental/manifest.json @@ -0,0 +1,3 @@ +{ + "name": "Open Enterprise DEV Template" +} diff --git a/templates/experimental/migrations/.keep b/templates/experimental/migrations/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/templates/experimental/package.json b/templates/experimental/package.json new file mode 100644 index 000000000..bfe753f84 --- /dev/null +++ b/templates/experimental/package.json @@ -0,0 +1,74 @@ +{ + "name": "@autark/templates-open-enterprise-experimental", + "version": "1.0.0", + "description": "Open Enterprise DEV template for Aragon organizations", + "author": "Autark ", + "license": "GPL-3.0-or-later", + "files": [ + "abi/", + "build/", + "contracts/", + "scripts/", + "truffle-config.js", + "test/" + ], + "scripts": { + "prepublishOnly": "npm run compile && npm run abi:extract -- --no-compile", + "abi:extract": "truffle-extract --output abi/ --keys abi", + "compile": "truffle compile", + "lint": "solium --dir ./contracts", + "coverage": "SOLIDITY_COVERAGE=true npm run test", + "test": "npm run test:ganache", + "test:ganache": "../open-enterprise/temp/scripts/test-ganache.sh", + "test:geth": "npm run docker:run && npm run docker:wait-gas && npm run deploy:devnet && truffle test --network devnet && npm run docker:stop", + "docker:run": "cd ../../node_modules/@aragon/templates-shared/; docker-compose -f docker-compose.yml up -d; RESULT=$?; cd -; $(exit $RESULT)", + "docker:stop": "cd ../../node_modules/@aragon/templates-shared/; docker-compose down; cd -", + "docker:wait-gas": "truffle exec ../../node_modules/@aragon/templates-shared/scripts/sleep-until-gaslimit.js --network devnet 6900000", + "deploy:rpc": "npm run compile && truffle exec ./scripts/deploy.js --network rpc", + "deploy:coverage": "npm run compile && truffle exec ./scripts/deploy.js --network coverage", + "deploy:devnet": "npm run compile && truffle exec ./scripts/deploy.js --network devnet", + "deploy:bounties": "cd ../../shared/integrations/StandardBounties && npm run migrate", + "deploy:aragen": "BOUNTIES=$(npm run deploy:bounties | tail -n 1) && npm run compile && truffle exec ./scripts/deploy.js --network rpc --ens 0x5f6f7e8cc7346a11ca2def8f827b7a0b612c56a1 --dao-factory 0x5d94e3e7aec542ab0f9129b9a7badeb5b3ca0f77 --mini-me-factory 0xd526b7aba39cccf76422835e7fd5327b98ad73c9 --r false --s $BOUNTIES", + "deploy:rinkeby": "npm run compile && truffle exec ./scripts/deploy.js --network rinkeby --ens 0x98Df287B6C145399Aaa709692c8D308357bC085D --dao-factory 0xad4d106b43b480faa3ef7f98464ffc27fc1faa96 --mini-me-factory 0x6ffeb4038f7f077c4d20eaf1706980caec31e2bf --standard-bounties 0x38f1886081759f7d352c28984908d04e8d2205a6", + "publish:rpc": "aragon apm publish major $(npm run deploy:rpc | tail -n 1) --environment default", + "publish:devnet": "aragon apm publish major $(npm run deploy:devnet | tail -n 1) --environment default", + "publish:aragen": "aragon apm publish major $(npm run deploy:aragen | tail -n 1) --environment default --files public --skip-confirmation --no-propagate-content --no-prepublish --no-build", + "publish:rinkeby": "aragon apm publish major $(npm run deploy:rinkeby | tail -n 1) --environment rinkeby --files public --no-prepublish --no-build", + "start:template:aragen": "truffle exec ./scripts/create-dao.js --network rpc", + "start:template:rinkeby": "truffle exec ./scripts/create-dao.js --network rinkeby" + }, + "dependencies": { + "@aragon/apps-agent": "^2.0.0", + "@aragon/apps-finance": "^3.0.0", + "@aragon/apps-payroll": "1.0.0", + "@aragon/apps-shared-minime": "1.0.1", + "@aragon/apps-token-manager": "2.1.0", + "@aragon/apps-vault": "4.1.0", + "@aragon/apps-voting": "^2.1.0", + "@aragon/id": "2.0.3", + "@aragon/os": "4.2.0", + "@aragon/templates-shared": "^1.0.0", + "@autarklabs/apps-address-book": "^0.0.1", + "@autarklabs/apps-allocations": "^0.0.1", + "@autarklabs/apps-discussions": "^1.0.0", + "@autarklabs/apps-dot-voting": "^0.0.1", + "@autarklabs/apps-projects": "^0.0.1", + "@autarklabs/apps-rewards": "^0.0.1", + "@autarklabs/apps-token-manager-custom": "^0.0.2", + "@autarklabs/apps-whitelist-oracle": "^0.0.1", + "@autarklabs/aragon-about": "^0.0.1", + "@autarklabs/test-helpers": "^0.0.1" + }, + "devDependencies": { + "@aragon/apps-survey": "1.0.0", + "@aragon/test-helpers": "^2.0.0", + "eth-ens-namehash": "^2.0.8", + "eth-gas-reporter": "0.1.12", + "ganache-cli": "6.1.8", + "solium": "1.1.8", + "truffle": "4.1.14", + "truffle-extract": "^1.2.1", + "truffle-hdwallet-provider": "0.0.3", + "web3-eth-abi": "^1.2.0" + } +} diff --git a/templates/experimental/public/.gitkeep b/templates/experimental/public/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/templates/experimental/scripts/create-dao.js b/templates/experimental/scripts/create-dao.js new file mode 100644 index 000000000..7964938ba --- /dev/null +++ b/templates/experimental/scripts/create-dao.js @@ -0,0 +1,35 @@ +/* global artifacts, web3 */ +const { randomId } = require('@aragon/templates-shared/helpers/aragonId') +const newDAO = require('../temp/scripts/new-dao') + + +const VOTE_DURATION = 60 // seconds +const SUPPORT_REQUIRED = 50e16 // 0 = 0%; 50e16 = 50% +const MIN_ACCEPTANCE_QUORUM = 20e16 // 20e16 = 20% + +// define template params +const settings = { + allocationsPeriod: 0, + // The order is important + dotVotingSettings: [ SUPPORT_REQUIRED, MIN_ACCEPTANCE_QUORUM, VOTE_DURATION ], + financePeriod: 0, + id: randomId(), + members: [ + '0xb4124cEB3451635DAcedd11767f004d8a28c6eE7', + '0x8401Eb5ff34cc943f096A32EF3d5113FEbE8D4Eb', + ], + stakes: [ + '1000000000000000000', + '100000000000000000', + ], + token1: { name: 'Autark Coin', symbol: 'AUT' }, + token2: { name: 'Reputation Coin', symbol: 'REP' }, + useDiscussions: true, + useAgentAsVault: false, + // The order is important for now. TODO: make it an object instead + votingSettings: [ SUPPORT_REQUIRED, MIN_ACCEPTANCE_QUORUM, VOTE_DURATION ], + votingBools: [ false, false ], +} + +// create new dao +module.exports = callback => newDAO({ artifacts, callback, settings, web3 }) diff --git a/templates/experimental/scripts/deploy.js b/templates/experimental/scripts/deploy.js new file mode 100644 index 000000000..8cc884170 --- /dev/null +++ b/templates/experimental/scripts/deploy.js @@ -0,0 +1,18 @@ +/* global artifacts, web3 */ +// const deployTemplate = require('@aragon/templates-shared/scripts/deploy-template') +const deployTemplate = require('../temp/scripts/deploy-template') + +const TEMPLATE_NAME = 'open-enterprise-template' +const CONTRACT_NAME = 'OpenEnterpriseTemplate' + +module.exports = callback => { + deployTemplate(web3, artifacts, TEMPLATE_NAME, CONTRACT_NAME) + .then(template => { + console.log( + 'Open Enterprise template deployed at address\n', + template.address + ) + callback() + }) + .catch(callback) +} diff --git a/templates/experimental/scripts/recover-dao.js b/templates/experimental/scripts/recover-dao.js new file mode 100644 index 000000000..7c4f15093 --- /dev/null +++ b/templates/experimental/scripts/recover-dao.js @@ -0,0 +1,114 @@ +/* global web3 */ +const fs = require('fs') +const exec = require('child_process').exec +const rl = require('readline').createInterface({ + input: process.stdin, + output: process.stdout +}) +const OpenEnterpriseTemplate = fs.readFileSync('../build/contracts/OpenEnterpriseTemplate.json') + +const zeroAddress = '0x00000000000000000000000000000000000000000' +const PCT_BASE = 1e18 + +let deployedInfo = null + +// This function queries APM for the contract address of the Open Enterprise template +// The script will not exit until this address is resolve or the request fails or times out +exec('aragon apm info open-enterprise-template --env production', (err, stdout, stderr) => { + if (err) { + console.error(err, ' error getting template address: ', stderr) + return + } + deployedInfo = JSON.parse(getJson(stdout)) +}) + +module.exports = async function getSecondDaoTxData(callback) { + // This function will generate a data string that the user can send in a tx to our deployments contract + const oeAbi = JSON.parse(OpenEnterpriseTemplate).abi + const TemplateContract = web3.eth.contract(oeAbi).at(zeroAddress) + const voteDuration = await getVoteDuration() + const minSupport = await getMinSupport() + const quorum = await getQuorum(minSupport) + const allocationsPeriod = await getAllocationsPeriod() + const dataString = TemplateContract.newOpenEnterprise.getData( + [ minSupport, quorum, voteDuration ], + allocationsPeriod, + false + ) + rl.write('generating information...') + while (!deployedInfo) { + await waitASecond() + rl.write('.') + } + rl.write(`\nHere's your data string:\n${dataString}\n\n`) + rl.write(`submit a transaction with the data string above to ${deployedInfo.contractAddress} in MyCrypto or MEW\n`) + + callback() +} + +const waitASecond = () => { + return new Promise((resolve) => { + setTimeout(resolve, 1000) + }) +} +const getVoteDuration = () => { + return new Promise((resolve) => { + rl.question('enter the desired dot voting period length, in seconds > ', async (answer) => { + if (answer <= 0) { + rl.write('Error: Vote Duration must be greater than zero\n') + resolve(await getVoteDuration()) + } + resolve(answer) + }) + }) +} + +const getMinSupport = () => { + return new Promise((resolve) => { + rl.question('enter the dot voting minimum candidate support percentage > ', async (answer) => { + if ((answer * 1e16) > PCT_BASE) { + rl.write('Error: Minimum Support Percentage must be less than 100%\n') + resolve(await getMinSupport()) + } else if (answer <= 0) { + rl.write('Error: Minimum Support Percentage must be greater than zero\n') + resolve(await getMinSupport()) + } + resolve(answer * 1e16) + }) + }) +} + +const getQuorum = (minSupport) => { + return new Promise((resolve) => { + rl.question('enter the dot voting quorum percentage > ', async (answer) => { + if ((answer * 1e16) < minSupport) { + rl.write('Error: Quorum must be greater than or equal to minimum candidate support percentage\n') + resolve(await getQuorum(minSupport)) + } else if ((answer * 1e16) > PCT_BASE) { + rl.write('Error: Quorum must be less than 100%\n') + resolve(await getQuorum(minSupport)) + } + resolve(answer * 1e16) + }) + }) +} + +const getAllocationsPeriod = () => { + return new Promise((resolve) => { + rl.question('enter the allocations period duration, in days > ', async (answer) => { + if (answer < 1) { + rl.write('Error: Allocations budgeting period must be one day or longer\n') + resolve(await getAllocationsPeriod()) + } + resolve(answer * 24 * 60 * 60) + }) + }) +} + +const getJson = (rawOutput) => { + // This helper function cleans up the string returned from the APM query + // and makes it convertible into JSON + const sanitizedOutput = rawOutput.replace(/\n/g, '') + const startIdx = sanitizedOutput.indexOf('{') + return sanitizedOutput.substring(startIdx) +} diff --git a/templates/experimental/temp/helpers/apps.js b/templates/experimental/temp/helpers/apps.js new file mode 100644 index 000000000..829a698f5 --- /dev/null +++ b/templates/experimental/temp/helpers/apps.js @@ -0,0 +1,39 @@ +const { hash: namehash } = require('eth-ens-namehash') + +const ARAGON_APPS = [ + { name: 'agent', contractName: 'Agent' }, + { name: 'vault', contractName: 'Vault' }, + { name: 'voting', contractName: 'Voting' }, + // { name: 'survey', contractName: 'Survey' }, + // { name: 'payroll', contractName: 'Payroll' }, + { name: 'finance', contractName: 'Finance' }, + //{ name: 'token-manager-custom', contractName: 'TokenManager' }, + //{ name: 'whitelist-oracle', contractName: 'WhitelistOracle' }, + { name: 'token-manager.hatch', contractName: 'TokenManager' }, + { name: 'whitelist-oracle.hatch', contractName: 'WhitelistOracle' }, +] + +const ARAGON_APP_IDS = ARAGON_APPS.reduce((ids, { name }) => { + ids[name] = namehash(`${name}.aragonpm.eth`) + return ids +}, {}) + +const OE_APPS = [ + { name: 'about.hatch', contractName: 'About' }, + { name: 'address-book-experimental.open', contractName: 'AddressBook' }, + { name: 'allocations-experimental.open', contractName: 'Allocations' }, + { name: 'discussions-experimental.open', contractName: 'DiscussionApp' }, + { name: 'dot-voting-experimental.open', contractName: 'DotVoting' }, + { name: 'projects-experimental.open', contractName: 'Projects' }, + { name: 'rewards-experimental.open', contractName: 'Rewards' }, +] + +const OE_APP_IDS = OE_APPS.reduce((ids, { name }) => { + ids[name] = namehash(`${name}.aragonpm.eth`) + return ids +}, {}) + +module.exports = { + APPS: [ ...ARAGON_APPS, ...OE_APPS ], + APP_IDS: { ...ARAGON_APP_IDS, ...OE_APP_IDS } +} diff --git a/templates/experimental/temp/helpers/events.js b/templates/experimental/temp/helpers/events.js new file mode 100644 index 000000000..8f0ad0561 --- /dev/null +++ b/templates/experimental/temp/helpers/events.js @@ -0,0 +1,36 @@ +const abi = require('web3-eth-abi') +const { APP_IDS } = require('./apps') + +module.exports = artifacts => { + function decodeEvents({ receipt }, contractAbi, eventName) { + const eventAbi = contractAbi.filter(abi => abi.name === eventName && abi.type === 'event')[0] + const eventSignature = abi.encodeEventSignature(eventAbi) + const eventLogs = receipt.logs.filter(l => l.topics[0] === eventSignature) + return eventLogs.map(log => { + log.event = eventAbi.name + log.args = abi.decodeLog(eventAbi.inputs, log.data, log.topics.slice(1)) + return log + }) + } + + function getInstalledApps(receipt, appId) { + const Kernel = artifacts.require('Kernel') + const events = decodeEvents(receipt, Kernel.abi, 'NewAppProxy') + const appEvents = events.filter(e => e.args.appId === appId) + const installedAddresses = appEvents.map(e => e.args.proxy) + return installedAddresses + } + + function getInstalledAppsById(receipt) { + return Object.keys(APP_IDS).reduce((apps, appName) => { + apps[appName] = getInstalledApps(receipt, APP_IDS[appName]) + return apps + }, {}) + } + + return { + decodeEvents, + getInstalledApps, + getInstalledAppsById, + } +} diff --git a/templates/experimental/temp/lib/OEDeployer.js b/templates/experimental/temp/lib/OEDeployer.js new file mode 100644 index 000000000..b641cf6ab --- /dev/null +++ b/templates/experimental/temp/lib/OEDeployer.js @@ -0,0 +1,47 @@ +const logDeploy = require('@aragon/os/scripts/helpers/deploy-logger') +// const deployStandardBounties = require('../scripts/deploy-standardBounties') +const TemplateDeployer = require('./TemplatesDeployer') + +module.exports = class OEDeployer extends TemplateDeployer { + constructor(web3, artifacts, owner, options = { verbose: false }) { + super(web3, artifacts, owner, options) + } + + async deploy(templateName, contractName) { + // console.log('deployed!', this.options) + await this._fetchOrDeployStandardBounties() + await this.fetchOrDeployDependencies() + const template = await this.deployTemplate(contractName) + await this.registerDeploy(templateName, template) + return template + } + + async deployTemplate(contractName) { + const Template = this.artifacts.require(contractName) + const template = await Template.new([ this.daoFactory.address, this.ens.address, this.miniMeFactory.address, this.aragonID.address, this.standardBounties.address ]) + await logDeploy(template) + return template + } + + async _fetchOrDeployStandardBounties() { + // const standardBounties = this.artifacts.require('StandardBounties') + if (this.options.standardBounties) { + this.log(`Using provided StandardBounties: ${this.options.standardBounties}`) + // this.standardBounties = standardBounties.at(this.options.standardBounties) + this.standardBounties = { address: this.options.standardBounties } + // TODO: unimplemented adding the address to arapp.json for now + // } else if (await this.arappStandardBounties()) { + // const standardBountiesAddress = await this.arappStandardBounties() + // this.log(`Using StandardBounties from arapp json file: ${standardBountiesAddress}`) + // this.standardBounties = standardBounties.at(standardBountiesAddress) + // TODO: The only option available for now is to provide the standardbounties address in the option + // } else if (await this.isLocal()) { + // const { standardBounties } = await deployStandardBounties(null, { web3: this.web3, artifacts: this.artifacts, owner: this.owner, verbose: this.verbose }) + // this.standardBounties = standardBounties + } else { + this.error('Please provide a StandardBounties instance, aborting.') + } + } + + +} diff --git a/templates/experimental/temp/lib/TemplatesDeployer.js b/templates/experimental/temp/lib/TemplatesDeployer.js new file mode 100644 index 000000000..1bd76ace5 --- /dev/null +++ b/templates/experimental/temp/lib/TemplatesDeployer.js @@ -0,0 +1,216 @@ +const { hash: namehash } = require('eth-ens-namehash') + +const logDeploy = require('@aragon/os/scripts/helpers/deploy-logger') +const deployAPM = require('@aragon/os/scripts/deploy-apm') +const deployENS = require('@aragon/os/scripts/deploy-test-ens') +const deployAragonID = require('@aragon/id/scripts/deploy-beta-aragonid') +const deployDAOFactory = require('@aragon/os/scripts/deploy-daofactory') + +module.exports = class TemplateDeployer { + constructor(web3, artifacts, owner, options = { verbose: false }) { + this.web3 = web3 + this.artifacts = artifacts + this.owner = owner + this.options = { ...options } + } + + async deploy(templateName, contractName) { + await this.fetchOrDeployDependencies() + const template = await this.deployTemplate(contractName) + await this.registerDeploy(templateName, template) + return template + } + + async deployTemplate(contractName) { + const Template = this.artifacts.require(contractName) + const template = await Template.new(this.daoFactory.address, this.ens.address, this.miniMeFactory.address, this.aragonID.address) + await logDeploy(template) + return template + } + + async fetchOrDeployDependencies() { + await this._fetchOrDeployENS() + await this._fetchOrDeployAPM() + await this._fetchOrDeployAragonID() + await this._fetchOrDeployDAOFactory() + await this._fetchOrDeployMiniMeFactory() + await this._checkAppsDeployment() + } + + async registerDeploy(templateName, template) { + if ((await this.isLocal()) && !(await this._isPackageRegistered(templateName))) { + await this._registerPackage(templateName, template) + } + await this._writeArappFile(templateName, template) + } + + async _checkAppsDeployment() { + for (const { name, contractName } of this.options.apps) { + if (await this._isPackageRegistered(name)) { + this.log(`Using registered ${name} app`) + } else if (await this.isLocal()) { + await this._registerApp(name, contractName) + } else { + this.log(`No ${name} app registered`) + } + } + } + + async _fetchOrDeployENS() { + const ENS = this.artifacts.require('ENS') + if (this.options.ens) { + this.log(`Using provided ENS: ${this.options.ens}`) + this.ens = ENS.at(this.options.ens) + } else if (await this.arappENS()) { + const ensAddress = await this.arappENS() + this.log(`Using ENS from arapp json file: ${ensAddress}`) + this.ens = ENS.at(ensAddress) + } else if (await this.isLocal()) { + const { ens } = await deployENS(null, { web3: this.web3, artifacts: this.artifacts, owner: this.owner, verbose: this.verbose }) + this.log('Deployed ENS:', ens.address) + this.ens = ens + } else { + this.error('Please provide an ENS instance, aborting.') + } + } + + async _fetchOrDeployAPM() { + const APM = this.artifacts.require('APMRegistry') + if (this.options.apm) { + this.log(`Using provided APM: ${this.options.apm}`) + this.apm = APM.at(this.options.apm) + } else { + if (await this._isAPMRegistered()) { + const apmAddress = await this._fetchRegisteredAPM() + this.log(`Using APM registered at aragonpm.eth: ${apmAddress}`) + this.apm = APM.at(apmAddress) + } else if (await this.isLocal()) { + await deployAPM(null, { artifacts: this.artifacts, web3: this.web3, owner: this.owner, ensAddress: this.ens.address, verbose: this.verbose }) + const apmAddress = await this._fetchRegisteredAPM() + if (!apmAddress) this.error('Local APM deployment failed, aborting.') + this.log('Deployed APM:', apmAddress) + this.apm = APM.at(apmAddress) + } else { + this.error('Please provide an APM instance or make sure there is one registered under "aragonpm.eth", aborting.') + } + } + } + + async _fetchOrDeployAragonID() { + const FIFSResolvingRegistrar = this.artifacts.require('FIFSResolvingRegistrar') + if (this.options.aragonID) { + this.log(`Using provided aragonID: ${this.options.aragonID}`) + this.aragonID = FIFSResolvingRegistrar.at(this.options.aragonID) + } else { + if (await this._isAragonIdRegistered()) { + const aragonIDAddress = await this._fetchRegisteredAragonID() + this.log(`Using aragonID registered at aragonid.eth: ${aragonIDAddress}`) + this.aragonID = FIFSResolvingRegistrar.at(aragonIDAddress) + } else if (await this.isLocal()) { + await deployAragonID(null, { artifacts: this.artifacts, web3: this.web3, owner: this.owner, ensAddress: this.ens.address, verbose: this.verbose }) + const aragonIDAddress = await this._fetchRegisteredAragonID() + if (!aragonIDAddress) this.error('Local aragon ID deployment failed, aborting.') + this.log('Deployed aragonID:', aragonIDAddress) + this.aragonID = FIFSResolvingRegistrar.at(aragonIDAddress) + } else { + this.error('Please provide an aragon ID instance or make sure there is one registered under "aragonid.eth", aborting.') + } + } + } + + async _fetchOrDeployDAOFactory() { + const DAOFactory = this.artifacts.require('DAOFactory') + if (this.options.daoFactory) { + this.log(`Using provided DAOFactory: ${this.options.daoFactory}`) + this.daoFactory = DAOFactory.at(this.options.daoFactory) + } else { + const { daoFactory } = await deployDAOFactory(null, { artifacts: this.artifacts, owner: this.owner, verbose: this.verbose }) + this.log('Deployed DAOFactory:', daoFactory.address) + this.daoFactory = daoFactory + } + } + + async _fetchOrDeployMiniMeFactory() { + const MiniMeTokenFactory = this.artifacts.require('MiniMeTokenFactory') + if (this.options.miniMeFactory) { + this.log(`Using provided MiniMeTokenFactory: ${this.options.miniMeFactory}`) + this.miniMeFactory = MiniMeTokenFactory.at(this.options.miniMeFactory) + } else { + this.miniMeFactory = await MiniMeTokenFactory.new() + this.log('Deployed MiniMeTokenFactory:', this.miniMeFactory.address) + } + } + + async _fetchRegisteredAPM() { + const aragonPMHash = namehash('aragonpm.eth') + const PublicResolver = this.artifacts.require('PublicResolver') + const resolver = PublicResolver.at(await this.ens.resolver(aragonPMHash)) + return resolver.addr(aragonPMHash) + } + + async _fetchRegisteredAragonID() { + const aragonIDHash = namehash('aragonid.eth') + return this.ens.owner(aragonIDHash) + } + + async _registerApp(name, contractName) { + const app = await this.artifacts.require(contractName).new() + return this._registerPackage(name, app) + } + + async _registerPackage(name, instance) { + if (this.options.register) { + this.log(`Registering package for ${instance.constructor.contractName} as "${name}.aragonpm.eth"`) + return this.apm.newRepoWithVersion(name, this.owner, [ 1, 0, 0 ], instance.address, '') + } + } + + async _isAPMRegistered() { + return this._isRepoRegistered(namehash('aragonpm.eth')) + } + + async _isAragonIdRegistered() { + return this._isRepoRegistered(namehash('aragonid.eth')) + } + + async _isPackageRegistered(name) { + return this._isRepoRegistered(namehash(`${name}.aragonpm.eth`)) + } + + async _isRepoRegistered(hash) { + const owner = await this.ens.owner(hash) + return owner !== '0x0000000000000000000000000000000000000000' && owner !== '0x' + } + + async _writeArappFile(templateName, template) { + const { constructor: { contractName } } = template + await this.arapp.write(templateName, contractName, this.ens.address) + this.log(`Template addresses saved to ${await this.arapp.filePath()}`) + } + + async arappENS() { + const environment = await this.arapp.getDeployedData() + return environment.registry + } + + async isLocal() { + const { isLocalNetwork } = require('./network')(this.web3) + return isLocalNetwork() + } + + get arapp() { + return require('./arapp-file')(this.web3) + } + + get verbose() { + return this.options.verbose + } + + log(...args) { + if (this.verbose) console.log(...args) + } + + error(message) { + throw new Error(message) + } +} diff --git a/templates/experimental/temp/lib/arapp-file.js b/templates/experimental/temp/lib/arapp-file.js new file mode 100644 index 000000000..146adf21a --- /dev/null +++ b/templates/experimental/temp/lib/arapp-file.js @@ -0,0 +1,49 @@ +const fs = require('fs') +const path = require('path') + +const FILE_NAME = 'arapp.json' +const LOCAL_FILE_NAME = 'arapp_local.json' +const DEFAULT_ARAPP_FILE = { environments: {} } + +module.exports = web3 => { + const { isLocalNetwork, getNetworkName } = require('./network')(web3) + + async function arappFileName() { + return (await isLocalNetwork()) ? LOCAL_FILE_NAME : FILE_NAME + } + + async function arappFilePath() { + return path.resolve(await arappFileName()) + } + + async function read() { + const filePath = await arappFilePath() + const file = fs.existsSync(filePath) ? require(filePath) : DEFAULT_ARAPP_FILE + if (!file.environments) file.environments = {} + return file + } + + async function getDeployedData() { + const network = await getNetworkName() + const file = await read() + return file.environments[network] || {} + } + + async function write(appName, contractName, registry) { + const network = await getNetworkName() + const data = await read() + data.path = `contracts/${contractName}.sol` + if (data.environments === undefined) data.environments = {} + const wsRPC = `wss://${network}.eth.aragon.network/ws` + data.environments[network] = { appName: `${appName}.aragonpm.eth`, network, registry, wsRPC } + fs.writeFileSync(await arappFilePath(), JSON.stringify(data, null, 2)) + } + + return { + read, + write, + getDeployedData, + fileName: arappFileName, + filePath: arappFilePath, + } +} diff --git a/templates/experimental/temp/lib/ens.js b/templates/experimental/temp/lib/ens.js new file mode 100644 index 000000000..738152e2d --- /dev/null +++ b/templates/experimental/temp/lib/ens.js @@ -0,0 +1,31 @@ +const { hash: namehash } = require('eth-ens-namehash') + +module.exports = (web3, artifacts) => { + const { getDeployedData } = require('./arapp-file')(web3) + + const getENS = async () => { + const { registry } = await getDeployedData() + return artifacts.require('ENS').at(registry) + } + + const getAPM = async () => { + const ens = await getENS() + const apmAddress = await ens.resolver(namehash('aragonpm.eth')) + return artifacts.require('PublicResolver').at(apmAddress) + } + + const getTemplateAddress = async () => { + const apm = await getAPM() + const { appName } = await getDeployedData() + + const repoAddress = await apm.addr(namehash(appName)) + const repo = artifacts.require('Repo').at(repoAddress) + return (await repo.getLatest())[1] + } + + return { + getENS, + getAPM, + getTemplateAddress + } +} diff --git a/templates/experimental/temp/lib/network.js b/templates/experimental/temp/lib/network.js new file mode 100644 index 000000000..0945fb9d4 --- /dev/null +++ b/templates/experimental/temp/lib/network.js @@ -0,0 +1,29 @@ +const DEFAULT_NETWORK = 'devnet' +const LOCAL_NETWORKS = ['devnet', 'rpc'] + +module.exports = web3 => { + + async function getNetworkId() { + return new Promise((resolve, reject) => + web3.version.getNetwork((error, result) => error ? reject(error) : resolve(result)) + ) + } + + async function getNetworkName() { + const id = await getNetworkId() + const { networks } = require('@aragon/os/truffle-config') + const networkName = Object.keys(networks).find(name => networks[name].network_id == id) + return networkName || DEFAULT_NETWORK + } + + async function isLocalNetwork() { + const networkName = await getNetworkName() + return LOCAL_NETWORKS.includes(networkName) + } + + return { + getNetworkId, + getNetworkName, + isLocalNetwork + } +} diff --git a/templates/experimental/temp/scripts/deploy-standardBounties.js b/templates/experimental/temp/scripts/deploy-standardBounties.js new file mode 100644 index 000000000..31a331b94 --- /dev/null +++ b/templates/experimental/temp/scripts/deploy-standardBounties.js @@ -0,0 +1,44 @@ +const logDeploy = require('@aragon/os/scripts/helpers/deploy-logger') +const getAccounts = require('@aragon/os/scripts/helpers/get-accounts') + +const globalArtifacts = this.artifacts // Not injected unless called directly via truffle +const globalWeb3 = this.web3 // Not injected unless called directly via truffle + +const defaultOwner = process.env.OWNER + +module.exports = async ( + truffleExecCallback, + { + artifacts = globalArtifacts, + web3 = globalWeb3, + owner = defaultOwner, + verbose = true + } = {} +) => { + const log = (...args) => { + if (verbose) { console.log(...args) } + } + + if (!owner) { + const accounts = await getAccounts(web3) + owner = accounts[0] + log(`No OWNER environment variable passed, setting StandardBounties owner to provider's account: ${owner}`) + } + + // TODO: we do this externally for now, this script is not called right now + // const standardBounties = artifacts.require('StandardBounties') + // log('Deploying StandardBounties...') + // const standardBountiesBase = await standardBounties.new(owner) + // await logDeploy(standardBountiesBase, { verbose }) + + if (typeof truffleExecCallback === 'function') { + // Called directly via `truffle exec` + truffleExecCallback() + } else { + return { + // standardBounties: standardBountiesBase + standardBounties: { address: 0 } + } + } +} + diff --git a/templates/experimental/temp/scripts/deploy-template.js b/templates/experimental/temp/scripts/deploy-template.js new file mode 100644 index 000000000..fd2f679ac --- /dev/null +++ b/templates/experimental/temp/scripts/deploy-template.js @@ -0,0 +1,31 @@ +const { APPS } = require('../helpers/apps') +const getAccounts = require('@aragon/os/scripts/helpers/get-accounts') +const TemplatesDeployer = require('../lib/OEDeployer') + +const errorOut = message => { + console.error(message) + throw new Error(message) +} + +module.exports = async function deployTemplate(web3, artifacts, templateName, contractName, apps = APPS) { + let { ens, owner, verbose, daoFactory, miniMeFactory, standardBounties, register } = require('yargs') + .option('e', { alias: 'ens', describe: 'ENS address', type: 'string' }) + .option('o', { alias: 'owner', describe: 'Sender address. Will use first address if no one is given.', type: 'string' }) + .option('v', { alias: 'verbose', describe: 'Verbose mode', type: 'boolean', default: true }) + .option('df', { alias: 'dao-factory', describe: 'DAO Factory address. Will deploy new instance if not given.', type: 'string' }) + .option('mf', { alias: 'mini-me-factory', describe: 'MiniMe Factory address. Will deploy new instance if not given.', type: 'string' }) + .option('s', { alias: 'standard-bounties', describe: 'StandardBounties address. Will deploy new instance if not given', type: 'string' }) + .option('r', { alias: 'register', describe: 'Whether the script will register the packages to aragon', type: 'boolean', default: true }) + .help('help') + .parse() + + if (!web3) errorOut('Missing "web3" object. This script must be run with a "web3" object globally defined, for example through "truffle exec".') + if (!artifacts) errorOut('Missing "artifacts" object. This script must be run with an "artifacts" object globally defined, for example through "truffle exec".') + if (!owner) owner = (await getAccounts(web3))[0] + if (!owner) errorOut('Missing sender address. Please specify one using "--owner" or make sure your web3 instance has one loaded.') + if (!templateName) errorOut('Missing template id.') + if (!contractName) errorOut('Missing template contract name.') + + const deployer = new TemplatesDeployer(web3, artifacts, owner, { apps, ens, verbose, daoFactory, miniMeFactory, standardBounties, register }) + return deployer.deploy(templateName, contractName) +} diff --git a/templates/experimental/temp/scripts/new-dao.js b/templates/experimental/temp/scripts/new-dao.js new file mode 100644 index 000000000..90d5cefd1 --- /dev/null +++ b/templates/experimental/temp/scripts/new-dao.js @@ -0,0 +1,73 @@ +module.exports = async function newDao({ + artifacts, + callback, + settings, + web3, +}) { + const { + allocationsPeriod, + dotVotingSettings, + financePeriod, + id, + members, + stakes, + token1, + token2, + useDiscussions, + votingSettings, + votingBools, + useAgentAsVault, + } = settings + console.log('settings:', settings) + const { getEventArgument } = require('@aragon/test-helpers/events') + const { getTemplateAddress } = require('../lib/ens')(web3, artifacts) + const OpenEnterpriseTemplate = artifacts.require('OpenEnterpriseTemplate') + const Kernel = artifacts.require('Kernel') + + try { + const template = OpenEnterpriseTemplate.at(await getTemplateAddress()) + + const baseDAO = await template.newTokensAndInstance( + id, + token1.name, + token1.symbol, + token2.name, + token2.symbol, + [ ...dotVotingSettings, ...votingSettings ], + votingBools, + useAgentAsVault, + { from: members[0] } + ) + + const dao = Kernel.at(getEventArgument(baseDAO, 'DeployDao', 'dao')) + + + const baseDaoWithTokenMgrs = await template.newTokenManagers( + members, + stakes, + members, + stakes, + [ false, true, false, false ], + { from: members[0] } + ) + + const baseOpenEnterprise = await template.finalizeDao( + [ 0, 0 ], + useDiscussions, + { from: members[0] } + ) + + console.log('🤓 Template found at:', template.address) + console.log( + '🚀 Created new dao at address:', + dao.address, + '\n⛽️ Total gas cost for creation:', + baseDAO.receipt.gasUsed + baseDaoWithTokenMgrs.receipt.gasUsed + baseOpenEnterprise.receipt.gasUsed, 'gas', + '\n🌐 You can access it at:', + `http://localhost:8080/ipfs/QmVptozeYf3XxqHfvMjofCkZsYSqi6YuvHFLMc83SECbNw/#/${id}` + ) + callback() + } catch (e) { + callback(e) + } +} diff --git a/templates/experimental/temp/scripts/test-ganache.sh b/templates/experimental/temp/scripts/test-ganache.sh new file mode 100755 index 000000000..1728a6673 --- /dev/null +++ b/templates/experimental/temp/scripts/test-ganache.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# Exit script as soon as a command fails. +set -o errexit + +# Executes cleanup function at script exit. +trap cleanup EXIT + +cleanup() { + # Kill the RPC instance that we started (if we started one and if it's still running). + if [ -n "$pid" ] && ps -p $pid > /dev/null; then + kill -9 $pid + fi + + # Remove local deploy file in case it was created + clean_deploy +} + +setup_coverage_variables() { + ACCOUNTS=${ACCOUNTS-200} + BALANCE=${BALANCE-100000} + COMMAND=${COMMAND-testrpc-sc} + ENVIRONMENT=${ENVIRONMENT-coverage} + GAS_LIMIT=${GAS_LIMIT-0xfffffffffff} + MAIN_TASK=${MAIN_TASK-run_coverage} + NETWORK_ID=${NETWORK_ID-16} + PORT=${PORT-8555} +} + +setup_testing_variables() { + ACCOUNTS=${ACCOUNTS-200} + BALANCE=${BALANCE-100000} + COMMAND=${COMMAND-ganache-cli} + ENVIRONMENT=${ENVIRONMENT-rpc} + GAS_LIMIT=${GAS_LIMIT-8000000} + MAIN_TASK=${MAIN_TASK-run_tests} + NETWORK_ID=${NETWORK_ID-15} + PORT=${PORT-8545} +} + +start_chain() { + echo "Starting ${COMMAND}..." + ${COMMAND} -i ${NETWORK_ID} -l ${GAS_LIMIT} -a ${ACCOUNTS} -e ${BALANCE} -p ${PORT} > /dev/null & + pid=$! + sleep 3 # give time to init chain + echo "Running ganache-cli with pid ${pid} in port ${PORT}, gas limit set to: ${GAS_LIMIT}" +} + +clean_deploy() { + rm -f arapp_local.json +} + +run_tests() { + echo "Running tests $@..." + truffle test --network rpc $@ +} + +run_coverage() { + echo "Measuring coverage $@..." + # TODO: rimraf or crossenv + # It is needed to remove this folder to prevent wrong instrumentation + # I didn't find a way to tell solidity-coverage to ignore it + rm -rf flattened_contracts + solidity-coverage $@ +} + +[[ "$SOLIDITY_COVERAGE" = true ]] && setup_coverage_variables || setup_testing_variables +start_chain +${MAIN_TASK} $@ \ No newline at end of file diff --git a/templates/experimental/test/OpenEnterpriseTemplate.test.js b/templates/experimental/test/OpenEnterpriseTemplate.test.js new file mode 100644 index 000000000..6258d10a8 --- /dev/null +++ b/templates/experimental/test/OpenEnterpriseTemplate.test.js @@ -0,0 +1,334 @@ +/* eslint-disable no-console */ +/* global artifacts, assert, before, context, contract, it */ + +const { randomId } = require('@aragon/templates-shared/helpers/aragonId') +const truffleAssert = require('truffle-assertions') + +const { APPS, APP_IDS } = require('../temp/helpers/apps') + +const namehash = require('eth-ens-namehash').hash +const promisify = require('util').promisify +const exec = promisify(require('child_process').exec) +const keccak256 = require('js-sha3').keccak_256 + +/** Helper function to import truffle contract artifacts */ +const getContract = name => artifacts.require(name) + +/** Helper function to read events from receipts */ +const getReceipt = (receipt, event, arg) => { + const result = receipt.logs.filter(l => l.event === event)[0].args + return arg ? result[arg] : result +} + +const getReceipts = (receipt, event, arg) => { + const results = receipt.logs.filter(l => l.event === event) + return arg ? results.map(e => e.args[arg]) : results +} + +const getTokenManagers = receipt => { + const tokenManagerAppId = namehash('token-manager.hatch.aragonpm.eth') + return receipt.logs + .filter(e => e.event === 'InstalledApp' && e.args.appId === tokenManagerAppId) + .map(e => e.args.appProxy) +} + +const getTokenFromTokenManager = async tokenManagerAddr => { + const tokenManager = getContract('TokenManager').at(tokenManagerAddr) + const tokenAddr = await tokenManager.token() + const token = getContract('MiniMeToken').at(tokenAddr) + return token +} + +/** Helper path functions to allow executing the script from relative or root location */ +const parentDir = () => { + const pwd = process.cwd().split('/') + return pwd[pwd.length - 2] +} + +const bountiesPath = `${ + process.env.SOLIDITY_COVERAGE + ? '../../../' + : parentDir() === 'templates' + ? '../../' + : '' +}shared/integrations/StandardBounties` + + +const ONE_DAY = 60 * 60 * 24 +const ONE_WEEK = ONE_DAY * 7 +const THIRTY_DAYS = ONE_DAY * 30 + +contract('OpenEnterpriseTemplate', ([ owner, member1, member2, member3 ]) => { + const TOKEN1_NAME = 'DaoToken1' + const TOKEN1_SYMBOL = 'DT1' + const TOKEN2_NAME = 'DaoToken2' + const TOKEN2_SYMBOL = 'DT2' + const VOTING_BOOLS = [ false, false ] + const TOKEN_TRANSFERABLE = false + const TOKENS_LIMIT = true + const TOKEN_HOLDERS = [ member1, member2, member3 ] + const TOKEN_STAKES = [ 100, 200, 500 ] + const TOKEN_BOOLS = [ TOKENS_LIMIT, TOKEN_TRANSFERABLE, TOKENS_LIMIT, TOKEN_TRANSFERABLE ] + + const VOTE_DURATION = ONE_WEEK + const SUPPORT_REQUIRED = 50e16 + const MIN_ACCEPTANCE_QUORUM = 20e16 + const A1_VOTING_SETTINGS = [ SUPPORT_REQUIRED, MIN_ACCEPTANCE_QUORUM, VOTE_DURATION ] + const DOT_VOTING_SETTINGS = [ SUPPORT_REQUIRED, MIN_ACCEPTANCE_QUORUM, VOTE_DURATION ] + const VOTING_SETTINGS = [ ...DOT_VOTING_SETTINGS, ...A1_VOTING_SETTINGS ] + const FINANCE_PERIOD = THIRTY_DAYS + + let template + + before('deploy required contract dependencies', async () => { + // TODO: Make verbosity optional + const bountiesAddress = + (await exec( + `cd ${bountiesPath} && npm run migrate${ + process.env.SOLIDITY_COVERAGE ? ':coverage' : '' + } | tail -n 1` + )).stdout.trim() + if (!bountiesAddress) { + throw new Error('StandardBounties deployment failed, the test cannot continue') + } + console.log(' Deployed StandardBounties at', bountiesAddress) + + // Deploy ENS + const ensFactBase = await getContract('ENSFactory').new() + const ensReceipt = await ensFactBase.newENS(owner) + const ens = getContract('ENS').at(getReceipt(ensReceipt, 'DeployENS', 'ens')) + console.log(' Deployed ENS at', ens.address) + + // Deploy DaoFactory + const kernelBase = await getContract('Kernel').new(true) // petrify immediately + const aclBase = await getContract('ACL').new() + const regFactBase = await getContract('EVMScriptRegistryFactory').new() + const daoFactBase = await getContract('DAOFactory').new( + kernelBase.address, + aclBase.address, + regFactBase.address + ) + console.log(' Deployed DAOFactory at', daoFactBase.address) + + // Deploy MiniMeTokenFactory + const miniMeFactoryBase = await getContract('MiniMeTokenFactory').new() + console.log(' Deployed MiniMeTokenFactory at', miniMeFactoryBase.address) + + // Deploy AragonID + const publicResolver = await ens.resolver(namehash('resolver.eth')) + const tld = namehash('eth') + const label = '0x'+keccak256('aragonid') + const node = namehash('aragonid.eth') + const aragonID = await getContract('FIFSResolvingRegistrar').new(ens.address, publicResolver, node) + console.log(' Deployed AragonID at', aragonID.address) + // TODO: Configure AragonID + // await ens.setOwner(node, aragonID.address) + await ens.setSubnodeOwner(tld, label, aragonID.address) + // await aragonID.register('0x'+keccak256('owner'), owner) + + // Deploy APM + const tldName = 'eth' + const labelName = 'aragonpm' + const tldHash = namehash(tldName) + const labelHash = '0x'+keccak256(labelName) + // Deploy Hatch + const hatchTldName = 'aragonpm.eth' + const hatchTldHash = namehash(hatchTldName) + const hatchLabelName = 'hatch' + const hatchLabelHash = '0x'+keccak256(hatchLabelName) + // Deploy Open + const openTldName = 'aragonpm.eth' + const openTldHash = namehash(openTldName) + const openLabelName = 'open' + const openLabelHash = '0x'+keccak256(openLabelName) + // const apmNode = namehash(`${labelName}.${tldName}`) + + const apmRegistryBase = await getContract('APMRegistry').new() + const apmRepoBase = await getContract('Repo').new() + const ensSubdomainRegistrarBase = await getContract('ENSSubdomainRegistrar').new() + const apmFactory = await getContract('APMRegistryFactory').new( + daoFactBase.address, + apmRegistryBase.address, + apmRepoBase.address, + ensSubdomainRegistrarBase.address, + ens.address, + ensFactBase.address + ) + // Assign ENS name (${labelName}.${tldName}) to factory... + // await ens.setOwner(apmNode, apmFactory.address) + // Create subdomains and assigning it to APMRegistryFactory + // assign `aragonpm.eth` to ourselves first so we can assign `hatch.aragonpm.eth` + // This is a workaround to setting up a dao around the `aragonpm.eth` namespace + await ens.setSubnodeOwner(tldHash, labelHash, owner) + // assign `hatch.aragonpm.eth` to apmFactory + await ens.setSubnodeOwner(hatchTldHash, hatchLabelHash, apmFactory.address) + // assign 'open.aragonpm.eth' to apmFactory + await ens.setSubnodeOwner(openTldHash, openLabelHash, apmFactory.address) + // transfer `aragonpm.eth` to apmFactory + await ens.setSubnodeOwner(tldHash, labelHash, apmFactory.address) + // TODO: Transferring name ownership from deployer to APMRegistryFactory + const apmReceipt = await apmFactory.newAPM(tldHash, labelHash, owner) + const hatchApmReceipt = await apmFactory.newAPM(hatchTldHash, hatchLabelHash, owner) + const openApmReceipt = await apmFactory.newAPM(openTldHash, openLabelHash, owner) + const apm = getContract('APMRegistry').at(getReceipt(apmReceipt, 'DeployAPM', 'apm')) + console.log(' Deployed APM at', apm.address) + const hatchApm = getContract('APMRegistry').at(getReceipt(hatchApmReceipt, 'DeployAPM', 'apm')) + console.log(' Deployed Hatch APM at', hatchApm.address) + const openApm = getContract('APMRegistry').at(getReceipt(openApmReceipt, 'DeployAPM', 'apm')) + console.log(' Deployed Open APM at', openApm.address) + + // Register apps + for (const { name, contractName } of APPS) { + const appBase = await getContract(contractName).new() + console.log(` Registering package for ${appBase.constructor.contractName} as "${name}.aragonpm.eth"`) + if (name.includes('hatch')) { + await hatchApm.newRepoWithVersion(name.replace('.hatch',''), owner, [ 1, 0, 0 ], appBase.address, '') + } else if (name.includes('open')) { + await openApm.newRepoWithVersion(name.replace('.open',''), owner, [ 1, 0, 0 ], appBase.address, '') + } else { + await apm.newRepoWithVersion(name, owner, [ 1, 0, 0 ], appBase.address, '') + } + } + + + // Deploy OpenEnterpriseTemplate + const baseContracts = [ daoFactBase.address, ens.address, miniMeFactoryBase.address, aragonID.address, bountiesAddress ] + template = await getContract('OpenEnterpriseTemplate').new(baseContracts) + console.log(' Deployed OpenEnterpriseTemplate at', template.address) + console.log('\n ===== Deployments completed =====\n\n') + }) + + context('dao instantiation', () => { + context('when using agent as vault', () => { + const USE_AGENT_AS_VAULT = true + it('should run newTokensAndInstance without error', async () => { + await template.newTokensAndInstance( + randomId(), + TOKEN1_NAME, + TOKEN1_SYMBOL, + TOKEN2_NAME, + TOKEN2_SYMBOL, + VOTING_SETTINGS, + VOTING_BOOLS, + USE_AGENT_AS_VAULT + ) + }) + + it('should run newTokenManagers without error', async () => { + await template.newTokenManagers( + TOKEN_HOLDERS, + TOKEN_STAKES, + TOKEN_HOLDERS, + TOKEN_STAKES, + TOKEN_BOOLS + ) + }) + + it('should run finalizeDao without error', async () => { + await template.finalizeDao( + [ FINANCE_PERIOD, FINANCE_PERIOD ], + false + ) + }) + }) + + context('when using just vault', () => { + const USE_AGENT_AS_VAULT = false + let installedOracle + it('should run newTokensAndInstance without error', async () => { + await template.newTokensAndInstance( + randomId(), + TOKEN1_NAME, + TOKEN1_SYMBOL, + TOKEN2_NAME, + TOKEN2_SYMBOL, + VOTING_SETTINGS, + VOTING_BOOLS, + USE_AGENT_AS_VAULT + ) + }) + + it('should run newTokenManagers without error', async () => { + const result = await template.newTokenManagers( + TOKEN_HOLDERS, + TOKEN_STAKES, + TOKEN_HOLDERS, + TOKEN_STAKES, + TOKEN_BOOLS + ) + installedOracle = getReceipts(result, 'InstalledApp') + .reduce((appAddress, l) => { + return l.args.appId === APP_IDS['whitelist-oracle.hatch'] ? + l.args.appProxy : appAddress + }, null) + }) + + it('should run finalizeDao without error', async () => { + await template.finalizeDao( + [ FINANCE_PERIOD, FINANCE_PERIOD ], + false + ) + }) + + it('should have initialized Oracle', async () => { + await truffleAssert.reverts( + getContract('WhitelistOracle') + .at(installedOracle) + .initialize([]), + 'INIT_ALREADY_INITIALIZED' + ) + }) + }) + + context('special cases', () => { + let installReceipt + + before('reproduce 2 non-transferable tokens scenario', async () => { + /* Create DAO to reproduce */ + await template.newTokensAndInstance( + randomId(), + TOKEN1_NAME, + TOKEN1_SYMBOL, + TOKEN2_NAME, + TOKEN2_SYMBOL, + VOTING_SETTINGS, + VOTING_BOOLS, + false // use agent as vault ? + ) + + installReceipt = await template.newTokenManagers( + [owner], // first token holders + [200], // first token holders stakes + [owner], // second token holders + [200], // second token holders stakes + [ false, false, false, false ] // [Token1Limit, Token1Transferable, Token2Limit, Token2Transferable] + ) + + await template.finalizeDao( + [ FINANCE_PERIOD, FINANCE_PERIOD ], + false + ) + }) + + it('should not allow token-transfers', async () => { + // get TokenManager apps and their tokens + const [ firstTokenManager, secondTokenManager ] = getTokenManagers(installReceipt) + const firstToken = await getTokenFromTokenManager(firstTokenManager) + const secondToken = await getTokenFromTokenManager(secondTokenManager) + console.log(' Tokens found:', await firstToken.name(), await secondToken.name()) + + // assert non-transferability of both tokens + await truffleAssert.reverts( + firstToken.transfer(member1, 100, { from: owner }), + '', // there is no reason for in the contract require + 'first token should be non-transferable' + ) + await truffleAssert.reverts( + secondToken.transfer(member1, 100, { from: owner }), + '', + 'second token should be non-transferable' + ) + }) + }) + }) +}) diff --git a/templates/experimental/truffle-config.js b/templates/experimental/truffle-config.js new file mode 100644 index 000000000..093993637 --- /dev/null +++ b/templates/experimental/truffle-config.js @@ -0,0 +1,14 @@ +// module.exports = require('@aragon/templates-shared/truffle.js') + +const config = require('@aragon/os/truffle-config') + +const gasLimit = 7e6 - 1 + +config.networks.rpc.gas = gasLimit +config.networks.devnet.gas = gasLimit +config.networks.rinkeby.gas = gasLimit +config.networks.ropsten.gas = gasLimit +config.networks.kovan.gas = gasLimit +config.solc.optimizer.runs = 1 + +module.exports = config