diff --git a/.eslintrc b/.eslintrc index 442375e..064cf34 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,9 +3,10 @@ "airbnb-base" ], "rules": { - "indent": ["error", 4], "no-console": "off", - "no-underscore-dangle": "off" + "no-underscore-dangle": "off", + "import/no-extraneous-dependencies": "off", + "strict": "off" }, "env": { "mocha": true, diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..52031de --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sol linguist-language=Solidity diff --git a/.gitignore b/.gitignore index abcc1f7..dbb7432 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,42 @@ # OSX .DS_Store -.idea/ - -# VS Code -.vscode # Vagrant .vagrant/ ubuntu-xenial-16.04-cloudimg-console.log +# npm_package +openst-openst-contracts-*.tgz +npm_package/dist/contracts.json +npm_package/test/node_modules +npm_package/test/openst-openst-contracts-*.tgz +npm_package/test/package.json +npm_package/test/package-lock.json + # don't commit node_modules node_modules +# Don't commit package-lock.json file package-lock.json + +# Do not track builds build/ -contracts/abi/ -contracts/bin/ \ No newline at end of file +# Do not track IDE files +.idea/ +.vscode/ +*.iml + +# LaTeX auxiliary files +*.aux +*.log + +# NPM package generated files: +dist/contracts.json + +## Build tool auxiliary files: +*.synctex +*.synctex(busy) +*.synctex.gz +*.synctex.gz(busy) +*.pdfsync diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c62c7fa --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/gnosis/safe-contracts"] + path = external/gnosis/safe-contracts + url = https://github.com/OpenST/safe-contracts.git diff --git a/.solcover.js b/.solcover.js deleted file mode 100644 index 1767ccb..0000000 --- a/.solcover.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - port: 8555, - compileCommand: '../node_modules/.bin/truffle compile', - testCommand: '../node_modules/.bin/truffle test --network coverage', - skipFiles: ['truffle/Migrations.sol'] -}; \ No newline at end of file diff --git a/.soliumignore b/.soliumignore new file mode 100644 index 0000000..dd159b1 --- /dev/null +++ b/.soliumignore @@ -0,0 +1,3 @@ +node_modules +external/gnosis +contracts/truffle/Migrations.sol diff --git a/.soliumrc.json b/.soliumrc.json new file mode 100644 index 0000000..8450100 --- /dev/null +++ b/.soliumrc.json @@ -0,0 +1,19 @@ +{ + "extends": "solium:recommended", + "plugins": [ + "security" + ], + "rules": { + "error-reason": [ + "error" + ], + "quotes": [ + "error", + "double" + ], + "indentation": [ + "error", + 4 + ] + } +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 4d64a72..ceb1a90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,13 @@ language: node_js sudo: required branches: only: - - master - - develop + - master + - develop + - /^feature\/.*/ + - /^release-.*/ +git: + quiet: true + submodules: true notifications: email: recipients: @@ -19,14 +24,19 @@ before_install: - sudo apt-get install npm - sudo apt-get install software-properties-common - sudo add-apt-repository -y ppa:ethereum/ethereum - - sudo apt-get update - - sudo apt-get --allow-unauthenticated install solc + - git submodule update --init --recursive install: - - npm install + - npm install --quiet + - export PATH=./node_modules/.bin/:${PATH} before_script: - - nohup sh tools/runGanacheCli.sh /dev/null 2>&1 & - - bash tools/compile.sh + - ./tools/run_ganache_cli.sh /dev/null 2>&1 & script: - - truffle test + - npm run update + - npm run lint:sol + - npm run lint:js + - npm run compile-all + - npm run build-package + - npm run test:contracts + - npm run test:package after_script: - - kill $(ps aux | grep 'testrpc' | awk '{print $2}') + - kill $(ps aux | grep 'ganache-cli' | awk '{print $2}') diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29..308e903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,74 @@ +# OpenST Contracts Change Log + +## Version 0.10.0 + +### Changes + +- Contracts: TokenHolder + - In the current release, TokenHolder's ownership key management functionality + (implemented previously in MultiSigWallet contract and inherited by TokenHolder) + and session key management functionality (authorizeSession(), revokeSession(), + logout(), executeRule(), executeRedemption()) are separated. Ownership key + management functionality (MultiSigWallet.sol) is removed and replaced by + Gnosis Safe. TokenHolder receives an owner key during construction. + authorizeSession(), revokeSession() and logout() functions are + guarded by onlyOwner modifier. Logout() function has been revisited and + currently logs out all active session keys and can be called only by the owner. +- Contracts: TokenRules + - Global constraints functionality is removed. + - Direct transfers functionality is added allowing a TokenHolder account to + execute transfers without going around with TransferRule. Direct transfers + are by default enabled for a token economy and can be disabled by + organization's workers. + - TokenRules is Organized, that allows to refine modifiers (previously only + onlyOrganization was available) and add onlyWorker. registerRule, + enableDirectTransfers, disableDirectTransfers are guarded by onlyWorker + modifier. +- Contracts: DelayedRecoveryModule + - DelayedRecoveryModule is a Gnosis Safe module allowing to recover an owner + key in case of loss. Gnosis Safe modules execute transactions without any + confirmation. An owner can sign an intent to recover access in + DelayedRecoveryModule which is relayed to the module by a controller key. The + intent execution can be carried on by any key after a required number of + blocks passes. An owner or controller can abort the recovery process. +- Contracts: PriceOracleInterface and PricerRule + - PriceOracleInterface defines the required interface for price oracles used + within PricerRule. + - PricerRule allows to pay beneficiaries in any currency: price oracle for the + pay currency should be registered beforehand. +- Contracts: MasterCopyNonUpgradable, Proxy, ProxyFactory and UserWalletFactory + - A proxy contracts family is introduced to save gas (currently, in + openst-contracts no contract is upgradeable). + - Proxy: A generic proxy contract + - ProxyFactory: A generic proxy factory contract + - UserWalletFactory: A proxy contract, allowing to create a user wallet + by composing gnosis safe and token holder in a single transaction + (UserWalletFactory::createUserWallet) + - MasterCopyNonUpgradable: Contracts acting as master copies should + inherit (should always be first in inheritance list) from this contract. +- Contracts: OrganizationInterface, Organization, and Organized + - Organization contracts are added into the project. TokenRules and PricerRule + are "is Organized" and using inherited onlyWorker and onlyOrganization modifiers. +- Contracts: Upgraded the contracts to 0.5.0 version of solidity. +- Infrastructure: Replaced the mock naming convention for test doubles to + correct ones: spy, fake, double. +- Infrastructure: Align the Airbnb JS style guide across OpenST protocol + projects through the .eslintrc. +- Infrastructure: Add .gitattributes for Solidity syntax highlighting in Github. +- Infrastructure: Contracts directory restructuring. +- Infrastructure: Remove .solcover.js as it does not support the solidity + version 0.5.0. +- Infrastructure: Add .soliumignore to exclude unnecessary directories and + files to be linted. +- Infrastructure: Add .soliumrc to share solidity lint rules across the project + contributors. +- Infrastructure: Improve travis.yml to include updating of git submodules, + using binaries from ./node_modules/.bin, lint-build-steps and use npm scripts + instead of raw calls. +- Infrastructure: Add CODE_OF_CONDUCT.md for contribution guidelines. +- Infrastructure: Update contracts license to Apache Version 2.0. +- Infrastructure: Update README.md file for release 0.10.0. +- Infrastructure: Update VERSION file for release 0.10.0. +- Infrastructure: Improved package.json with new set of scripts: compile, + compile-all, lint:js, lint:js:fix, lint:sol, lint:sol:fix, lint:build-package. +- Infrastructure: NPM module publishing. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..50b693b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@ost.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/LICENSE b/LICENSE index 65c5ca8..8dada3e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,165 +1,201 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE.CONTRACTS b/LICENSE.CONTRACTS deleted file mode 100644 index 9ff5b0a..0000000 --- a/LICENSE.CONTRACTS +++ /dev/null @@ -1,203 +0,0 @@ -## This license applies to the contents of the /contracts directory. ## - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/OpenSTv0.9.4Components.jpg b/OpenSTv0.9.4Components.jpg deleted file mode 100644 index 22eb42b..0000000 Binary files a/OpenSTv0.9.4Components.jpg and /dev/null differ diff --git a/README.md b/README.md index 274cfcc..926ffb4 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,128 @@ -

OpenST - Empowering Decentralized Economies

+# OpenST Contracts - Empowering Decentralized Economies -[![Gitter: JOIN CHAT](https://img.shields.io/badge/gitter-JOIN%20CHAT-brightgreen.svg)](https://gitter.im/OpenSTFoundation/SimpleToken) +OpenST Contracts is a collection of smart contracts that enable developers to +program Token Economies. -OpenST blockchain infrastructure empowers new economies for mainstream businesses and emerging (D)Apps. The central component of this infrastructure is the OpenST Protocol, a framework for tokenizing businesses. +## Getting Started -_While OpenST is available as-is for anyone to use, we caution that this is early stage software and under heavy ongoing development and improvement. Please report bugs and suggested improvements._ +These instructions will get you a copy of the project up and running on your +local machine for development and testing purposes. See deployment for notes on +how to deploy the project on a live system. -#### Purpose +### Prerequisites -This repository houses the work that focuses on improving the usability of decentralized applications. +Project requires [node](https://nodejs.org/en/) and +[npm](https://www.npmjs.com/get-npm) to be installed on dev machine. -OpenST-contracts is a collection of smart contracts that enable developers to program Token Economies. -
+### Cloning -The major components of this repository are the TokenHolder contracts, TokenRules contract, and the custom Rule contracts that can be registered with the TokenRules contracts to establish the economy specific payments and rewards. -
+In case of fresh clone, use `--recursive-submodules` option while cloning: -TokenHolder Contracts are multi-sig contracts with multiple ownership keys that are housed in different wallets that are controlled by a single human owner (say, a participant in the token economy). The multi-sig logic supports both custodial and fully decentralized key management, thus supporting a wide range of user profiles. -
+```bash +git clone --recursive-submodules git@github.com:openst/openst-contracts.git +``` -TokenRules Contract and custom Rule Contracts represent the administration functionality that enables economy managers to design intra-economy transactions that map to their business logic. +To update git submodules for already cloned repos, run: -#### Diagram +```bash +git submodule update --init --recursive +``` -![Image for readme](OpenSTv0.9.4Components.jpg) +### Installing -#### Related Repositories +Install npm packages, by running: -[openst.js](https://github.com/OpenSTFoundation/openst.js): OpenST.js is a library that enables interaction with openst-contracts to easily create token economies in (D)Apps. The library supports deploying and/or interacting with token, token holder, token rules, and token rule contracts. +```bash +npm install +``` -[openst-js-examples](https://github.com/OpenSTFoundation/openst-js-examples): This repository contains an example usage of -OpenST.js where we walk you through registering rules, adding users, adding wallets to users, revoking ephemeral keys and other functionality in the context of a token economy. In order to make the best use of openst.js, we recommend working through the example to familiarize yourself with the functionality and usage of the library. +Afterwards, add `./node_modules/.bin` to `PATH` environment variable: + +```bash +export PATH=./node_modules/.bin:${PATH} +``` + +## Compiling the contracts + +The following npm script compiles updated contracts from the last call: + +```bash +npm run compile +``` + +, to compile all contracts, run: + +```bash +npm run compile-all +``` + +## Linters + +### Solidity + +In `openst-contracts` to lint solidity files we use [Ethlint](https://github.com/duaraghav8/Ethlint). +The following npm script lints all contracts within `./contracts` directory: + +```bash +npm run lint:sol +``` + +[Ethlint](https://github.com/duaraghav8/Ethlint) is able to fix a subset of rules. +The following npm script fixes (only a subset of rules) contracts within `./contracts` directory: + +```bash +npm run lint:sol:fix +``` + +### JS + +[ESLint](https://eslint.org) is used to lint js files. + +To lint all js files within `./test` directory, run: + +```bash +npm run lint:js +``` + +[ESLint](https://eslint.org) is able to fix a subset of rules. +To fix js files, run: + +```bash +npm run lint:js:fix +``` + +## Running the tests + +Before running the tests run `ganache-cli` by: + +```bash +npm run ganache-cli +``` + +Run tests by calling: + +```bash +npm run test +``` + +## Contributing + +Please read [CODE_OF_CONDUCT.md](https://github.com/openst/openst-contracts/blob/develop/CODE_OF_CONDUCT.md) +for details on our code of conduct, and the process for submitting pull +requests to us. + +## Versioning + +We use [SemVer](http://semver.org/) for versioning. For the versions available, +see the [tags on this repository](https://github.com/openst/openst-contracts/tags). + +## Authors + +See also the list of [contributors](https://github.com/openst/openst-contracts/contributors) +who participated in this project. + +## License + +This contracts are licensed under the Apache License Version 2.0 - see +the [LICENSE.md](https://github.com/openst/openst-contracts/blob/develop/LICENSE.md) +file for details. diff --git a/VERSION b/VERSION deleted file mode 100644 index 2bd77c7..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.9.4 \ No newline at end of file diff --git a/contracts/EIP20Token.sol b/contracts/EIP20Token.sol deleted file mode 100644 index b23f199..0000000 --- a/contracts/EIP20Token.sol +++ /dev/null @@ -1,218 +0,0 @@ -pragma solidity ^0.4.23; - -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import "./EIP20TokenInterface.sol"; -import "./SafeMath.sol"; - -/** - * @title EIP20Token contract which implements EIP20Interface. - * - * @notice Implements EIP20 token. - */ -contract EIP20Token is EIP20TokenInterface { - using SafeMath for uint256; - - string private tokenName; - string private tokenSymbol; - uint8 private tokenDecimals; - - mapping(address => uint256) balances; - mapping(address => mapping (address => uint256)) allowed; - - /** - * @notice Contract constructor. - * - * @param _symbol Symbol of the token. - * @param _name Name of the token. - * @param _decimals Decimal places of the token. - */ - constructor(string _symbol, string _name, uint8 _decimals) public - { - tokenSymbol = _symbol; - tokenName = _name; - tokenDecimals = _decimals; - } - - /** - * @notice Public view function name. - * - * @return string Name of the token. - */ - function name() public view returns (string) { - return tokenName; - } - - /** - * @notice Public view function symbol. - * - * @return string Symbol of the token. - */ - function symbol() public view returns (string) { - return tokenSymbol; - } - - /** - * @notice Public view function decimals. - * - * @return uint8 Decimal places of the token. - */ - function decimals() public view returns (uint8) { - return tokenDecimals; - } - - /** - * @notice Public view function balanceOf. - * - * @param _owner Address of the owner account. - * - * @return uint256 Account balance of the owner account. - */ - function balanceOf(address _owner) public view returns (uint256) { - return balances[_owner]; - } - - /** - * @notice Public view function allowance. - * - * @param _owner Address of the owner account. - * @param _spender Address of the spender account. - * - * @return uint256 Remaining allowance for the spender to spend from owner's account. - */ - function allowance(address _owner, address _spender) public view returns (uint256 remaining) { - return allowed[_owner][_spender]; - } - - /** - * @notice Public function transfer. - * - * @dev Fires the transfer event, throws if, _from account does not have enough - * tokens to spend. - * - * @param _to Address to which tokens are transferred. - * @param _value Amount of tokens to be transferred. - * - * @return bool True for a successful transfer, false otherwise. - */ - function transfer(address _to, uint256 _value) public returns (bool success) { - // According to the EIP20 spec, "transfers of 0 values MUST be treated as normal - // transfers and fire the Transfer event". - // Also, should throw if not enough balance. This is taken care of by SafeMath. - balances[msg.sender] = balances[msg.sender].sub(_value); - balances[_to] = balances[_to].add(_value); - - emit Transfer(msg.sender, _to, _value); - - return true; - } - - /** - * @notice Public function transferFrom. - * - * @dev Allows a contract to transfer tokens on behalf of _from address to _to address, - * the function caller has to be pre-authorized for multiple transfers up to the - * total of _value amount by the _from address. - * - * @param _from Address from which tokens are transferred. - * @param _to Address to which tokens are transferred. - * @param _value Amount of tokens transferred. - * - * @return bool True for a successful transfer, false otherwise. - */ - function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { - balances[_from] = balances[_from].sub(_value); - allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); - balances[_to] = balances[_to].add(_value); - - emit Transfer(_from, _to, _value); - - return true; - } - - /** - * @notice Public function approve. - * - * @dev Allows _spender address to withdraw from function caller's account, multiple times up - * to the _value amount, if this function is called again - * it overwrites the current allowance with _value. - * - * @param _spender Address authorized to spend from the function caller's address. - * @param _value Amount up to which spender is authorized to spend. - * - * @return bool True for a successful approval, false otherwise. - */ - function approve(address _spender, uint256 _value) public returns (bool success) { - - allowed[msg.sender][_spender] = _value; - - emit Approval(msg.sender, _spender, _value); - - return true; - } - - /** - * @notice Internal function claimEIP20. - * - * @dev Subtracts _amount of tokens from EIP20Token contract balance, - * adds _amount to beneficiary's balance. - * - * @param _beneficiary Address of tokens beneificary. - * @param _amount Amount of tokens claimed for beneficiary. - * - * @return bool True if claim of tokens for beneficiary address is successful, - * false otherwise. - */ - function claimEIP20(address _beneficiary, uint256 _amount) internal returns (bool success) { - // claimable tokens are minted in the contract address to be pulled on claim - balances[address(this)] = balances[address(this)].sub(_amount); - balances[_beneficiary] = balances[_beneficiary].add(_amount); - - emit Transfer(address(this), _beneficiary, _amount); - - return true; - } - - /** - * @notice Internal function mintEIP20. - * - * @dev Adds _amount tokens to EIP20Token contract balance. - * - * @param _amount Amount of tokens to mint. - * - * @return bool True if mint is successful, false otherwise. - */ - function mintEIP20(uint256 _amount) internal returns (bool /* success */) { - // mint EIP20 tokens in contract address for them to be claimed - balances[address(this)] = balances[address(this)].add(_amount); - - return true; - } - - /** - * @notice Internal function burnEIP20. - * - * @dev Subtracts _amount tokens from the balance of function caller's address. - * - * @param _amount Amount of tokens to burn. - * - * @return bool True if burn is successful, false otherwise. - */ - function burnEIP20(uint256 _amount) internal returns (bool /* success */) { - balances[msg.sender] = balances[msg.sender].sub(_amount); - - return true; - } -} \ No newline at end of file diff --git a/contracts/EIP20TokenInterface.sol b/contracts/EIP20TokenInterface.sol deleted file mode 100644 index 75c5479..0000000 --- a/contracts/EIP20TokenInterface.sol +++ /dev/null @@ -1,45 +0,0 @@ -pragma solidity ^0.4.23; - -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Based on the 'final' EIP20 token standard as specified at: -// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md - -/** - * @title EIP20Interface - * - * @notice Provides EIP20 token interface - */ -contract EIP20TokenInterface { - - /** Events */ - - event Transfer(address indexed _from, address indexed _to, uint256 _value); - event Approval(address indexed _owner, address indexed _spender, uint256 _value); - - /** Public functions */ - - function name() public view returns (string); - function symbol() public view returns (string); - function decimals() public view returns (uint8); - function totalSupply() public view returns (uint256); - - function balanceOf(address _owner) public view returns (uint256 balance); - function allowance(address _owner, address _spender) public view returns (uint256 remaining); - - function transfer(address _to, uint256 _value) public returns (bool success); - function transferFrom(address _from, address _to, uint256 _value) public returns (bool success); - function approve(address _spender, uint256 _value) public returns (bool success); -} diff --git a/contracts/EIP20TokenMock.sol b/contracts/EIP20TokenMock.sol deleted file mode 100644 index 232ba48..0000000 --- a/contracts/EIP20TokenMock.sol +++ /dev/null @@ -1,79 +0,0 @@ -/* solhint-disable-next-line compiler-fixed */ -pragma solidity ^0.4.23; - -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import "./EIP20Token.sol"; - - -/// @title EIP20TokenMock - Provides EIP20Token with mock functionality to facilitate testing payments -contract EIP20TokenMock is EIP20Token { - uint256 public conversionRate; - uint8 public conversionRateDecimals; - - /// @dev Takes _conversionRate, _symbol, _name, _decimals - /// @param _conversionRate conversionRate - /// @param _symbol symbol - /// @param _name name - /// @param _decimals decimals - constructor( - uint256 _conversionRate, - uint8 _conversionRateDecimals, - string _symbol, - string _name, - uint8 _decimals) - /* solhint-disable-next-line visibility-modifier-order */ - EIP20Token(_symbol, _name, _decimals) - public - { - conversionRate = _conversionRate; - conversionRateDecimals = _conversionRateDecimals; - } - - /// @dev Returns 0 as mock total supply - function totalSupply() - public - view - returns (uint256 /* mock total supply */) - { - return 0; - } - - /// @dev Takes _owner, _value; sets balance of _owner to _value - /// @param _owner owner - /// @param _value value - /// @return bool success - function setBalance( - address _owner, - uint256 _value) - public - returns (bool /* success */) - { - balances[_owner] = _value; - return true; - } - - /// @dev Takes _conversionRate; sets conversionRate to _conversionRate - /// @param _conversionRate conversionRate - /// @return bool success - function setConverionRate( - uint256 _conversionRate) - public - returns (bool /* success */) - { - conversionRate = _conversionRate; - return true; - } -} diff --git a/contracts/MultiSigWallet.sol b/contracts/MultiSigWallet.sol deleted file mode 100644 index 687ed0f..0000000 --- a/contracts/MultiSigWallet.sol +++ /dev/null @@ -1,658 +0,0 @@ -pragma solidity ^0.4.23; - -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import "./SafeMath.sol"; - - -/** - * @title Allows multiple parties to agree on transactions before execution. - * - * @dev The contract implementation is heavily inspired by Gnosis MultiSigWallet - * (https://github.com/gnosis/MultiSigWallet). - */ -contract MultiSigWallet { - - /* Usings */ - - using SafeMath for uint256; - - - /* Events */ - - event WalletAdditionSubmitted( - uint256 indexed _transactionID, - address _wallet - ); - - event WalletRemovalSubmitted( - uint256 indexed _transactionID, - address _wallet - ); - - event RequirementChangeSubmitted( - uint256 indexed _transactionID, - uint256 _required - ); - - event WalletReplacementSubmitted( - uint256 indexed _transactionID, - address _oldWallet, - address _newWallet - ); - - event TransactionConfirmed(uint256 indexed _transactionID, address _wallet); - - event TransactionConfirmationRevoked( - uint256 indexed _transactionID, - address _wallet - ); - - event TransactionExecutionSucceeded(uint256 indexed _transactionID); - - event TransactionExecutionFailed(uint256 indexed _transactionID); - - - /* Constants */ - - bytes4 constant public ADD_WALLET_CALLPREFIX = bytes4( - keccak256("addWallet(address)") - ); - - bytes4 constant public REMOVE_WALLET_CALLPREFIX = bytes4( - keccak256("removeWallet(address)") - ); - - bytes4 constant public REPLACE_WALLET_CALLPREFIX = bytes4( - keccak256("replaceWallet(address,address)") - ); - - bytes4 constant public CHANGE_REQUIREMENT_CALLPREFIX = bytes4( - keccak256("changeRequirement(uint256)") - ); - - - /* Structs */ - - struct Transaction { - address destination; - bytes data; - bool executed; - } - - - /* Storage */ - - /** - * Specifies the number of confirmations required for a proposed - * transaction to be executed. - */ - uint256 public required; - - /** Direct lookup to check existence of a wallet. */ - mapping (address => bool) public isWallet; - - address[] public wallets; - - /** Mapping from transaction ids to transaction confirmations status. */ - mapping (uint256 => mapping (address => bool)) public confirmations; - - /** Mapping from transaction ids to transactions. */ - mapping (uint256 => Transaction) public transactions; - - /** Submitted transaction count. */ - uint256 public transactionCount; - - - /* Modifiers */ - - /** - * Checks that: - * - Number of confirmations (requirement) is less than or - * equal to the wallet numbers. - * - Requirement is not 0. - * - Wallet count is not 0. - */ - modifier validRequirement(uint256 _walletCount, uint256 _required) - { - require( - _required <= _walletCount && - _required != uint256(0) && - _walletCount != uint256(0), - "Requirement validity not fulfilled." - ); - _; - } - - modifier onlyMultisig() - { - require( - msg.sender == address(this), - "Only multisig is allowed to call." - ); - _; - } - - modifier onlyWallet() - { - require( - isWallet[msg.sender], - "Only wallet is allowed to call." - ); - _; - } - - modifier walletExists(address _wallet) - { - require( - isWallet[_wallet], - "Wallet does not exist." - ); - _; - } - - modifier walletDoesNotExist(address _wallet) - { - require( - !isWallet[_wallet], - "Wallet exists." - ); - _; - } - - modifier transactionExists(uint256 _transactionID) - { - require( - transactions[_transactionID].data.length != 0, - "Transaction does not exist." - ); - _; - } - - modifier transactionIsConfirmedBy(uint256 _transactionID, address _wallet) - { - require( - confirmations[_transactionID][_wallet], - "Transaction is not confirmed by the wallet." - ); - _; - } - - modifier transactionIsNotConfirmedBy( - uint256 _transactionID, - address _wallet - ) - { - require( - !confirmations[_transactionID][_wallet], - "Transaction is confirmed by the wallet." - ); - _; - } - - modifier transactionIsNotExecuted(uint256 _transactionID) - { - require( - !transactions[_transactionID].executed, - "Transaction is executed." - ); - _; - } - - /** - * Modifier is used as a replacement to require call as this check - * should be done before calling other modifiers like validRequirement. - */ - modifier walletIsNotNull(address _address) - { - require( - _address != address(0), - "Wallet address is null." - ); - _; - } - - - /* Special Functions */ - - /** - * @dev Contract constructor sets initial wallets and required number of - * confirmations. - * Requires: - * - Requirement validity held. - * - No wallet address is null. - * - No duplicate wallet address in list. - * - * @param _wallets List of initial wallets. - * @param _required Number of required confirmations. - */ - constructor(address[] _wallets, uint256 _required) - public - validRequirement(_wallets.length, _required) - { - for (uint256 i = 0; i < _wallets.length; i++) { - require(_wallets[i] != address(0), "Wallet address is 0."); - require(!isWallet[_wallets[i]], "Duplicate wallet address."); - isWallet[_wallets[i]] = true; - } - - wallets = _wallets; - required = _required; - } - - - /* External Functions */ - - /** - * @notice Submits a transaction for wallet addition. - * - * @dev Function requires: - * - Only registered wallet can call. - * - Wallet to add is not null. - * - Wallet to add does not exist. - * - Requirement validity held. - * - * @return Newly created transaction id. - */ - function submitAddWallet(address _wallet) - external - onlyWallet - walletIsNotNull(_wallet) - walletDoesNotExist(_wallet) - validRequirement(wallets.length.add(1), required) - returns (uint256 transactionID_) - { - transactionID_ = addTransaction( - address(this), - abi.encodeWithSelector(ADD_WALLET_CALLPREFIX, _wallet) - ); - - emit WalletAdditionSubmitted(transactionID_, _wallet); - - confirmTransaction(transactionID_); - } - - /** - * @notice Submits a transaction for wallet removal. - * - * @dev Updates the requirement by setting equal to the registered wallets - * number if after wallet removal the requirement is bigger then - * the wallet number. - * Function requires: - * - Only registered wallet can call. - * - Wallet to remove exists. - * - * @return Newly created transaction id. - */ - function submitRemoveWallet(address _wallet) - external - onlyWallet - walletExists(_wallet) - returns (uint256 transactionID_) - { - require( - wallets.length > 1, - "Last wallet cannot be submitted for removal." - ); - - transactionID_ = addTransaction( - address(this), - abi.encodeWithSelector(REMOVE_WALLET_CALLPREFIX, _wallet) - ); - - emit WalletRemovalSubmitted(transactionID_, _wallet); - - confirmTransaction(transactionID_); - } - - /** - * @notice Submits a transaction for a wallet replacement. - * - * @dev Function requires: - * - Only registered wallet can call. - * - Old wallet exists. - * - New wallet address is not null. - * - New wallet does not exist. - * - * @param _oldWallet Wallet to remove. - * @param _newWallet Wallet to add. - * - * @return Newly created transaction id. - */ - function submitReplaceWallet(address _oldWallet, address _newWallet) - external - onlyWallet - walletExists(_oldWallet) - walletIsNotNull(_newWallet) - walletDoesNotExist(_newWallet) - returns (uint256 transactionID_) - { - transactionID_ = addTransaction( - address(this), - abi.encodeWithSelector( - REPLACE_WALLET_CALLPREFIX, - _oldWallet, - _newWallet - ) - ); - - emit WalletReplacementSubmitted(transactionID_, _oldWallet, _newWallet); - - confirmTransaction(transactionID_); - } - - /** - * @notice Submits a transaction for changing the requirement. - * - * @dev Function requires: - * - Only registered wallet can call. - * - Requirement validity held. - * - * @param _required The number of required confirmations. - * - * @return Newly created transaction id. - */ - function submitRequirementChange(uint256 _required) - external - onlyWallet - validRequirement(wallets.length, _required) - returns (uint256 transactionID_) - { - transactionID_ = addTransaction( - address(this), - abi.encodeWithSelector(CHANGE_REQUIREMENT_CALLPREFIX, _required) - ); - - emit RequirementChangeSubmitted(transactionID_, _required); - - confirmTransaction(transactionID_); - } - - - /* Public Functions */ - - /** - * @notice Confirms the transaction by the caller. - * - * @dev After confirmation by the caller, function asks to execute - * transaction. If required number of confirmations were done - * execution happens. - * Function requires: - * - Only registered wallet can call. - * - Transaction exists. - * - Transaction was not confirmed by the caller. - */ - function confirmTransaction(uint256 _transactionID) - public - onlyWallet - transactionExists(_transactionID) - transactionIsNotConfirmedBy(_transactionID, msg.sender) - { - confirmations[_transactionID][msg.sender] = true; - - emit TransactionConfirmed(_transactionID, msg.sender); - - executeTransaction(_transactionID); - } - - /** - * @notice Revokes confirmation made previously by the caller wallet. - * - * @dev Function requires: - * - Only registered wallet can call. - * - Transaction to revoke exists. - * - Transaction was confirmed previously be the caller. - * - Transaction was not executed. - * - * @param _transactionID Transaction id to revoke the previous confirmation. - */ - function revokeConfirmation(uint256 _transactionID) - public - onlyWallet - transactionExists(_transactionID) - transactionIsConfirmedBy(_transactionID, msg.sender) - transactionIsNotExecuted(_transactionID) - { - confirmations[_transactionID][msg.sender] = false; - - emit TransactionConfirmationRevoked(_transactionID, msg.sender); - } - - /** - * @dev Executes transaction if it is confirmed. - * - * Marks stored transaction object's 'executed' field false if - * transaction failed otherwise true. - * Registered wallet could execute failed transaction again. - * - * Function requires: - * - Only registered wallet can call. - * - Transaction exists. - * - Transaction is confirmed by the caller wallet. - * - Transaction is not executed. - * - * @param _transactionID Transaction ID to execute. - */ - function executeTransaction(uint256 _transactionID) - public - onlyWallet - transactionExists(_transactionID) - transactionIsConfirmedBy(_transactionID, msg.sender) - transactionIsNotExecuted(_transactionID) - { - if (isTransactionConfirmed(_transactionID)) { - Transaction storage t = transactions[_transactionID]; - // solium-disable-next-line security/no-low-level-calls - if (t.destination.call(t.data)) { - t.executed = true; - emit TransactionExecutionSucceeded(_transactionID); - } else { - t.executed = false; - emit TransactionExecutionFailed(_transactionID); - } - } - } - - /** @notice Returns the number of registered wallets. */ - function walletCount() - public - view - returns(uint256) - { - return wallets.length; - } - - /** - * @notice Adds a new wallet. - * - * @dev Validity of requirement is going to be called in all - * functions that alter either wallets count or required confirmations - * count. This needs to make sure that imporant invariant always - * holds. - * Function requires: - * - Wallet address to add is not null. - * - Wallet to add does not exist. - * - Transaction is sent by multisig. - * - Requirement validity held. - * - * @param _wallet Wallet address to add. - */ - function addWallet(address _wallet) - public - onlyMultisig - walletIsNotNull(_wallet) - walletDoesNotExist(_wallet) - validRequirement(wallets.length.add(1), required) - { - isWallet[_wallet] = true; - wallets.push(_wallet); - } - - /** - * @notice Remove the specified wallet. - * - * @dev Function requires: - * - Transaction is sent by multisig. - * - Wallet address to remove exists. - * - * @param _wallet Wallet address to remove. - */ - function removeWallet(address _wallet) - public - onlyMultisig - walletExists(_wallet) - { - delete isWallet[_wallet]; - - for (uint256 i = 0; i < wallets.length - 1; i++) { - if (wallets[i] == _wallet) { - wallets[i] = wallets[wallets.length - 1]; - break; - } - } - wallets.length = wallets.length.sub(1); - if (required > wallets.length) { - changeRequirement(wallets.length); - } - } - - /** - * @notice Replace the wallet with a new one. - * - * @dev Function requires: - * - Only multisig can call. - * - Wallet to remove exists. - * - Wallet to add is not null. - * - Wallet to add does not exist. - * - * @param _oldWallet Wallet to remove. - * @param _newWallet Wallet to add. - */ - function replaceWallet(address _oldWallet, address _newWallet) - public - onlyMultisig - walletExists(_oldWallet) - walletIsNotNull(_newWallet) - walletDoesNotExist(_newWallet) - { - for (uint256 i = 0; i < wallets.length; i++) { - if (wallets[i] == _oldWallet) { - wallets[i] = _newWallet; - break; - } - } - isWallet[_oldWallet] = false; - isWallet[_newWallet] = true; - } - - /** - * @notice Changes requirement. - * - * @dev Function requires: - * - Transaction is sent by multisig. - * - Requirement validity held. - * - * @param _required The number of required confirmations. - */ - function changeRequirement( - uint256 _required - ) - public - onlyMultisig - validRequirement(wallets.length, _required) - { - required = _required; - } - - - /* Internal Functions */ - - /** - * @notice Returns the confirmation status of a transaction. - * - * @dev Transaction is confirmed if wallets' count that confirmed - * the transaction is bigger or equal to required. - * Function checks confirmation condition based on current set of - * registered wallets. - * Function returns true if transaction was executed despite if - * with current set of registered wallets it might not. - * Function requires: - * - Transaction with the specified id exists. - * - * @param _transactionID Transaction id to check. - * - * @return Returns true if the transaction is executed or confirmation is - * achieved with current set of registered wallets, otherwise false. - */ - function isTransactionConfirmed(uint256 _transactionID) - public - view - transactionExists(_transactionID) - returns (bool) - { - if (transactions[_transactionID].executed) { - return true; - } - - uint256 count = 0; - for (uint256 i = 0; i < wallets.length; i++) { - if (confirmations[_transactionID][wallets[i]]) { - count += 1; - } - if (count == required) { - return true; - } - } - - return false; - } - - /** - * @dev Adds a new transaction into transactions mapping. - * Function requires: - * - Destination address is not null. - * - Data payload is not empty. - * - * - * @param _destination Transaction destination address to execute against. - * @param _data Transaction data payload. - * - * @return transactionID_ Returns newly created transaction id. - */ - function addTransaction(address _destination, bytes _data) - internal - returns (uint256 transactionID_) - { - require( - _destination != address(0), - "Destination address is null." - ); - require( - _data.length != 0, - "Payload data length is 0." - ); - - transactionID_ = transactionCount; - - transactions[transactionID_] = Transaction({ - destination: _destination, - data: _data, - executed: false - }); - - transactionCount = transactionCount.add(1); - } - -} \ No newline at end of file diff --git a/contracts/TokenHolder.sol b/contracts/TokenHolder.sol deleted file mode 100644 index 6eade7b..0000000 --- a/contracts/TokenHolder.sol +++ /dev/null @@ -1,451 +0,0 @@ -pragma solidity ^0.4.23; - -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import "./SafeMath.sol"; -import "./EIP20TokenInterface.sol"; -import "./MultiSigWallet.sol"; -import "./TokenRules.sol"; - - -/** - * @title TokenHolder contract. - * - * @notice Implements executable transactions (EIP-1077) for users to interact - * with token rules. It enables users to authorise sessions for - * ephemeral keys that dapps and mainstream applications can use to - * generate token events on-chain. - */ -contract TokenHolder is MultiSigWallet { - - /* Usings */ - - using SafeMath for uint256; - - - /* Events */ - - event SessionAuthorizationSubmitted( - uint256 indexed _transactionID, - address _ephemeralKey, - uint256 _spendingLimit, - uint256 _expirationHeight - ); - - event SessionRevoked( - address _ephemeralKey - ); - - event RuleExecuted( - address indexed _to, - bytes4 _functionSelector, - address _ephemeralKey, - uint256 _nonce, - bytes32 _messageHash, - bool _status - ); - - - /* Enums */ - - enum AuthorizationStatus { - NOT_AUTHORIZED, - AUTHORIZED, - REVOKED - } - - - /* Structs */ - - /** expirationHeight is the block number at which ephemeralKey expires. */ - struct EphemeralKeyData { - uint256 spendingLimit; - uint256 nonce; - uint256 expirationHeight; - AuthorizationStatus status; - } - - - /* Constants */ - - bytes4 constant public AUTHORIZE_SESSION_CALLPREFIX = bytes4( - keccak256("authorizeSession(address,uint256,uint256)") - ); - - bytes4 public constant EXECUTE_RULE_CALLPREFIX = bytes4( - keccak256( - "executeRule(address,bytes,uint256,uint8,bytes32,bytes32)" - ) - ); - - - /* Storage */ - - EIP20TokenInterface public token; - - mapping(address /* key */ => EphemeralKeyData) public ephemeralKeys; - - address public tokenRules; - - - /* Modifiers */ - - modifier keyIsNotNull(address _key) - { - require(_key != address(0), "Key address is null."); - _; - } - - /** Requires that key is in authorized state. */ - modifier keyIsAuthorized(address _key) - { - AuthorizationStatus status = ephemeralKeys[_key].status; - require( - status == AuthorizationStatus.AUTHORIZED, - "Key is not authorized." - ); - _; - } - - /** Requires that key was not authorized. */ - modifier keyDoesNotExist(address _key) - { - AuthorizationStatus status = ephemeralKeys[_key].status; - require( - status == AuthorizationStatus.NOT_AUTHORIZED, - "Key exists." - ); - _; - } - - - /* Special Functions */ - - /** - * @dev Constructor requires: - * - EIP20 token address is not null. - * - Token rules address is not null. - * - * @param _token eip20 contract address deployed for an economy. - * @param _tokenRules Token rules contract address. - * @param _wallets array of wallet addresses. - * @param _required No of requirements for multi sig wallet. - */ - constructor( - EIP20TokenInterface _token, - address _tokenRules, - address[] _wallets, - uint256 _required - ) - public - MultiSigWallet(_wallets, _required) - { - require( - _token != address(0), - "Token contract address is null." - ); - require( - _tokenRules != address(0), - "TokenRules contract address is null." - ); - - token = _token; - tokenRules = _tokenRules; - } - - - /* External Functions */ - - /** - * @notice Submits a transaction for a session authorization with - * the specified ephemeral key. - * - * @dev Function requires: - * - Only registered wallet can call. - * - The key is not null. - * - The key does not exist. - * - Expiration height is bigger than the current block height. - * - * @param _ephemeralKey Ephemeral key to authorize. - * @param _spendingLimit Spending limit of the key. - * @param _expirationHeight Expiration height of the ephemeral key. - * - * @return transactionID_ Newly created transaction id. - */ - function submitAuthorizeSession( - address _ephemeralKey, - uint256 _spendingLimit, - uint256 _expirationHeight - ) - public - onlyWallet - keyIsNotNull(_ephemeralKey) - keyDoesNotExist(_ephemeralKey) - returns (uint256 transactionID_) - { - require( - _expirationHeight > block.number, - "Expiration height is lte to the current block height." - ); - - transactionID_ = addTransaction( - address(this), - abi.encodeWithSelector( - AUTHORIZE_SESSION_CALLPREFIX, - _ephemeralKey, - _spendingLimit, - _expirationHeight - ) - ); - - emit SessionAuthorizationSubmitted( - transactionID_, - _ephemeralKey, - _spendingLimit, - _expirationHeight - ); - - confirmTransaction(transactionID_); - } - - /** - * @notice Revokes session for the specified ephemeral key. - * - * @dev Function revokes the key even if it has expired. - * Function requires: - * - Only registered wallet can call. - * - The key is authorized. - * - * @param _ephemeralKey Ephemeral key to revoke. - */ - function revokeSession(address _ephemeralKey) - external - onlyWallet - keyIsAuthorized(_ephemeralKey) - { - ephemeralKeys[_ephemeralKey].status = AuthorizationStatus.REVOKED; - - emit SessionRevoked(_ephemeralKey); - } - - /* Public Functions */ - - /** - * @notice Evaluates executable transaction signed by an ephemeral key. - * - * @dev As a first step, function validates executable transaction by - * checking that the specified signature matches one of the - * authorized (non-expired) ephemeral keys. - * - * On success, function executes transaction by calling: - * _to.call(_data); - * - * Before execution, it approves the tokenRules as a spender - * for ephemeralKey.spendingLimit amount. This allowance is cleared - * after execution. - * - * Function requires: - * - The key used to sign data is authorized and have not expired. - * - nonce matches the next available one (+1 of the last - * used one). - * - * @param _to The target contract address the transaction will be executed - * upon. - * @param _data The payload of a function to be executed in the target - * contract. - * @param _nonce The nonce of an ephemeral key that was used to sign - * the transaction. - * - * @return executeStatus_ True in case of successfull execution of the - * executable transaction, otherwise, false. - */ - function executeRule( - address _to, - bytes _data, - uint256 _nonce, - uint8 _v, - bytes32 _r, - bytes32 _s - ) - public - payable - returns (bool executionStatus_) - { - bytes32 messageHash = bytes32(0); - address ephemeralKey = address(0); - (messageHash, ephemeralKey) = verifyExecutableTransaction( - EXECUTE_RULE_CALLPREFIX, - _to, - _data, - _nonce, - _v, - _r, - _s - ); - - EphemeralKeyData storage ephemeralKeyData = ephemeralKeys[ephemeralKey]; - - TokenRules(tokenRules).allowTransfers(); - - token.approve( - tokenRules, - ephemeralKeyData.spendingLimit - ); - - // solium-disable-next-line security/no-call-value - executionStatus_ = _to.call.value(msg.value)(_data); - - token.approve(tokenRules, 0); - - TokenRules(tokenRules).disallowTransfers(); - - bytes4 functionSelector = bytesToBytes4(_data); - - emit RuleExecuted( - _to, - functionSelector, - ephemeralKey, - _nonce, - messageHash, - executionStatus_ - ); - } - - function authorizeSession( - address _ephemeralKey, - uint256 _spendingLimit, - uint256 _expirationHeight - ) - public - onlyMultisig - keyIsNotNull(_ephemeralKey) - keyDoesNotExist(_ephemeralKey) - { - require( - _expirationHeight > block.number, - "Expiration height is lte to the current block height." - ); - - EphemeralKeyData storage keyData = ephemeralKeys[_ephemeralKey]; - - keyData.spendingLimit = _spendingLimit; - keyData.expirationHeight = _expirationHeight; - keyData.nonce = 0; - keyData.status = AuthorizationStatus.AUTHORIZED; - } - - - /* Private Functions */ - - function verifyExecutableTransaction( - bytes4 _callPrefix, - address _to, - bytes _data, - uint256 _nonce, - uint8 _v, - bytes32 _r, - bytes32 _s - ) - private - returns (bytes32 messageHash_, address key_) - { - messageHash_ = getMessageHash( - _callPrefix, - _to, - keccak256(_data), - _nonce - ); - - key_ = ecrecover(messageHash_, _v, _r, _s); - - EphemeralKeyData storage keyData = ephemeralKeys[key_]; - - require( - keyData.status == AuthorizationStatus.AUTHORIZED && - keyData.expirationHeight > block.number, - "Ephemeral key is not active." - ); - - uint256 expectedNonce = keyData.nonce.add(1); - - require( - _nonce == expectedNonce, - "The next nonce is not provided." - ); - - keyData.nonce = expectedNonce; - } - - /** - * @notice The hashed message format is compliant with EIP-1077. - * - * @dev EIP-1077 enables user to sign messages to show intent of execution, - * but allows a third party relayer to execute them. - * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1077.md - */ - function getMessageHash( - bytes4 _callPrefix, - address _to, - bytes32 _dataHash, - uint256 _nonce - ) - private - view - returns (bytes32 messageHash_) - { - messageHash_ = keccak256( - abi.encodePacked( - byte(0x19), // Starting a transaction with byte(0x19) ensure - // the signed data from being a valid ethereum - // transaction. - byte(0), // The version control byte. - address(this), // The from field will always be the contract - // executing the code. - _to, - uint8(0), // The amount in ether to be sent. - _dataHash, - _nonce, - uint8(0), // gasPrice - uint8(0), // gasLimit - uint8(0), // gasToken - _callPrefix, // 4 byte standard call prefix of the - // function to be called in the 'from' contract. - // This guarantees that signed message can - // be only executed in a single instance. - uint8(0), // 0 for a standard call, 1 for a DelegateCall and 2 - // for a create opcode - bytes32(0) // extraHash is always hashed at the end. This is - // done to increase future compatibility of the - // standard. - ) - ); - } - - /** - * @dev Retrieves the first 4 bytes of input byte array into byte4. - * Function requires: - * - Input byte array's length is greater than or equal to 4. - */ - function bytesToBytes4(bytes _input) public pure returns (bytes4 out_) { - require( - _input.length >= 4, - "Input bytes length is less than 4." - ); - - for (uint8 i = 0; i < 4; i++) { - out_ |= bytes4(_input[i] & 0xFF) >> (i * 8); - } - } -} \ No newline at end of file diff --git a/contracts/TokenRulesMock.sol b/contracts/TokenRulesMock.sol deleted file mode 100644 index 1e167ac..0000000 --- a/contracts/TokenRulesMock.sol +++ /dev/null @@ -1,41 +0,0 @@ -pragma solidity ^0.4.23; - -contract TokenRulesMock { - - /* Storage */ - - mapping (address => bool) public allowedTransfers; - address public from; - address[] public transferTo; - uint256[] public transferAmount; - - - /* External Functions */ - - function allowTransfers() - external - { - allowedTransfers[msg.sender] = true; - } - - function disallowTransfers() - external - { - allowedTransfers[msg.sender] = false; - } - - function executeTransfers( - address _from, - address[] _transfersTo, - uint256[] _transfersAmount - ) - external - { - - from = _from; - transferTo = _transfersTo; - transferAmount = _transfersAmount; - - } - -} diff --git a/contracts/TransferRule.sol b/contracts/TransferRule.sol deleted file mode 100644 index 60cf283..0000000 --- a/contracts/TransferRule.sol +++ /dev/null @@ -1,60 +0,0 @@ -pragma solidity ^0.4.23; - - -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import "./TokenRules.sol"; - -contract TransferRule { - - /* Variables */ - - TokenRules tokenRules; - - - /* Functions */ - - constructor ( - address _tokenRules - ) - public - { - require (_tokenRules != address(0), "Token rules address is null."); - tokenRules = TokenRules(_tokenRules); - } - - function transferFrom ( - address _from, - address _to, - uint256 _amount - ) - public - returns (bool) - { - address[] memory transfersTo = new address[](1); - transfersTo[0] = _to; - - uint256[] memory transfersAmount = new uint256[](1); - transfersAmount[0] = _amount; - - TokenRules(tokenRules).executeTransfers( - _from, - transfersTo, - transfersAmount - ); - - return true; - } -} diff --git a/contracts/SafeMath.sol b/contracts/external/SafeMath.sol similarity index 50% rename from contracts/SafeMath.sol rename to contracts/external/SafeMath.sol index 92a8e94..584bb15 100644 --- a/contracts/SafeMath.sol +++ b/contracts/external/SafeMath.sol @@ -1,6 +1,5 @@ -pragma solidity ^0.4.23; - -// Copyright 2018 OpenST Ltd. +pragma solidity ^0.5.0; +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,7 +12,7 @@ pragma solidity ^0.4.23; // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// +// // ---------------------------------------------------------------------------- // Common: SafeMath Library Implementation // @@ -25,43 +24,74 @@ pragma solidity ^0.4.23; // The MIT License. // ---------------------------------------------------------------------------- +/* solium-disable */ /** - @title SafeMath - @notice Implements SafeMath -*/ + * @title SafeMath + * @dev Math operations with safety checks that revert on error. + */ library SafeMath { + /** + * @dev Multiplies two numbers, reverts on overflow. + */ function mul(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 c = a * b; + // Gas optimization: this is cheaper than requiring 'a' not being zero, + // but the benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 + if (a == 0) { + return 0; + } - assert(a == 0 || c / a == b); + uint256 c = a * b; + require(c / a == b); return c; } - + /** + * @dev Integer division of two numbers truncating the quotient, reverts + * on division by zero. + */ function div(uint256 a, uint256 b) internal pure returns (uint256) { - // Solidity automatically throws when dividing by 0 + // Solidity only automatically asserts when dividing by 0. + require(b > 0); uint256 c = a / b; - // assert(a == b * c + a % b); // There is no case in which this doesn't hold + // There is no case in which this doesn't hold + // assert(a == b * c + a % b); + return c; } - + /** + * @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is + * greater than minuend). + * + */ function sub(uint256 a, uint256 b) internal pure returns (uint256) { - assert(b <= a); + require(b <= a); + uint256 c = a - b; - return a - b; + return c; } - + /** + * @dev Adds two numbers, reverts on overflow. + */ function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; - - assert(c >= a); + require(c >= a); return c; } -} + + /** + * @dev Divides two numbers and returns the remainder, + * (unsigned integer modulo) reverts when dividing by zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + require(b != 0); + return a % b; + } +} \ No newline at end of file diff --git a/contracts/gnosis_safe_modules/DelayedRecoveryModule.sol b/contracts/gnosis_safe_modules/DelayedRecoveryModule.sol new file mode 100644 index 0000000..4c6ca93 --- /dev/null +++ b/contracts/gnosis_safe_modules/DelayedRecoveryModule.sol @@ -0,0 +1,494 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "./GnosisSafeModule.sol"; +import "./GnosisSafeModuleManagerInterface.sol"; + +/** + * @title Allows to replace an owner without Safe confirmations + * if the recovery owner and the recovery controller approves + * replacement. + * + * @dev The contract is a module for gnosis safe multisig contract. + * Gnosis safe multisig's modules assume a common interface which + * in the current gnosis safe implementation is inside Module.sol + * contract of Gnosis Safe. Instead of inheriting from this contract + * (not to include gnosis contracts into build process) + * GnosisSafeModule.sol and GnosisSafeModuleManagerInterface.sol contracts + * are introduced that contains the required public interfaces. + */ +contract DelayedRecoveryModule is GnosisSafeModule { + + /* Events */ + + event RecoveryInitiated( + address _prevOwner, + address _oldOwner, + address _newOwner + ); + + event RecoveryExecuted( + address _prevOwner, + address _oldOwner, + address _newOwner + ); + + event RecoveryAborted( + address _prevOwner, + address _oldOwner, + address _newOwner + ); + + event ResetRecoveryOwner( + address _oldRecoveryOwner, + address _newRecoveryOwner + ); + + + /* Constants */ + + string public constant NAME = "Delayed Recovery Module"; + + string public constant VERSION = "0.1.0"; + + bytes32 public constant DOMAIN_SEPARATOR_TYPEHASH = keccak256( + "EIP712Domain(address verifyingContract)" + ); + + bytes32 public constant INITIATE_RECOVERY_STRUCT_TYPEHASH = keccak256( + "InitiateRecoveryStruct(address prevOwner,address oldOwner,address newOwner)" + ); + + bytes32 public constant ABORT_RECOVERY_STRUCT_TYPEHASH = keccak256( + "AbortRecoveryStruct(address prevOwner,address oldOwner,address newOwner)" + ); + + bytes32 public constant RESET_RECOVERY_OWNER_STRUCT_TYPEHASH = keccak256( + "ResetRecoveryOwnerStruct(address oldRecoveryOwner,address newRecoveryOwner)" + ); + + + /* Structs */ + + struct RecoveryInfo { + address prevOwner; + address oldOwner; + address newOwner; + uint256 executionBlockHeight; + } + + + /* Storage */ + + bytes32 public domainSeparator; + + address public recoveryController; + + address public recoveryOwner; + + uint256 public recoveryBlockDelay; + + RecoveryInfo public activeRecoveryInfo; + + + /* Modifiers */ + + modifier onlyRecoveryController() + { + require( + msg.sender == recoveryController, + "Only recovery controller is allowed to call." + ); + + _; + } + + modifier activeRecovery() + { + require( + activeRecoveryInfo.executionBlockHeight > 0, + "There is no active recovery." + ); + + _; + } + + modifier noActiveRecovery() + { + require( + activeRecoveryInfo.executionBlockHeight == 0, + "There is an active recovery." + ); + + _; + } + + modifier validRecovery( + address _prevOwner, + address _oldOwner, + address _newOwner + ) + { + require( + // solium-disable-next-line operator-whitespace + activeRecoveryInfo.prevOwner == _prevOwner + && activeRecoveryInfo.oldOwner == _oldOwner + && activeRecoveryInfo.newOwner == _newOwner, + "The execution request's data does not match with the active one." + ); + + _; + } + + + /* External Functions */ + + /** + * @notice Setups the contract initial storage. + * + * Function requires: + * - Function can be called only once. This is assured by + * checking domainSeparator not to be set previously (bytes32(0)) + * - Recovery owner's address is not null. + * - Recovery controller's address is not null. + * - A required number of blocks to pass to be able to execute + * a recovery shoudld be greater than 0. + * + * @param _recoveryOwner An address that signs the "recovery + * initiation/abortion" and "reset recovery owner" + * requests. + * @param _recoveryController An address that relays signed requests of + * different types. + * @param _recoveryBlockDelay A required number of blocks to pass to + * be able to execute a recovery request. + */ + function setup( + address _recoveryOwner, + address _recoveryController, + uint256 _recoveryBlockDelay + ) + external + { + // This check assures that the setup function is called only once. + require( + domainSeparator == bytes32(0), + "Domain separator was already set." + ); + + require( + _recoveryOwner != address(0), + "Recovery owner's address is null." + ); + + require( + _recoveryController != address(0), + "Recovery controller's address is null." + ); + + require( + _recoveryBlockDelay > 0, + "Recovery block delay is 0." + ); + + domainSeparator = keccak256( + abi.encode( + DOMAIN_SEPARATOR_TYPEHASH, + this + ) + ); + + manager = GnosisSafeModuleManagerInterface(msg.sender); + + recoveryOwner = _recoveryOwner; + + recoveryController = _recoveryController; + + recoveryBlockDelay = _recoveryBlockDelay; + } + + /** + * @notice Initiates a recovery procedure. + * + * @dev Function requires: + * - Only the recovery controller can call. + * - There is no active recovery procedure. + * - Recovery owner has signed struct. + * + * @param _prevOwner Owner that pointed to the owner to be replaced in the + * linked list. + * @param _oldOwner Owner address to replace. + * @param _newOwner New owner address. + */ + function initiateRecovery( + address _prevOwner, + address _oldOwner, + address _newOwner, + bytes32 _r, + bytes32 _s, + uint8 _v + ) + external + onlyRecoveryController + noActiveRecovery + { + bytes32 recoveryHash = hashRecovery( + INITIATE_RECOVERY_STRUCT_TYPEHASH, + _prevOwner, + _oldOwner, + _newOwner + ); + + verify(recoveryHash, _r, _s, _v); + + activeRecoveryInfo = RecoveryInfo({ + prevOwner: _prevOwner, + oldOwner: _oldOwner, + newOwner: _newOwner, + executionBlockHeight: block.number + recoveryBlockDelay + }); + + emit RecoveryInitiated( + _prevOwner, + _oldOwner, + _newOwner + ); + } + + /** + * @notice Executes the initiated recovery. + * + * @dev Function requires: + * - Only recovery controller can call. + * - There is an initiated recovery with the same tuple of + * addresses (prevOwner, oldOwner, newOwner). + * - The required (delay) block numbers has been progressed. + */ + function executeRecovery( + address _prevOwner, + address _oldOwner, + address _newOwner + ) + external + onlyRecoveryController + activeRecovery + validRecovery(_prevOwner, _oldOwner, _newOwner) + { + require( + activeRecoveryInfo.executionBlockHeight < block.number, + "Required number of blocks to recover was not progressed." + ); + + // prevent re-entry by deleting activeRecoveryInfo early. + delete activeRecoveryInfo; + + bytes memory data = abi.encodeWithSignature( + "swapOwner(address,address,address)", + _prevOwner, + _oldOwner, + _newOwner + ); + + require( + manager.execTransactionFromModule( + address(manager), + 0, + data, + GnosisSafeModuleManagerInterface.Operation.Call + ), + "Recovery execution failed." + ); + + emit RecoveryExecuted( + _prevOwner, + _oldOwner, + _newOwner + ); + } + + /** + * @notice Aborts the initiated recovery. + * + * @dev Function requires: + * - There is an initiated recovery with the same tuple of + * addresses (prevOwner, oldOwner, newOwner). + * - Recovery owner has signed struct. + */ + function abortRecoveryByOwner( + address _prevOwner, + address _oldOwner, + address _newOwner, + bytes32 _r, + bytes32 _s, + uint8 _v + ) + external + activeRecovery + validRecovery(_prevOwner, _oldOwner, _newOwner) + { + bytes32 recoveryHash = hashRecovery( + ABORT_RECOVERY_STRUCT_TYPEHASH, + _prevOwner, + _oldOwner, + _newOwner + ); + + verify(recoveryHash, _r, _s, _v); + + delete activeRecoveryInfo; + + emit RecoveryAborted( + _prevOwner, + _oldOwner, + _newOwner + ); + } + + /** + * @notice Aborts the initiated recovery. + * + * @dev Function requires: + * - Only recovery controller can call. + * - There is an initiated recovery with the same tuple of + * addresses (prevOwner, oldOwner, newOwner). + */ + function abortRecoveryByController( + address _prevOwner, + address _oldOwner, + address _newOwner + ) + external + onlyRecoveryController + activeRecovery + validRecovery(_prevOwner, _oldOwner, _newOwner) + { + delete activeRecoveryInfo; + + emit RecoveryAborted( + _prevOwner, + _oldOwner, + _newOwner + ); + } + + function resetRecoveryOwner( + address _newRecoveryOwner, + bytes32 _r, + bytes32 _s, + uint8 _v + ) + external + onlyRecoveryController + { + require( + _newRecoveryOwner != address(0), + "New recovery owner's address is null." + ); + + bytes32 resetRecoveryOwnerHash = hashResetRecoveryOwner( + recoveryOwner, + _newRecoveryOwner + ); + + verify(resetRecoveryOwnerHash, _r, _s, _v); + + address oldRecoveryOwner = recoveryOwner; + + recoveryOwner = _newRecoveryOwner; + + emit ResetRecoveryOwner( + oldRecoveryOwner, + recoveryOwner + ); + } + + + /* Private Functions */ + + function verify( + bytes32 _digest, + bytes32 _r, + bytes32 _s, + uint8 _v + ) + private + view + { + require( + ecrecover(_digest, _v, _r, _s) == recoveryOwner, + "Invalid signature for recovery owner." + ); + } + + function hashEIP712TypedData( + bytes32 _structHash + ) + private + view + returns (bytes32 eip712TypedDataHash_) + { + eip712TypedDataHash_ = keccak256( + abi.encodePacked( + "\x19", + "\x01", + domainSeparator, + _structHash + ) + ); + } + + function hashRecovery( + bytes32 _structTypeHash, + address _prevOwner, + address _oldOwner, + address _newOwner + ) + private + view + returns (bytes32 recoveryHash_ ) + { + bytes32 recoveryStructHash = keccak256( + abi.encode( + _structTypeHash, + _prevOwner, + _oldOwner, + _newOwner + ) + ); + + recoveryHash_ = hashEIP712TypedData(recoveryStructHash); + } + + function hashResetRecoveryOwner( + address _oldRecoverySigner, + address _newRecoverySigner + ) + private + view + returns (bytes32 resetRecoveryOwnerHash_ ) + { + bytes32 resetRecoveryOwnerStructHash = keccak256( + abi.encode( + RESET_RECOVERY_OWNER_STRUCT_TYPEHASH, + _oldRecoverySigner, + _newRecoverySigner + ) + ); + + resetRecoveryOwnerHash_ = hashEIP712TypedData( + resetRecoveryOwnerStructHash + ); + } +} \ No newline at end of file diff --git a/contracts/gnosis_safe_modules/GnosisSafeModule.sol b/contracts/gnosis_safe_modules/GnosisSafeModule.sol new file mode 100644 index 0000000..cf830a8 --- /dev/null +++ b/contracts/gnosis_safe_modules/GnosisSafeModule.sol @@ -0,0 +1,50 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "../proxies/MasterCopyNonUpgradable.sol"; +import "./GnosisSafeModuleManagerInterface.sol"; + +/** + * @title Contract contains the required, public interface by + * gnosis/safe-contracts/contracts/base/Module.sol + * + * @dev Instead of inheriting from + * gnosis/safe-contracts/contracts/base/Module.sol and pulling all needed + * contracts by Module.sol into openst-contracts building process, + * we define this contract, that contains only required, public interface + * of Module.sol. + * + * The current contract is non-upgradable in contrast to GnosisSafe + * Module contract. + */ +contract GnosisSafeModule is MasterCopyNonUpgradable +{ + /* Storage */ + + GnosisSafeModuleManagerInterface public manager; + + + /* Modifiers */ + + modifier authorized() { + require( + msg.sender == address(manager), + "Method can only be called from manager" + ); + + _; + } +} \ No newline at end of file diff --git a/contracts/gnosis_safe_modules/GnosisSafeModuleManagerInterface.sol b/contracts/gnosis_safe_modules/GnosisSafeModuleManagerInterface.sol new file mode 100644 index 0000000..0cc9dc5 --- /dev/null +++ b/contracts/gnosis_safe_modules/GnosisSafeModuleManagerInterface.sol @@ -0,0 +1,62 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @title Contract contains the required, public interface by + * gnosis/safe-contracts/contracts/base/ModuleManager.sol + * + * @dev Instead of inheriting from + * gnosis/safe-contracts/contracts/base/ModuleManager.sol and pulling all + * needed contracts by Module.sol into openst-contracts building process, + * we define this contract, that contains only required, public interface + * of ModuleManager.sol. + */ +interface GnosisSafeModuleManagerInterface +{ + + /* Enums */ + + /** + * @dev This enum mimics the "Operation" enum defined within: + * gnosis/safe-contracts/contract/common/Enum.sol + */ + enum Operation { + Call, + DelegateCall, + Create + } + + + /* External Functions */ + + /** + * @dev Allows a module to execute a Safe transaction without any + * further confirmations. + * + * @param _to Destination address of module transaction. + * @param _value Ether value of module transaction. + * @param _data Data payload of module transaction. + * @param _operation Operation type of module transaction. + */ + function execTransactionFromModule( + address _to, + uint256 _value, + bytes calldata _data, + Operation _operation + ) + external + returns (bool success_); +} \ No newline at end of file diff --git a/contracts/organization/Organization.sol b/contracts/organization/Organization.sol new file mode 100644 index 0000000..2d10dd3 --- /dev/null +++ b/contracts/organization/Organization.sol @@ -0,0 +1,353 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ---------------------------------------------------------------------------- +// +// http://www.simpletoken.org/ +// +// ---------------------------------------------------------------------------- + +import "./OrganizationInterface.sol"; + +/** + * @title Organization contract handles an organization and its workers. + * + * @notice The organization represents an entity that manages other contracts + * and therefore the `Organization.sol` contract holds all the keys + * required to administer the other contracts. + * This contract supports the notion of an "admin" that can act on + * behalf of the organization. When seen from the outside by consumers + * of the `OrganizationInterface`, a notion of an admin does not exist. + */ +contract Organization is OrganizationInterface { + + /* Events */ + + /** Emitted when a current owner initiates a change of ownership. */ + event OwnershipTransferInitiated( + address indexed proposedOwner, + address currentOwner + ); + + /** Emitted when a new owner accepts the ownership transfer. */ + event OwnershipTransferCompleted(address newOwner, address previousOwner); + + /** Emitted whenever an owner or admin changes the address of the admin. */ + event AdminAddressChanged(address indexed newAdmin, address previousAdmin); + + /** Emitted when a worker address was set. */ + event WorkerSet(address indexed worker, uint256 expirationHeight); + + /** Emitted when a worker address is deleted from the contract. */ + event WorkerUnset(address worker); + + + /* Storage */ + + /** Address for which private key will be owned by the organization. */ + address public owner; + + /** + * Proposed Owner is the newly proposed address that was proposed by the + * current owner for ownership transfer. + */ + address public proposedOwner; + + /** + * Admin address set by owner to facilitate operations of an economy on + * behalf of the owner. + * While this contract includes details that regard the admin, e.g. a + * modifier, when looking at the `OrganizationInterface`, the existence of + * an admin is a concrete implementation detail and not known to the + * consumers of the interface. + */ + address public admin; + + /** + * Map of whitelisted worker addresses to their expiration block height. + */ + mapping(address => uint256) public workers; + + + /* Modifiers */ + + /** + * onlyOwner functions can only be called from the address that is + * registered as the owner. + */ + modifier onlyOwner() { + require( + msg.sender == owner, + "Only owner is allowed to call this method." + ); + + _; + } + + /** + * onlyOwnerOrAdmin functions can only be called from an address that is + * registered as owner or as admin. + */ + modifier onlyOwnerOrAdmin() { + require( + msg.sender == owner || msg.sender == admin, + "Only owner and admin are allowed to call this method." + ); + + _; + } + + + /* Constructor */ + + /** + * @notice Creates a new organization. When you first initialize the + * organization, you can specify owner, admin, and workers. The + * owner is mandatory as it will be the only address able to make + * all later changes. An admin and workers can be added at + * construction or they can be set by the owner later. + * + * @param _owner The address that shall be registered as the owner of the + * organization. + * @param _admin The address that shall be registered as the admin of the + * organization. Can be address(0) if no admin is desired. + * @param _workers An array of initial worker addresses. Can be an empty + * array if no workers are desired or known at construction. + * @param _expirationHeight If any workers are given, this will be the + * block height at which they expire. + */ + constructor( + address _owner, + address _admin, + address[] memory _workers, + uint256 _expirationHeight + ) + public + { + require( + _owner != address(0), + "The owner must not be the zero address." + ); + + owner = _owner; + admin = _admin; + + for(uint256 i = 0; i < _workers.length; i++) { + setWorkerInternal(_workers[i], _expirationHeight); + } + } + + + /* External Functions */ + + /** + * @notice Proposes a new owner of this contract. Ownership will not be + * transferred until the new, proposed owner accepts the proposal. + * Allows resetting of proposed owner to address(0). + * + * @param _proposedOwner Proposed owner address. + * + * @return success_ True on successful execution. + */ + function initiateOwnershipTransfer( + address _proposedOwner + ) + external + onlyOwner + returns (bool success_) + { + require( + _proposedOwner != owner, + "Proposed owner address can't be current owner address." + ); + + proposedOwner = _proposedOwner; + + emit OwnershipTransferInitiated(_proposedOwner, owner); + + success_ = true; + } + + /** + * @notice Complete ownership transfer to proposed owner. Must be called by + * the proposed owner. + * + * @return success_ True on successful execution. + */ + function completeOwnershipTransfer() external returns (bool success_) + { + require( + msg.sender == proposedOwner, + "Caller is not proposed owner address." + ); + + emit OwnershipTransferCompleted(proposedOwner, owner); + + owner = proposedOwner; + proposedOwner = address(0); + + success_ = true; + } + + /** + * @notice Sets the admin address. Can only be called by owner or current + * admin. If called by the current admin, adminship is transferred + * to the given address immediately. + * It is discouraged to set the admin address to be the same as the + * address of the owner. The point of the admin is to act on behalf + * of the organization without requiring the possibly very safely + * stored owner key(s). + * Admin can be set to `address(0)` if no admin is desired. + * + * @param _admin Admin address to be set. + * + * @return success_ True on successful execution. + */ + function setAdmin( + address _admin + ) + external + onlyOwnerOrAdmin + returns (bool success_) + { + /* + * If the address does not change, the call is considered a success, + * but we don't need to emit an event as it did not actually change. + */ + if (admin != _admin) { + emit AdminAddressChanged(_admin, admin); + admin = _admin; + } + + success_ = true; + } + + /** + * @notice Sets worker and its expiration block height. + * Admin/Owner has the flexibility to extend/reduce worker + * expiration height. This way, a worker activation/deactivation + * can be controlled without adding/removing worker keys. + * + * @param _worker Worker address to be added. + * @param _expirationHeight Expiration block height of worker. + * + * @return remainingBlocks_ Remaining number of blocks for which worker is + * active. + */ + function setWorker( + address _worker, + uint256 _expirationHeight + ) + external + onlyOwnerOrAdmin + { + setWorkerInternal(_worker, _expirationHeight); + } + + /** + * @notice Removes a worker. + * + * @param _worker Worker address to be removed. + * + * @return isUnset_ True if the worker existed else returns false. + */ + function unsetWorker( + address _worker + ) + external + onlyOwnerOrAdmin + returns (bool isUnset_) + { + if (workers[_worker] > 0) { + delete workers[_worker]; + emit WorkerUnset(_worker); + + isUnset_ = true; + } + } + + /** + * @notice Checks if an address is currently registered as the organization. + * + * @dev It is an implementation detail of this contract that the admin can + * act on behalf of the organization. To the outside, an "admin" + * doesn't exist. See also the `admin` storage variable. + * + * @param _organization Address to check. + * + * @return isOrganization_ True if the given address represents the + * organization. Returns false otherwise. + */ + function isOrganization( + address _organization + ) + external + view + returns (bool isOrganization_) + { + isOrganization_ = _organization == owner || _organization == admin; + } + + /** + * @notice Checks if an address is currently registered as an active worker. + * + * @param _worker Address to check. + * + * @return isWorker_ True if the worker is already added and expiration + * height is more than or equal to current block number. + * Returns false otherwise. + */ + function isWorker(address _worker) external view returns (bool isWorker_) + { + isWorker_ = workers[_worker] > block.number; + } + + + /* Private Functions */ + + /** + * @notice Sets worker and its expiration block height. If the worker + * already exists, then its expiration height will be overwritten + * with the given one. + * + * @param _worker Worker address to be added. + * @param _expirationHeight Expiration block height of worker. + * + * @return remainingBlocks_ Remaining number of blocks for which worker is + * active. + */ + function setWorkerInternal( + address _worker, + uint256 _expirationHeight + ) + private + { + require( + _worker != address(0), + "Worker address cannot be null." + ); + + require( + _expirationHeight > block.number, + "Expiration height must be in the future." + ); + + workers[_worker] = _expirationHeight; + + emit WorkerSet(_worker, _expirationHeight); + } + +} diff --git a/contracts/organization/OrganizationInterface.sol b/contracts/organization/OrganizationInterface.sol new file mode 100644 index 0000000..ed7ecca --- /dev/null +++ b/contracts/organization/OrganizationInterface.sol @@ -0,0 +1,54 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ---------------------------------------------------------------------------- +// +// http://www.simpletoken.org/ +// +// ---------------------------------------------------------------------------- + +/** + * @title OrganizationInterface provides methods to check if an address is + * currently registered as an active participant in the organization. + */ +interface OrganizationInterface { + + /** + * @notice Checks if an address is currently registered as the organization. + * + * @param _organization Address to check. + * + * @return isOrganization_ True if the given address represents the + * organization. Returns false otherwise. + */ + function isOrganization( + address _organization + ) + external + view + returns (bool isOrganization_); + + /** + * @notice Checks if an address is currently registered as an active worker. + * + * @param _worker Address to check. + * + * @return isWorker_ True if the given address is a registered, active + * worker. Returns false otherwise. + */ + function isWorker(address _worker) external view returns (bool isWorker_); + +} diff --git a/contracts/organization/Organized.sol b/contracts/organization/Organized.sol new file mode 100644 index 0000000..f759b01 --- /dev/null +++ b/contracts/organization/Organized.sol @@ -0,0 +1,79 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ---------------------------------------------------------------------------- +// +// http://www.simpletoken.org/ +// +// ---------------------------------------------------------------------------- + +import "./OrganizationInterface.sol"; + +/** + * @title Organized contract. + * + * @notice The Organized contract facilitates integration of + * organization administration keys with different contracts. + */ +contract Organized { + + + /* Storage */ + + /** Organization which holds all the keys needed to administer the economy. */ + OrganizationInterface public organization; + + + /* Modifiers */ + + modifier onlyOrganization() + { + require( + organization.isOrganization(msg.sender), + "Only the organization is allowed to call this method." + ); + + _; + } + + modifier onlyWorker() + { + require( + organization.isWorker(msg.sender), + "Only whitelisted workers are allowed to call this method." + ); + + _; + } + + + /* Constructor */ + + /** + * @notice Sets the address of the organization contract. + * + * @param _organization A contract that manages worker keys. + */ + constructor(OrganizationInterface _organization) public { + require( + address(_organization) != address(0), + "Organization contract address must not be zero." + ); + + organization = _organization; + } + +} diff --git a/contracts/proxies/MasterCopyNonUpgradable.sol b/contracts/proxies/MasterCopyNonUpgradable.sol new file mode 100644 index 0000000..8d6f76b --- /dev/null +++ b/contracts/proxies/MasterCopyNonUpgradable.sol @@ -0,0 +1,35 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +contract MasterCopyNonUpgradable { + + /* Storage */ + + /** + * @dev This storage variable *MUST* be the first storage element + * for this contract. + * + * A contract acting as a master copy for a proxy contract + * inherits from this contract. In inherited contracts list, this + * contract *MUST* be the first one. This would assure that + * the storage variable is always the first storage element for + * the inherited contract. + * + * The proxy is applied to save gas during deployment, and importantly + * the proxy is not upgradable. + */ + address reservedStorageSlotForProxy; +} \ No newline at end of file diff --git a/contracts/proxies/Proxy.sol b/contracts/proxies/Proxy.sol new file mode 100644 index 0000000..7f22237 --- /dev/null +++ b/contracts/proxies/Proxy.sol @@ -0,0 +1,63 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @title Proxy contract that delegates calls to the master contract. + * + * @notice Allows to create a proxy from a master copy of any contract. + * An important requirement on a master contract is to have a reserved + * slot of an address type, within its storage, in a first position. + * For an example, please, see TokenHolder contract. + */ +contract Proxy { + + /** + * @dev THIS STORAGE VARIABLE *MUST* BE ALWAYS THE FIRST STORAGE + * ELEMENT FOR THIS CONTRACT. + */ + address public masterCopy; + + constructor(address _masterCopy) + public + { + require( + _masterCopy != address(0), + "Master copy address is null." + ); + + masterCopy = _masterCopy; + } + + /** + * @dev Fallback function allowing to perform a delegatecall to the given + * implementation. The function will return whatever the + * implementation call returns. + */ + function () + external + payable + { + // solium-disable-next-line security/no-inline-assembly + assembly { + let masterCopy := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff) + calldatacopy(0, 0, calldatasize()) + let success := delegatecall(gas, masterCopy, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + if eq(success, 0) { revert(0, returndatasize()) } + return(0, returndatasize()) + } + } +} diff --git a/contracts/proxies/ProxyFactory.sol b/contracts/proxies/ProxyFactory.sol new file mode 100644 index 0000000..acb23ac --- /dev/null +++ b/contracts/proxies/ProxyFactory.sol @@ -0,0 +1,70 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "./Proxy.sol"; + +/** + * @title Allows to create new proxy contact and execute a message call to the + * new proxy within one transaction. + * + * @dev This contract is adapted from a ProxyFactory contract implementation + * from https://github.com/gnosis/safe-contracts. + */ +contract ProxyFactory { + + /** + * @notice The event is emitted from the ProxyFactory::createProxy + * function on success. + * + * @param _proxy A newly created proxy. + */ + event ProxyCreated(Proxy _proxy); + + /** + * @dev Allows to create new proxy contact and execute a message call (if + * a message data is non-empty) to the new proxy within one + * transaction. + * + * Function requires: + * - The specified master copy address is not null. + * + * @param _masterCopy Address of a master copy. + * @param _data Payload for message call sent to new proxy contract. + * Executes a message call to this parameter if it's not empty. + * + * @return proxy_ A newly created proxy. + */ + function createProxy(address _masterCopy, bytes memory _data) + public + returns (Proxy proxy_) + { + require( + _masterCopy != address(0), + "Master copy address is null." + ); + + proxy_ = new Proxy(_masterCopy); + if (_data.length > 0) + // solium-disable-next-line security/no-inline-assembly + assembly { + if eq(call(gas, proxy_, 0, add(_data, 0x20), mload(_data), 0, 0), 0) { + revert(0, 0) + } + } + + emit ProxyCreated(proxy_); + } +} diff --git a/contracts/proxies/UserWalletFactory.sol b/contracts/proxies/UserWalletFactory.sol new file mode 100644 index 0000000..cd76e9a --- /dev/null +++ b/contracts/proxies/UserWalletFactory.sol @@ -0,0 +1,125 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "./Proxy.sol"; + + +/** + * @title Allows to create a new gnosis safe proxy and executes a + * message call to the newly created proxy. Afterwards, in the same + * transaction, creates a new token holder proxy by specifying + * as an owner the newly created gnosis safe proxy contract. + */ +contract UserWalletFactory { + + /* Events */ + + /** + * @notice The event is emitted the from UserWalletFactory::createUserWallet + * function on success. + * + * @param _gnosisSafeProxy A newly created gnosis safe's proxy. + * @param _tokenHolderProxy A newly created token holder's proxy. + */ + event UserWalletCreated( + Proxy _gnosisSafeProxy, + Proxy _tokenHolderProxy + ); + + + /* Constants */ + + /** The callprefix of the TokenHolder::setup function. */ + bytes4 public constant TOKENHOLDER_SETUP_CALLPREFIX = bytes4( + keccak256( + "setup(address,address,address,address[],uint256[],uint256[])" + ) + ); + + + /* External Functions */ + + /** + * @notice Create a new gnosis safe proxy and executes a + * message call to the newly created proxy. Afterwards, in the same + * transaction, creates a new token holder proxy by specifying + * as an owner the newly created gnosis safe proxy contract. + * + * @param _gnosisSafeMasterCopy The address of a master copy of gnosis safe. + * @param _gnosisSafeData The message data to be called on a newly created + * gnosis safe proxy. + * @param _tokenHolderMasterCopy The address of a master copy of token + * holder. + * @param _token The address of the economy token. + * @param _tokenRules The address of the token rules. + * @param _sessionKeys Session key addresses to authorize. + * @param _sessionKeysSpendingLimits Session keys' spending limits. + * @param _sessionKeysExpirationHeights Session keys' expiration heights. + * + * @return gnosisSafeProxy_ A newly created gnosis safe's proxy address. + * @return tokenHolderProxy_ A newly created token holder's proxy address. + */ + function createUserWallet( + address _gnosisSafeMasterCopy, + bytes calldata _gnosisSafeData, + address _tokenHolderMasterCopy, + address _token, + address _tokenRules, + address[] calldata _sessionKeys, + uint256[] calldata _sessionKeysSpendingLimits, + uint256[] calldata _sessionKeysExpirationHeights + ) + external + returns (Proxy gnosisSafeProxy_, Proxy tokenHolderProxy_) + { + gnosisSafeProxy_ = new Proxy(_gnosisSafeMasterCopy); + callProxyData(gnosisSafeProxy_, _gnosisSafeData); + + tokenHolderProxy_ = new Proxy(_tokenHolderMasterCopy); + + bytes memory tokenHolderData = abi.encodeWithSelector( + TOKENHOLDER_SETUP_CALLPREFIX, + _token, + _tokenRules, + gnosisSafeProxy_, + _sessionKeys, + _sessionKeysSpendingLimits, + _sessionKeysExpirationHeights + ); + callProxyData(tokenHolderProxy_, tokenHolderData); + + emit UserWalletCreated(gnosisSafeProxy_, tokenHolderProxy_); + } + + + /* Private Functions */ + + function callProxyData( + Proxy _proxy, + bytes memory _data + ) + private + { + if (_data.length > 0) { + // solium-disable-next-line security/no-inline-assembly + assembly { + if eq(call(gas, _proxy, 0, add(_data, 0x20), mload(_data), 0, 0), 0) { + revert(0, 0) + } + } + } + } +} diff --git a/contracts/rules/PriceOracleInterface.sol b/contracts/rules/PriceOracleInterface.sol new file mode 100644 index 0000000..50a07af --- /dev/null +++ b/contracts/rules/PriceOracleInterface.sol @@ -0,0 +1,70 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +interface PriceOracleInterface { + + /* Events */ + + event PriceUpdated( + uint256 _price + ); + + + /* External Functions */ + + /** + * @notice Returns base currency code. + * + * @dev Base currency code is not according to ISO 4217 or other standard. + */ + function baseCurrency() + external + view + returns (bytes3); + + /** + * @notice Returns quote currency code. + * + * @dev Quote currency code is not according to ISO 4217 or other standard. + */ + function quoteCurrency() + external + view + returns (bytes3); + + /** + * @notice Returns quote currency decimals. + */ + function decimals() + external + view + returns (uint8); + + /** + * @notice Returns an amount of the quote currency (see decimals()) needed + * to purchase one unit of the base currency. + * + * @dev Function throws an exception if the price is invalid, for example, + * was not set, or became outdated, etc. + * + * @return An amount of the quote currency needed to purchase one unit of + * the base base currency. + */ + function getPrice() + external + view + returns (uint256); +} \ No newline at end of file diff --git a/contracts/rules/PricerRule.sol b/contracts/rules/PricerRule.sol new file mode 100644 index 0000000..c14716a --- /dev/null +++ b/contracts/rules/PricerRule.sol @@ -0,0 +1,439 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "./PriceOracleInterface.sol"; +import "../token/EIP20TokenInterface.sol"; +import "../token/TokenRules.sol"; +import "../external/SafeMath.sol"; + + +contract PricerRule is Organized { + + /* Usings */ + + using SafeMath for uint256; + + + /* Events */ + + event PriceOracleAdded( + address _priceOracle + ); + + event PriceOracleRemoved( + address _priceOracle + ); + + event AcceptanceMarginSet( + bytes3 _quoteCurrencyCode, + uint256 _acceptanceMargin + ); + + event AcceptanceMarginRemoved( + bytes3 _quoteCurrencyCode + ); + + + /* Storage */ + + /** + * @dev The code of the base currency of the economy. + */ + bytes3 public baseCurrencyCode; + + /** + * @dev EIP20 token address of the economy. + */ + EIP20TokenInterface public eip20Token; + + /** + * @dev Conversion rate from the base currency to the economy token. + */ + uint256 public conversionRateFromBaseCurrencyToToken; + + /** + * @dev Conversion rate decimals from the base currency to the + * economy token. + */ + uint256 public conversionRateDecimalsFromBaseCurrencyToToken; + + /** + * @dev Token decimals of EIP20Token. Since Pay method can be called multiple + * times, for optimization it's fetched from eip20Token and stored. + */ + uint8 public tokenDecimals; + + /** + * @dev Required decimals for price oracles. + */ + uint8 public requiredPriceOracleDecimals; + + /** + * @dev Token rules address of the economy. + */ + TokenRules public tokenRules; + + /** + * @dev Mapping from pay-currency code to corresponding price oracle. + * The mapped price oracle's base currency code should be the economy + * base currency code (see baseCurrencyCode storage variable) + * and the quote currency code should be the corresponding pay-currency + * code. + */ + mapping(bytes3 => PriceOracleInterface) public baseCurrencyPriceOracles; + + /** + * @dev Mapping from pay-currency code to price difference accepatance + * margin. During a pay operation an intended price for + * pay-currency is presented, that is checked against current price of + * pay-currenency wrt stored acceptance margin. + */ + mapping(bytes3 => uint256) public baseCurrencyPriceAcceptanceMargins; + + + /* Special Functions */ + + /** + * @notice Constructs a new pricer object. + * + * @dev Function requires: + * - The economy token address is not null. + * - The base currency code is not empty. + * - Conversion rate from the base currency to the token is not 0. + * - The economy token rules address is not null. + * + * @param _organization Organization address. + * @param _eip20Token The economy token address. + * @param _baseCurrencyCode The economy base currency code. + * @param _conversionRate The conversion rate from the economy base currency + * to the token. + * @param _conversionRateDecimals The conversion rate's decimals from the + * economy base currency to the token. + * @param _requiredPriceOracleDecimals Required decimals for price oracles. + * @param _tokenRules The economy token rules address. + */ + constructor( + OrganizationInterface _organization, + address _eip20Token, + bytes3 _baseCurrencyCode, + uint256 _conversionRate, + uint8 _conversionRateDecimals, + uint8 _requiredPriceOracleDecimals, + address _tokenRules + ) + Organized(_organization) + public + { + require(_eip20Token != address(0), "Token address is null."); + + require( + _baseCurrencyCode != bytes3(0), + "Base currency code is null." + ); + + require( + _conversionRate != 0, + "Conversion rate from the base currency to the token is 0." + ); + + require( + _tokenRules != address(0), + "Token rules address is null." + ); + + eip20Token = EIP20TokenInterface(_eip20Token); + + tokenDecimals = EIP20TokenInterface(_eip20Token).decimals(); + + baseCurrencyCode = _baseCurrencyCode; + + conversionRateFromBaseCurrencyToToken = _conversionRate; + + conversionRateDecimalsFromBaseCurrencyToToken = _conversionRateDecimals; + + requiredPriceOracleDecimals = _requiredPriceOracleDecimals; + + tokenRules = TokenRules(_tokenRules); + } + + + /* External Functions */ + + /** + * @notice Transfers from the msg.sender account to the specified addresses + * an amount of the economy tokens equivalent (after conversion) to + * the specified amounts in pay currency. + * + * @dev Function requires: + * - From address is not null. + * - The lengths of arrays of beneficiaries and amounts are equal. + * - The intended price of the base currency in the pay currency + * wrt the current price is in the registered acceptance margin. + * + * @param _payCurrencyCode Currency code of the specified amounts. + * @param _baseCurrencyIntendedPrice The intended price of the base currency + * used during conversion within function. + */ + function pay( + address _from, + address[] calldata _toList, + uint256[] calldata _amountList, + bytes3 _payCurrencyCode, + uint256 _baseCurrencyIntendedPrice + ) + external + { + require( + _from != address(0), + "From address is null." + ); + + require( + _toList.length == _amountList.length, + "'to' and 'amount' transfer arrays' lengths are not equal." + ); + + if (_toList.length == 0) { + return; + } + + uint256 baseCurrencyCurrentPrice = baseCurrencyPrice( + _payCurrencyCode + ); + + require( + baseCurrencyCurrentPrice != 0, + "Base currency price in pay currency is 0." + ); + + require( + isPriceInRange( + _baseCurrencyIntendedPrice, + baseCurrencyCurrentPrice, + baseCurrencyPriceAcceptanceMargins[_payCurrencyCode] + ), + "Intended price is not in the acceptable margin wrt current price." + ); + + uint256[] memory convertedAmounts = new uint256[](_amountList.length); + + for(uint256 i = 0; i < _amountList.length; ++i) { + convertedAmounts[i] = convertPayCurrencyToToken( + baseCurrencyCurrentPrice, + _amountList[i] + ); + } + + tokenRules.executeTransfers( + _from, + _toList, + convertedAmounts + ); + } + + /** + * @notice Adds a new price oracle. + * + * @dev Function requires: + * - Only organization's workers are allowed to call the function. + * - The proposed price oracle's address is not null. + * - The proposed price oracle's base currency code is + * equal to the economy base currency code specified in this + * contract constructor. + * - The proposed price oracle does not exist. + * - The proposed price oracle decimals number is equal to + * the contract required price oracle decimals number. + * + * @param _priceOracle The proposed price oracle. + */ + function addPriceOracle( + PriceOracleInterface _priceOracle + ) + external + onlyWorker + { + require( + address(_priceOracle) != address(0), + "Price oracle address is null." + ); + + require( + _priceOracle.decimals() == requiredPriceOracleDecimals, + "Price oracle decimals number is difference from the required one." + ); + + bytes3 payCurrencyCode = _priceOracle.quoteCurrency(); + + require( + address(baseCurrencyPriceOracles[payCurrencyCode]) == address(0), + "Price oracle already exists." + ); + + require( + _priceOracle.baseCurrency() == baseCurrencyCode, + "Price oracle's base currency code does not match." + ); + + baseCurrencyPriceOracles[payCurrencyCode] = _priceOracle; + + emit PriceOracleAdded(address(_priceOracle)); + } + + /** + * @notice Removes the price oracle for the specified pay currency code. + * + * @dev Function requires: + * - Only organization's workers are allowed to call the function. + * - Price oracle matching with the specified pay currency code + * exists. + */ + function removePriceOracle( + bytes3 _payCurrencyCode + ) + external + onlyWorker + { + PriceOracleInterface priceOracle = baseCurrencyPriceOracles[ + _payCurrencyCode + ]; + + require( + address(priceOracle) != address(0), + "Price oracle to remove does not exist." + ); + + delete baseCurrencyPriceOracles[_payCurrencyCode]; + + emit PriceOracleRemoved(address(priceOracle)); + } + + /** + * @notice Sets an acceptance margin for the base currency price per pay + * currency. + * + * @dev Function requires: + * - Only organization's workers are allowed to call the function. + * - The specified pay currency code is not null. + */ + function setAcceptanceMargin( + bytes3 _payCurrencyCode, + uint256 _acceptanceMargin + ) + external + onlyWorker + { + require( + _payCurrencyCode != bytes3(0), + "Pay currency code is null." + ); + + baseCurrencyPriceAcceptanceMargins[_payCurrencyCode] = _acceptanceMargin; + + emit AcceptanceMarginSet( + _payCurrencyCode, + _acceptanceMargin + ); + } + + /** + * @notice Removes an acceptance margin of the base currency price in the + * specified pay currency. + * + * @dev Function requires: + * - Only organization's workers are allowed to call the function. + * - The specified pay currency code is not null. + */ + function removeAcceptanceMargin( + bytes3 _payCurrencyCode + ) + external + onlyWorker + { + require( + _payCurrencyCode != bytes3(0), + "Pay currency code is null." + ); + + delete baseCurrencyPriceAcceptanceMargins[_payCurrencyCode]; + + emit AcceptanceMarginRemoved(_payCurrencyCode); + } + + + /* Private Functions */ + + function baseCurrencyPrice( + bytes3 _payCurrencyCode + ) + private + view + returns(uint256) + { + PriceOracleInterface priceOracle = baseCurrencyPriceOracles[ + _payCurrencyCode + ]; + + require( + address(priceOracle) != address(0), + "Price oracle for the specified currency code does not exist." + ); + + return priceOracle.getPrice(); + } + + function convertPayCurrencyToToken( + uint256 _baseCurrencyPriceInPayCurrency, + uint256 _payCurrencyAmount + ) + private + view + returns (uint256) + { + return _payCurrencyAmount + .mul( + conversionRateFromBaseCurrencyToToken + ) + .mul( + 10 ** uint256(tokenDecimals) + ) + .div( + 10 ** conversionRateDecimalsFromBaseCurrencyToToken + ) + .div( + _baseCurrencyPriceInPayCurrency + ); + } + + function isPriceInRange( + uint256 _intendedPrice, + uint256 _currentPrice, + uint256 _acceptanceMargin + ) + private + pure + returns(bool isInRange) + { + uint256 diff = 0; + if (_currentPrice > _intendedPrice) { + diff = _currentPrice.sub(_intendedPrice); + } else { + diff = _intendedPrice.sub(_currentPrice); + } + + return diff <= _acceptanceMargin; + } + +} \ No newline at end of file diff --git a/contracts/test/multisigwallet/MultiSigWalletDouble.sol b/contracts/test/multisigwallet/MultiSigWalletDouble.sol deleted file mode 100644 index a5b9c5c..0000000 --- a/contracts/test/multisigwallet/MultiSigWalletDouble.sol +++ /dev/null @@ -1,110 +0,0 @@ -pragma solidity ^0.4.23; - -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// ---------------------------------------------------------------------------- -// Utility Chain: MultiSigWallet -// -// http://www.simpletoken.org/ -// -// ---------------------------------------------------------------------------- - -import "../../MultiSigWallet.sol"; - - -/** - * @dev Contract introduces submitFoo() function which behaves like - * other submit* functions (submitAddWallet, etc). - * The foo() function fails based on storage variable which could be - * set/unset. This way the test of transaction execution of - * MultisigWallet could imitate the non-happy-path. - */ -contract MultiSigWalletDouble is MultiSigWallet { - - /* Usings */ - - using SafeMath for uint256; - - - /* Constants */ - - bytes4 constant public FOO_CALLPREFIX = bytes4( - keccak256("foo()") - ); - - - /* Storage */ - - bool fooThrows; - - - /* Special Functions */ - - constructor(address[] _wallets, uint256 _required) - MultiSigWallet(_wallets, _required) - public - { - fooThrows = true; - } - - - /* External Functions */ - - /** - * @dev Submits a foo() function that fails based on storage variable - * fooThrows. Used for testing non-happy path of - * MultisigWallet executeTransaction. - * - * @return Newly created transaction id. - */ - function submitFoo() - external - onlyWallet - returns (uint256 transactionID_) - { - transactionID_ = addTransaction( - address(this), - abi.encodeWithSelector(FOO_CALLPREFIX) - ); - - confirmTransaction(transactionID_); - } - - - /* Public Functions */ - - function makeFooThrow() - public - { - fooThrows = true; - } - - function makeFooNotThrow() - public - { - fooThrows = false; - } - - function foo() - public - view - onlyMultisig - { - if ( fooThrows ) { - revert("Foo is set to throw."); - } - } - -} \ No newline at end of file diff --git a/contracts/test/token_rules/TokenRulesFailingGlobalConstraint.sol b/contracts/test/token_rules/TokenRulesFailingGlobalConstraint.sol deleted file mode 100644 index a30afe2..0000000 --- a/contracts/test/token_rules/TokenRulesFailingGlobalConstraint.sol +++ /dev/null @@ -1,38 +0,0 @@ -pragma solidity ^0.4.23; - -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import "../../GlobalConstraintInterface.sol"; - -contract TokenRulesFailingGlobalConstraint is GlobalConstraintInterface -{ - /* External Functions */ - - function check( - address _from, - address[] _transfersTo, - uint256[] _transfersAmount - ) - external - view - returns (bool) - { - _from; - _transfersTo; - _transfersAmount; - - return false; - } -} diff --git a/contracts/test_doubles/unit_tests/EIP20TokenFake.sol b/contracts/test_doubles/unit_tests/EIP20TokenFake.sol new file mode 100644 index 0000000..a6277a3 --- /dev/null +++ b/contracts/test_doubles/unit_tests/EIP20TokenFake.sol @@ -0,0 +1,231 @@ +/* solhint-disable-next-line compiler-fixed */ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "../../token/EIP20TokenInterface.sol"; +import "../../external/SafeMath.sol"; + + +contract EIP20TokenFake is EIP20TokenInterface { + + /* Usings */ + + using SafeMath for uint256; + + + /* Storage */ + + uint256 tokenTotalSupply; + + string private tokenName; + + string private tokenSymbol; + + uint8 private tokenDecimals; + + mapping(address => uint256) balances; + mapping(address => mapping (address => uint256)) allowed; + + + /* Special Functions */ + + /** + * @notice Constructs an EIP20 token based on input parameters. + * + * @param _symbol Symbol of the token. + * @param _name Name of the token. + * @param _decimals Decimal places of the token. + */ + constructor(string memory _symbol, string memory _name, uint8 _decimals) + public + { + tokenTotalSupply = 0; + + tokenSymbol = _symbol; + tokenName = _name; + tokenDecimals = _decimals; + } + + + /* External Functions */ + + /** + * @dev Implements EIP20TokenInterface::name() function. + * + * @return Name of the EIP20 token. + */ + function name() external view returns (string memory) { + return tokenName; + } + + /** + * @dev Implements EIP20TokenInterface::symbol() function. + * + * @return Symbol of the EIP20 token. + */ + function symbol() external view returns (string memory) { + return tokenSymbol; + } + + /** + * @dev Implements EIP20TokenInterface::decimals() function. + * + * @return Decimal places of the EIP20 token. + */ + function decimals() external view returns (uint8) { + return tokenDecimals; + } + + /** + * @dev Implements EIP20TokenInterface::totalSupply() function. + * Function is only overriden and not implemented as it's not used + * during testing. + */ + function totalSupply() external view returns (uint256) { + return tokenTotalSupply; + } + + /** + * @notice Returns balance of the owner account. + * + * @param _owner Address of the owner account. + * + * @return Account balance of the owner account. + */ + function balanceOf(address _owner) external view returns (uint256) { + return balances[_owner]; + } + + /** + * @notice Returns allowance of a spender for the owner account. + * + * @param _owner Address of the owner account. + * @param _spender Address of the spender account. + * + * @return remaining_ Remaining allowance for the spender to spend from + * the owner's account. + */ + function allowance(address _owner, address _spender) + external + view + returns (uint256 remaining_) + { + remaining_ = allowed[_owner][_spender]; + } + + /** + * @notice Transfers an amount from msg.sender account. + * + * @dev Fires the "Transfer" event, throws if, _from account does not + * have enough tokens to spend. + * + * @param _to Address to which tokens are transferred. + * @param _value Amount of tokens to be transferred. + * + * @return True for a successful transfer, false otherwise. + */ + function transfer(address _to, uint256 _value) + external + returns (bool success) + { + // According to the EIP20 spec, "transfers of 0 values MUST be treated + // as normal transfers and fire the Transfer event". + // Also, should throw if not enough balance. + // This is taken care of by SafeMath. + + balances[msg.sender] = balances[msg.sender].sub(_value); + balances[_to] = balances[_to].add(_value); + + emit Transfer(msg.sender, _to, _value); + + return true; + } + + /** + * @notice Transfer the specified amount between accounts. + * + * @dev Allows a contract to transfer tokens on behalf of _from address + * to _to address, the function caller has to be pre-authorized for + * multiple transfers up to the total of _value amount by the + * _from address. + * + * @param _from Address from which tokens are transferred. + * @param _to Address to which tokens are transferred. + * @param _value Amount of tokens transferred. + * + * @return True for a successful transfer, false otherwise. + */ + function transferFrom(address _from, address _to, uint256 _value) + external + returns (bool success) + { + balances[_from] = balances[_from].sub(_value); + allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); + balances[_to] = balances[_to].add(_value); + + emit Transfer(_from, _to, _value); + + return true; + } + + /** + * @notice Allows spender to withdraw from function caller's account. + * + * @dev Allows _spender address to withdraw from function caller's account, + * multiple times up to the _value amount, if this function is called + * again it overwrites the current allowance with _value. + * Emits "Approval" event. + * + * @param _spender Address authorized to spend from the function + * caller's address. + * @param _value Amount up to which spender is authorized to spend. + * + * @return True for a successful approval, false otherwise. + */ + function approve(address _spender, uint256 _value) + external + returns (bool success) + { + allowed[msg.sender][_spender] = _value; + + emit Approval(msg.sender, _spender, _value); + + return true; + } + + /** + * @notice Increase balance of the account with the specified value. + * + * @dev The function is not part of EIP20TokenInterface and is here + * to fill the balance of accounts during testing. + * + * @param _owner Account's address. + * @param _value Amount to increase for the account. + */ + function increaseBalance(address _owner, uint256 _value) + external + { + require( + _owner != address(0), + "The account to set balance is null." + ); + + balances[_owner] = balances[_owner].add(_value); + + tokenTotalSupply = tokenTotalSupply.add(_value); + } + +} diff --git a/contracts/test_doubles/unit_tests/MasterCopySpy.sol b/contracts/test_doubles/unit_tests/MasterCopySpy.sol new file mode 100644 index 0000000..0a1c533 --- /dev/null +++ b/contracts/test_doubles/unit_tests/MasterCopySpy.sol @@ -0,0 +1,100 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "../../external/SafeMath.sol"; +import "../../proxies/MasterCopyNonUpgradable.sol"; + +/** + * @title A test double (spy) contract acting as a master copy for proxies + * during testing. + * Contract is a generic master copy implementation, that implements + * setup() function to initialize proxy's storage layout, records + * parameters, msg.value and msg.sender values once calling the pay() + * function to test validity afterwards. + */ +contract MasterCopySpy is MasterCopyNonUpgradable { + + /* Usings */ + + using SafeMath for uint256; + + + /* Events */ + + event Payment( + address _beneficiary, + uint256 _amount + ); + + + /* Storage */ + + address public reservedStorageSlotForProxy; + + address public recordedMsgSender; + + address public recordedBeneficiary; + + uint8 public recordedCurrencyCode; + + uint256 public recordedMsgValue; + + uint256 public remainingBalance; + + bool public contractShouldFail; + + + /* Special Functions */ + + constructor(uint256 _initialBalance) + public + { + setup(_initialBalance); + } + + function setup(uint256 _initialBalance) + public + { + remainingBalance = _initialBalance; + } + + + /* External Functions */ + + function pay(uint8 _currencyCode, address _beneficiary) + external + payable + returns (uint256) + { + if (contractShouldFail) { + revert("Contract has been marked to fail."); + } + + recordedMsgSender = msg.sender; + + recordedCurrencyCode = _currencyCode; + + recordedBeneficiary = _beneficiary; + + recordedMsgValue = msg.value; + + remainingBalance = remainingBalance.sub(msg.value); + + emit Payment(_beneficiary, msg.value); + + return remainingBalance; + } +} \ No newline at end of file diff --git a/contracts/test_doubles/unit_tests/TokenRulesSpy.sol b/contracts/test_doubles/unit_tests/TokenRulesSpy.sol new file mode 100644 index 0000000..b3d6bca --- /dev/null +++ b/contracts/test_doubles/unit_tests/TokenRulesSpy.sol @@ -0,0 +1,62 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +contract TokenRulesSpy { + + /* Storage */ + + mapping (address => bool) public allowedTransfers; + + address public recordedFrom; + + address[] public recordedTransfersTo; + uint256 public recordedTransfersToLength; + + uint256[] public recordedTransfersAmount; + uint256 public recordedTransfersAmountLength; + + + /* External Functions */ + + function allowTransfers() + external + { + allowedTransfers[msg.sender] = true; + } + + function disallowTransfers() + external + { + allowedTransfers[msg.sender] = false; + } + + function executeTransfers( + address _from, + address[] calldata _transfersTo, + uint256[] calldata _transfersAmount + ) + external + { + recordedFrom = _from; + + recordedTransfersTo = _transfersTo; + recordedTransfersToLength = _transfersTo.length; + + recordedTransfersAmount = _transfersAmount; + recordedTransfersAmountLength = _transfersAmount.length; + } + +} diff --git a/contracts/test_doubles/unit_tests/UtilityTokenFake.sol b/contracts/test_doubles/unit_tests/UtilityTokenFake.sol new file mode 100644 index 0000000..1580076 --- /dev/null +++ b/contracts/test_doubles/unit_tests/UtilityTokenFake.sol @@ -0,0 +1,58 @@ +/* solhint-disable-next-line compiler-fixed */ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "./EIP20TokenFake.sol"; + +/** + * @title EIP20TokenMock contract. + * + * @notice Provides EIP20Token with mock functionality to facilitate testing. + */ +contract UtilityTokenFake is EIP20TokenFake { + + /* Storage */ + + address public coGateway; + + + /* Special Functions */ + + /** + * @param _symbol Symbol of the token. + * @param _name Name of the token. + * @param _decimals Decimal places of the token. + */ + constructor( + string memory _symbol, + string memory _name, + uint8 _decimals + ) + EIP20TokenFake(_symbol, _name, _decimals) + public + { + } + + + /* External functions */ + + /** @notice Sets coGateway address. */ + function setCoGateway(address _coGateway) + external + { + coGateway = _coGateway; + } +} diff --git a/contracts/test_doubles/unit_tests/gnosis_safe/GnosisSafeModuleManagerSpy.sol b/contracts/test_doubles/unit_tests/gnosis_safe/GnosisSafeModuleManagerSpy.sol new file mode 100644 index 0000000..ec6c78e --- /dev/null +++ b/contracts/test_doubles/unit_tests/gnosis_safe/GnosisSafeModuleManagerSpy.sol @@ -0,0 +1,93 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "../../../gnosis_safe_modules/GnosisSafeModuleManagerInterface.sol"; +import "../../../gnosis_safe_modules/DelayedRecoveryModule.sol"; + +contract GnosisSafeModuleManagerSpy is GnosisSafeModuleManagerInterface +{ + /* Events */ + + event DelayedRedcoveryModuleCreated(address _contractAddress); + + + /* Storage */ + + bool shouldExecutionFail; + + address public recordedTo; + + uint256 public recordedValue; + + bytes public recordedData; + + Operation public recordedOperation; + + + /* External Functions */ + + function makeFail() + external + { + shouldExecutionFail = true; + } + + function createDelayedRecoveryModule( + address _recoveryOwner, + address _recoveryController, + uint256 _recoveryBlockDelay + ) + external + returns (DelayedRecoveryModule) + { + DelayedRecoveryModule module = new DelayedRecoveryModule(); + module.setup( + _recoveryOwner, + _recoveryController, + _recoveryBlockDelay + ); + + emit DelayedRedcoveryModuleCreated(address(module)); + + return module; + } + + /** + * @dev Allows a module to execute a Safe transaction without any + * further confirmations. + * + * @param _to Destination address of module transaction. + * @param _value Ether value of module transaction. + * @param _data Data payload of module transaction. + * @param _operation Operation type of module transaction. + */ + function execTransactionFromModule( + address _to, + uint256 _value, + bytes calldata _data, + Operation _operation + ) + external + returns (bool success_) + { + recordedTo = _to; + recordedValue = _value; + recordedData = _data; + recordedOperation = _operation; + + return !shouldExecutionFail; + } +} \ No newline at end of file diff --git a/contracts/test_doubles/unit_tests/pricer_rule/PriceOracleFake.sol b/contracts/test_doubles/unit_tests/pricer_rule/PriceOracleFake.sol new file mode 100644 index 0000000..503d6f2 --- /dev/null +++ b/contracts/test_doubles/unit_tests/pricer_rule/PriceOracleFake.sol @@ -0,0 +1,147 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "../../../rules/PriceOracleInterface.sol"; + +contract PriceOracleFake is PriceOracleInterface { + + /* Storage */ + + bytes3 baseCurrencyCode; + + bytes3 quoteCurrencyCode; + + uint256 price; + + uint256 expirationHeight; + + uint8 quoteCurrencyDecimals; + + + /* Special */ + + constructor( + bytes3 _baseCurrencyCode, + bytes3 _quoteCurrencyCode, + uint8 _quoteCurrencyDecimals, + uint256 _initialPrice, + uint256 _expirationHeight + ) + public + { + require( + _baseCurrencyCode != bytes3(0), + "Base currency code is empty." + ); + + require( + _quoteCurrencyCode != bytes3(0), + "Quote currency code is empty." + ); + + baseCurrencyCode = _baseCurrencyCode; + + quoteCurrencyCode = _quoteCurrencyCode; + + quoteCurrencyDecimals = _quoteCurrencyDecimals; + + setPrice(_initialPrice, _expirationHeight); + } + + + /* External Functions */ + + /** + * @notice Returns base currency code. + * + * @dev Base currency code is not according to ISO 4217 or other standard. + */ + function baseCurrency() + external + view + returns (bytes3) + { + return baseCurrencyCode; + } + + /** + * @notice Returns quote currency code. + * + * @dev Quote currency code is not according to ISO 4217 or other standard. + */ + function quoteCurrency() + external + view + returns (bytes3) + { + return quoteCurrencyCode; + } + + /** + * @notice Returns quote currency decimals. + */ + function decimals() + external + view + returns(uint8) + { + return quoteCurrencyDecimals; + } + + /** + * @notice Returns an amount of the quote currency needed to purchase + * one unit of the base currency. + * + * @dev Function throws an exception if the price is invalid, for example, + * was not set, or became outdated, etc. + * + * @return An amount of the quote currency needed to purchase one unit of + * the base base currency. + */ + function getPrice() + external + view + returns (uint256) + { + require( + expirationHeight > block.number, + "Price expiration height is lte to the current block height." + ); + + return price; + } + + + /* Public Functions */ + + function setPrice( + uint256 _price, + uint256 _expirationHeight + ) + public + { + require( + _expirationHeight > block.number, + "Price expiration height is lte to the current block height." + ); + + price = _price; + + expirationHeight = _expirationHeight; + + emit PriceUpdated(price); + } +} \ No newline at end of file diff --git a/contracts/test_doubles/unit_tests/safe_math/SafeMathLibraryDouble.sol b/contracts/test_doubles/unit_tests/safe_math/SafeMathLibraryDouble.sol new file mode 100644 index 0000000..717de17 --- /dev/null +++ b/contracts/test_doubles/unit_tests/safe_math/SafeMathLibraryDouble.sol @@ -0,0 +1,68 @@ +pragma solidity ^0.5.0; + +import "../../../external/SafeMath.sol"; + +/** + * It is used to test SafeMath contract. + */ +contract SafeMathLibraryDouble { + + /* Public Functions */ + + /** @dev Multiplies two numbers, reverts on overflow.*/ + function mul(uint256 a, uint256 b) + public + pure + returns (uint256) + { + return SafeMath.mul(a,b); + } + + /** + * @dev Integer division of two numbers truncating the quotient, + * reverts on division by zero. + */ + function div(uint256 a, uint256 b) + public + pure + returns (uint256) + { + return SafeMath.div(a,b); + } + + /** + * @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend + * is greater than minuend). + */ + function sub(uint256 a, uint256 b) + public + pure + returns (uint256) + { + return SafeMath.sub(a,b); + } + + /** + * @dev Adds two numbers, reverts on overflow. + */ + function add(uint256 a, uint256 b) + public + pure + returns (uint256) + { + return SafeMath.add(a,b); + } + + /** + * @dev Divides two numbers and returns the remainder (unsigned integer + * modulo), reverts when dividing by zero. + */ + function mod(uint256 a, uint256 b) + public + pure + returns (uint256) + { + return SafeMath.mod(a,b); + } + +} diff --git a/contracts/test_doubles/unit_tests/token_holder/execute_redeem/CoGatewaySpy.sol b/contracts/test_doubles/unit_tests/token_holder/execute_redeem/CoGatewaySpy.sol new file mode 100644 index 0000000..8931d56 --- /dev/null +++ b/contracts/test_doubles/unit_tests/token_holder/execute_redeem/CoGatewaySpy.sol @@ -0,0 +1,87 @@ +pragma solidity ^0.5.0; + + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @notice Spy test double to catch inputs to verify. + */ +contract CoGatewaySpy { + + /* Storage */ + + uint256 public recordedPayedAmount; + + uint256 public recordedAmount; + + address public recordedBeneficiary; + + uint256 public recordedGasPrice; + + uint256 public recordedGasLimit; + + uint256 public recordedNonce; + + bytes32 public recoerdedHashLock; + + /** @dev If true, redeem() function fails by throwing an exception */ + bool public failRedemption; + + + /* External Functions */ + + /** + * @dev Calling this function makes mocked redeem() function to fail + * by throwing an exception. + */ + function makeRedemptionToFail() + external + { + failRedemption = true; + } + + function redeem( + uint256 _amount, + address _beneficiary, + uint256 _gasPrice, + uint256 _gasLimit, + uint256 _nonce, + bytes32 _hashLock + ) + external + payable + returns (bytes32) + { + if (failRedemption) { + revert("Calls to redeem are made to fail."); + } + + recordedPayedAmount = msg.value; + + recordedAmount = _amount; + + recordedBeneficiary = _beneficiary; + + recordedGasPrice = _gasPrice; + + recordedGasLimit = _gasLimit; + + recordedNonce = _nonce; + + recoerdedHashLock = _hashLock; + + return bytes32(0); + } +} diff --git a/contracts/MockRule.sol b/contracts/test_doubles/unit_tests/token_holder/execute_rule/CustomRuleDouble.sol similarity index 80% rename from contracts/MockRule.sol rename to contracts/test_doubles/unit_tests/token_holder/execute_rule/CustomRuleDouble.sol index 3c41cef..ec989df 100644 --- a/contracts/MockRule.sol +++ b/contracts/test_doubles/unit_tests/token_holder/execute_rule/CustomRuleDouble.sol @@ -1,7 +1,7 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.5.0; -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ pragma solidity ^0.4.23; // limitations under the License. -contract MockRule { +contract CustomRuleDouble { /* Constants */ @@ -31,8 +31,8 @@ contract MockRule { /* Storage */ - address public value; - uint256 public receivedPayableAmount; + address public recordedValue; + uint256 public recordedPayedAmount; /* Public Functions */ @@ -42,7 +42,7 @@ contract MockRule { ) public { - value = _value; + recordedValue = _value; revert("The function should fail by throwing."); } @@ -52,7 +52,7 @@ contract MockRule { public { require(_value != address(0), "Value is null."); - value = _value; + recordedValue = _value; } function passPayable( @@ -62,7 +62,8 @@ contract MockRule { payable { require(_value != address(0), "Value is null."); - value = _value; - receivedPayableAmount = msg.value; + recordedValue = _value; + recordedPayedAmount = msg.value; } + } diff --git a/contracts/token/EIP20TokenInterface.sol b/contracts/token/EIP20TokenInterface.sol new file mode 100644 index 0000000..85c2695 --- /dev/null +++ b/contracts/token/EIP20TokenInterface.sol @@ -0,0 +1,68 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Based on the 'final' EIP20 token standard as specified at: +// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md + +/** + * @title EIP20 token interface with optional and required interface functions. + */ +interface EIP20TokenInterface { + + /* Events */ + + event Transfer( + address indexed _from, + address indexed _to, + uint256 _value + ); + + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _value + ); + + + /* External Functions */ + + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function decimals() external view returns (uint8); + + function totalSupply() external view returns (uint256); + + function balanceOf(address _owner) external view returns (uint256 balance_); + + function allowance(address _owner, address _spender) + external + view + returns (uint256 remaining_); + + function transfer(address _to, uint256 _value) + external + returns (bool success_); + + function transferFrom(address _from, address _to, uint256 _value) + external + returns (bool success_); + + function approve(address _spender, uint256 _value) + external + returns (bool success_); +} diff --git a/contracts/token/TokenHolder.sol b/contracts/token/TokenHolder.sol new file mode 100644 index 0000000..18c4d32 --- /dev/null +++ b/contracts/token/TokenHolder.sol @@ -0,0 +1,540 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import "./EIP20TokenInterface.sol"; +import "../proxies/MasterCopyNonUpgradable.sol"; +import "../external/SafeMath.sol"; +import "./TokenRules.sol"; +import "./UtilityTokenRequiredInterface.sol"; + +/** + * @title TokenHolder contract. + * + * @notice Implements executable transactions (EIP-1077) for users to interact + * with token rules. It enables users to authorise sessions for + * session keys that dapps and mainstream applications can use to + * generate token events on-chain. + */ +contract TokenHolder is MasterCopyNonUpgradable +{ + + /* Usings */ + + using SafeMath for uint256; + + + /* Events */ + + event SessionAuthorized( + address _sessionKey + ); + + event SessionRevoked( + address _sessionKey + ); + + event SessionsLoggedOut( + uint256 _sessionWindow + ); + + /** + * @param _messageHash Executed rule message hash according to EIP-1077. + * @param _status Rule execution's status. + */ + event RuleExecuted( + bytes32 _messageHash, + bool _status + ); + + /** + * @param _messageHash Executed redemption request message hash according + * to EIP-1077. + * @param _status Redemption execution's status. + */ + event RedemptionExecuted( + bytes32 _messageHash, + bool _status + ); + + + /* Enums */ + + enum AuthorizationStatus { + NOT_AUTHORIZED, + REVOKED + } + + + /* Structs */ + + /** expirationHeight is the block number at which sessionKey expires. */ + struct SessionKeyData { + uint256 spendingLimit; + uint256 expirationHeight; + uint256 nonce; + uint256 session; + } + + + /* Constants */ + + bytes4 public constant EXECUTE_RULE_CALLPREFIX = bytes4( + keccak256( + "executeRule(address,bytes,uint256,bytes32,bytes32,uint8)" + ) + ); + + bytes4 public constant EXECUTE_REDEMPTION_CALLPREFIX = bytes4( + keccak256( + "executeRedemption(address,bytes,uint256,bytes32,bytes32,uint8)" + ) + ); + + + /* Storage */ + + EIP20TokenInterface public token; + + uint256 public sessionWindow; + + mapping(address /* key */ => SessionKeyData) public sessionKeys; + + address public tokenRules; + + address public owner; + + + /* Modifiers */ + + modifier onlyOwner() + { + require(msg.sender == owner, "Only owner is allowed to call."); + + _; + } + + modifier keyIsNotNull(address _key) + { + require(_key != address(0), "Key address is null."); + _; + } + + /** Requires that key is in authorized state. */ + modifier keyIsAuthorized(address _key) + { + require( + sessionKeys[_key].session > uint256(AuthorizationStatus.REVOKED), + "Key is not authorized." + ); + _; + } + + /** Requires that key was not authorized. */ + modifier keyDoesNotExist(address _key) + { + require( + sessionKeys[_key].session == uint256( + AuthorizationStatus.NOT_AUTHORIZED + ), + "Key exists." + ); + _; + } + + + /* External Functions */ + + /** + * @notice Setups an already deployed contract. + * + * @dev The function acts as a "constructor" to the contract and initializes + * the proxy's storage layout. + * + * Function requires: + * - It can be called only once for this contract. + * - Token address is not null. + * - Token rules address is not null. + * - Owner address is not null. + * - Session key addresses, spending limits and expiration height + * arrays lengths are equal. + * + * @param _token EIP20 token contract address deployed for an economy. + * @param _tokenRules Token rules contract address. + * @param _owner The contract's owner address. + * @param _sessionKeys Session key addresses to authorize. + * @param _sessionKeysSpendingLimits Session keys' spending limits. + * @param _sessionKeysExpirationHeights Session keys' expiration heights. + */ + function setup( + EIP20TokenInterface _token, + address _tokenRules, + address _owner, + address[] calldata _sessionKeys, + uint256[] calldata _sessionKeysSpendingLimits, + uint256[] calldata _sessionKeysExpirationHeights + ) + external + { + // Assures that function can be called only once. + require( + address(token) == address(0) && + address(owner) == address(0) && + address(tokenRules) == address(0), + "Contract has been already setup." + ); + + require( + address(_token) != address(0), + "Token contract address is null." + ); + require( + _tokenRules != address(0), + "TokenRules contract address is null." + ); + require( + _owner != address(0), + "Owner address is null." + ); + + require( + _sessionKeys.length == _sessionKeysSpendingLimits.length, + "Session keys and spending limits arrays lengths are different." + ); + + require( + _sessionKeys.length == _sessionKeysExpirationHeights.length, + "Session keys and expiration heights arrays lengths are different." + ); + + token = _token; + tokenRules = _tokenRules; + owner = _owner; + + sessionWindow = 2; + + for (uint256 i = 0; i < _sessionKeys.length; ++i) { + _authorizeSession( + _sessionKeys[i], + _sessionKeysSpendingLimits[i], + _sessionKeysExpirationHeights[i] + ); + } + } + + /** + * @notice Authorizes a session. + * + * @dev Function requires: + * - Only owner address can call. + * - The key is not null. + * - The key does not exist. + * - Expiration height is bigger than the current block height. + * + * @param _sessionKey Session key address to authorize. + * @param _spendingLimit Spending limit of the session key. + * @param _expirationHeight Expiration height of the session key. + */ + function authorizeSession( + address _sessionKey, + uint256 _spendingLimit, + uint256 _expirationHeight + ) + external + onlyOwner + { + _authorizeSession(_sessionKey, _spendingLimit, _expirationHeight); + } + + /** + * @notice Revokes session for the specified session key. + * + * @dev Function revokes the key even if it has expired. + * Function requires: + * - Only owner can call. + * - The key is authorized. + * + * @param _sessionKey Session key to revoke. + */ + function revokeSession(address _sessionKey) + external + onlyOwner + keyIsAuthorized(_sessionKey) + { + sessionKeys[_sessionKey].session = uint256(AuthorizationStatus.REVOKED); + + emit SessionRevoked(_sessionKey); + } + + /** + * @notice Logout all authorized sessions. + * + * @dev Function requires: + * - Only owner is allowed to call. + */ + function logout() + external + onlyOwner + { + emit SessionsLoggedOut(sessionWindow); + + sessionWindow = sessionWindow.add(1); + } + + /** + * @notice Evaluates executable transaction signed by a session key. + * + * @dev As a first step, function validates executable transaction by + * checking that the specified signature matches one of the + * authorized (non-expired) session keys. + * + * On success, function executes transaction by calling: + * _to.call(_data); + * + * Before execution, it approves the tokenRules as a spender + * for sessionKey.spendingLimit amount. This allowance is cleared + * after execution. + * + * Function requires: + * - _to address cannot be EIP20 Token. + * - key used to sign data is authorized and have not expired. + * - nonce is equal to the stored nonce value. + * + * @param _to The target contract address the transaction will be executed + * upon. + * @param _data The payload of a function to be executed in the target + * contract. + * @param _nonce The nonce of an session key that was used to sign + * the transaction. + * + * @return executeStatus_ True in case of successfull execution of the + * executable transaction, otherwise, false. + */ + function executeRule( + address _to, + bytes calldata _data, + uint256 _nonce, + bytes32 _r, + bytes32 _s, + uint8 _v + ) + external + payable + returns (bool executionStatus_) + { + require( + _to != address(token), + "'to' address is utility token address." + ); + + require( + _to != address(this), + "'to' address is TokenHolder address itself." + ); + + (bytes32 messageHash, address sessionKey) = verifyExecutableTransaction( + EXECUTE_RULE_CALLPREFIX, + _to, + _data, + _nonce, + _r, + _s, + _v + ); + + SessionKeyData storage sessionKeyData = sessionKeys[sessionKey]; + + TokenRules(tokenRules).allowTransfers(); + + token.approve( + tokenRules, + sessionKeyData.spendingLimit + ); + + bytes memory returnData; + // solium-disable-next-line security/no-call-value + (executionStatus_, returnData) = _to.call.value(msg.value)(_data); + + token.approve(tokenRules, 0); + + TokenRules(tokenRules).disallowTransfers(); + + emit RuleExecuted( + messageHash, + executionStatus_ + ); + } + + function executeRedemption( + address _to, + bytes calldata _data, + uint256 _nonce, + bytes32 _r, + bytes32 _s, + uint8 _v + ) + external + payable + returns (bool executionStatus_) + { + address coGateway = UtilityTokenRequiredInterface( + address(token) + ).coGateway(); + + require(_to == coGateway,"'to' address is not coGateway address."); + + (bytes32 messageHash, address sessionKey) = verifyExecutableTransaction( + EXECUTE_REDEMPTION_CALLPREFIX, + _to, + _data, + _nonce, + _r, + _s, + _v + ); + + SessionKeyData storage sessionKeyData = sessionKeys[sessionKey]; + + token.approve(_to, sessionKeyData.spendingLimit); + + bytes memory returnData; + // solium-disable-next-line security/no-call-value + (executionStatus_, returnData) = _to.call.value(msg.value)(_data); + + token.approve(_to, 0); + + emit RedemptionExecuted( + messageHash, + executionStatus_ + ); + } + + + /* Private Functions */ + + function _authorizeSession( + address _sessionKey, + uint256 _spendingLimit, + uint256 _expirationHeight + ) + private + keyIsNotNull(_sessionKey) + keyDoesNotExist(_sessionKey) + { + require( + _expirationHeight > block.number, + "Expiration height is lte to the current block height." + ); + + SessionKeyData storage keyData = sessionKeys[_sessionKey]; + + keyData.spendingLimit = _spendingLimit; + keyData.expirationHeight = _expirationHeight; + keyData.nonce = 0; + keyData.session = sessionWindow; + + emit SessionAuthorized(_sessionKey); + } + + function verifyExecutableTransaction( + bytes4 _callPrefix, + address _to, + bytes memory _data, + uint256 _nonce, + bytes32 _r, + bytes32 _s, + uint8 _v + ) + private + returns (bytes32 messageHash_, address key_) + { + messageHash_ = getMessageHash( + _callPrefix, + _to, + keccak256(_data), + _nonce + ); + + key_ = ecrecover(messageHash_, _v, _r, _s); + + SessionKeyData storage keyData = sessionKeys[key_]; + + // The following checks appears here and not higher in the stack, + // as session key is only retrieved here, after calculation + // of message hash according EIP-1077 and retriving the key + // from the signature (_r, _s, _v). + require( + keyData.session == sessionWindow, + "Key's session is not equal to contract's session window." + ); + + require( + keyData.expirationHeight > block.number, + "Session key was expired." + ); + + require( + _nonce == keyData.nonce, + "Incorrect nonce is specified." + ); + + keyData.nonce = keyData.nonce.add(1); + } + + /** + * @notice The hashed message format is compliant with EIP-1077. + * + * @dev EIP-1077 enables user to sign messages to show intent of execution, + * but allows a third party relayer to execute them. + * https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1077.md + */ + function getMessageHash( + bytes4 _callPrefix, + address _to, + bytes32 _dataHash, + uint256 _nonce + ) + private + view + returns (bytes32 messageHash_) + { + messageHash_ = keccak256( + abi.encodePacked( + byte(0x19), // Starting a transaction with byte(0x19) ensure + // the signed data from being a valid ethereum + // transaction. + byte(0), // The version control byte. + address(this), // The from field will always be the contract + // executing the code. + _to, + uint8(0), // The amount in ether to be sent. + _dataHash, + _nonce, + uint8(0), // gasPrice + uint8(0), // gasLimit + uint8(0), // gasToken + _callPrefix, // 4 byte standard call prefix of the + // function to be called in the 'from' contract. + // This guarantees that signed message can + // be only executed in a single instance. + uint8(0), // 0 for a standard call, 1 for a DelegateCall and 2 + // for a create opcode + bytes32(0) // extraHash is always hashed at the end. This is + // done to increase future compatibility of the + // standard. + ) + ); + } +} diff --git a/contracts/TokenRules.sol b/contracts/token/TokenRules.sol similarity index 58% rename from contracts/TokenRules.sol rename to contracts/token/TokenRules.sol index fe26021..59bcbe0 100644 --- a/contracts/TokenRules.sol +++ b/contracts/token/TokenRules.sol @@ -1,6 +1,6 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.5.0; -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,35 +14,26 @@ pragma solidity ^0.4.23; // See the License for the specific language governing permissions and // limitations under the License. -import "./GlobalConstraintInterface.sol"; -import "./SafeMath.sol"; import "./EIP20TokenInterface.sol"; - +import "../organization/Organized.sol"; /** * @notice Register of whitelisted rules that are allowed to initiate transfers * from a token holder accounts. * * @dev TokenHolder.executeRule() function will execute any rule that are - * signed by an authorized and non-expired ephemeral key. + * signed by an authorized and non-expired session key. * However, only the rules, that are registered in TokenRules * can initiate transfers of token from TokenHolder to other beneficiaries. * TokenHolder is going to allow TokenRules as a spender before * execution of the rule (amount is limited by spendingLimit registered - * during an authorizaiton of an ephemeral key.). TokenHolder will + * during an authorizaiton of an session key.). TokenHolder will * clear this allowance after execution. - * Before execution of transfers from TokenHolder, TokenRules will - * check that all global constraints are satisified. + * Before execution of transfers from TokenHolder. * During a execution, rule can call TokenRules.executeTransfers() - * function only once. This allows global constraints to be checked - * on complete list of transfers. + * function only once. */ -contract TokenRules { - - /* Usings */ - - using SafeMath for uint256; - +contract TokenRules is Organized { /* Events */ @@ -51,11 +42,6 @@ contract TokenRules { address _ruleAddress ); - event GlobalConstraintAdded(address _globalConstraintAddress); - - event GlobalConstraintRemoved(address _globalConstraintAddress); - - /* Structs */ struct TokenRule { @@ -88,10 +74,6 @@ contract TokenRules { /** Mapping from a rule name hash to the index in the `rules` array. */ mapping (bytes32 => RuleIndex) public rulesByNameHash; - /** Contains a list of all registered global constraints. */ - address[] public globalConstraints; - - address public organization; EIP20TokenInterface public token; /** @@ -102,22 +84,27 @@ contract TokenRules { */ mapping (address => bool) public allowedTransfers; + bool public areDirectTransfersEnabled; + /* Modifiers */ - modifier onlyOrganization { + + modifier onlyRule { require( - organization == msg.sender, - "Only organization is allowed to call." + rulesByAddress[msg.sender].exists, + "Only registered rule is allowed to call." ); _; } - modifier onlyRule { + modifier directTransfersAreEnabled { + require( - rulesByAddress[msg.sender].exists, - "Only registered rule is allowed to call." + areDirectTransfersEnabled, + "Direct transfers are not allowed." ); + _; } @@ -126,20 +113,20 @@ contract TokenRules { /** * @dev Function requires: - * - Organization address is not null. * - Token address is not null. */ constructor( - address _organization, + OrganizationInterface _organization, EIP20TokenInterface _token ) + Organized(_organization) public { - require(_organization != address(0), "Organization address is null."); - require(_token != address(0), "Token address is null."); + require(address(_token) != address(0), "Token address is null."); - organization = _organization; token = _token; + + areDirectTransfersEnabled = true; } @@ -147,7 +134,7 @@ contract TokenRules { /** * @dev Function requires: - * - Only organization can call. + * - Only worker can call. * - Rule name is not empty. * - Rule with the specified name does not exist. * - Rule address is not null. @@ -159,12 +146,12 @@ contract TokenRules { * @param _ruleAbi The abi of the rule to register. */ function registerRule( - string _ruleName, + string calldata _ruleName, address _ruleAddress, - string _ruleAbi + string calldata _ruleAbi ) external - onlyOrganization + onlyWorker { require(bytes(_ruleName).length != 0, "Rule name is empty."); require(_ruleAddress != address(0), "Rule address is null."); @@ -218,13 +205,6 @@ contract TokenRules { * accounts corresponding amounts. * Function requires: * - Only registered rule can call. - * - An account from which (_from) transfer will be done should - * allow this transfer by calling to TokenRules.allowTransfers(). - * TokenRules will set this allowance back, hence, only - * one call is allowed per execution session. - * - _transfersTo and _transfersAmount arrays length should match. - * - All globally registered constraints should satisfy before - * execution. * * @param _from An address from which transfer is done. * @param _transfersTo List of addresses to transfer. @@ -232,169 +212,106 @@ contract TokenRules { */ function executeTransfers( address _from, - address[] _transfersTo, - uint256[] _transfersAmount + address[] calldata _transfersTo, + uint256[] calldata _transfersAmount ) external onlyRule { - require( - allowedTransfers[_from], - "Transfers from the address are not allowed." - ); - - require( - _transfersTo.length == _transfersAmount.length, - "'to' and 'amount' transfer arrays' lengths are not equal." - ); - - require( - checkGlobalConstraints(_from, _transfersTo, _transfersAmount), - "Constraints not fullfilled." - ); - - for(uint256 i = 0; i < _transfersTo.length; ++i) { - token.transferFrom( - _from, - _transfersTo[i], - _transfersAmount[i] - ); - } - - allowedTransfers[_from] = false; + _executeTransfers(_from, _transfersTo, _transfersAmount); } /** - * @notice Registers a constraint to check globally before - * executing transfers. + * @notice Enables direct transfers from token rules. * * @dev Function requires: - * - Only organization can call. - * - Constraint address is not null. - * - Constraint is not registered. + * - Only organization worker is allowed to call. + * + * \see directTransfers() */ - function addGlobalConstraint( - address _globalConstraintAddress - ) + function enableDirectTransfers() external - onlyOrganization + onlyWorker { - require( - _globalConstraintAddress != address(0), - "Constraint to add is null." - ); - - uint256 index = findGlobalConstraintIndex(_globalConstraintAddress); - - require( - index == globalConstraints.length, - "Constraint to add already exists." - ); - - globalConstraints.push(_globalConstraintAddress); - - emit GlobalConstraintAdded(_globalConstraintAddress); + areDirectTransfersEnabled = true; } /** + * @notice Disables direct transfers from token rules. + * * @dev Function requires: - * - Only organization can call. - * - Constraint exists. + * - Only organization worker is allowed to call. + * + * \see directTransfers() */ - function removeGlobalConstraint( - address _globalConstraintAddress - ) + function disableDirectTransfers() external - onlyOrganization + onlyWorker { - uint256 index = findGlobalConstraintIndex(_globalConstraintAddress); - - require( - index != globalConstraints.length, - "Constraint to remove does not exist." - ); - - removeGlobalConstraintByIndex(index); - - emit GlobalConstraintRemoved(_globalConstraintAddress); + areDirectTransfersEnabled = false; } - - /* Public Functions */ - - function globalConstraintCount() - public - view - returns (uint256) + /** + * @dev Transfers from the caller's account to all beneficiary + * accounts corresponding amounts. + * + * @param _transfersTo List of addresses to transfer. + * @param _transfersAmount List of amounts to transfer. + */ + function directTransfers( + address[] calldata _transfersTo, + uint256[] calldata _transfersAmount + ) + external + directTransfersAreEnabled { - return globalConstraints.length; + _executeTransfers(msg.sender, _transfersTo, _transfersAmount); } + + /* Private Functions */ + /** - * @dev Function requires: + * @dev Transfers from the specified account to all beneficiary + * accounts corresponding amounts. + * Function requires: + * - An account from which (_from) transfer will be done should + * allow this transfer by calling to TokenRules.allowTransfers(). + * TokenRules will set this allowance back, hence, only + * one call is allowed per execution session. * - _transfersTo and _transfersAmount arrays length should match. * - * @return Returns true, if all registered global constraints - * are satisfied, otherwise false. + * @param _from An address from which transfer is done. + * @param _transfersTo List of addresses to transfer. + * @param _transfersAmount List of amounts to transfer. */ - function checkGlobalConstraints( + function _executeTransfers( address _from, - address[] _transfersTo, - uint256[] _transfersAmount + address[] memory _transfersTo, + uint256[] memory _transfersAmount ) - public - view - returns (bool _passed) + private { + require( + allowedTransfers[_from], + "Transfers from the address are not allowed." + ); + + // prevent possible re-entry by the external call to token + // by early-on setting the stateful check to false. + allowedTransfers[_from] = false; + require( _transfersTo.length == _transfersAmount.length, "'to' and 'amount' transfer arrays' lengths are not equal." ); - _passed = true; - - for(uint256 i = 0; i < globalConstraints.length && _passed; ++i) { - _passed = GlobalConstraintInterface(globalConstraints[i]).check( + for(uint256 i = 0; i < _transfersTo.length; ++i) { + token.transferFrom( _from, - _transfersTo, - _transfersAmount + _transfersTo[i], + _transfersAmount[i] ); } } - - - /* Private Functions */ - - /** - * @dev Finds index of constraint. - * - * @param _constraint Constraint to find in constraints array. - * - * @return index_ Returns index of the constraint if exists, - * otherwise returns constraints.length. - */ - function findGlobalConstraintIndex(address _constraint) - private - view - returns (uint256 index_) - { - index_ = 0; - while( - index_ < globalConstraints.length && - globalConstraints[index_] != _constraint - ) - { - ++index_; - } - } - - function removeGlobalConstraintByIndex(uint256 _index) - private - { - require(_index < globalConstraints.length, "Index is out of range."); - - uint256 lastElementIndex = globalConstraints.length - 1; - globalConstraints[_index] = globalConstraints[lastElementIndex]; - --globalConstraints.length; - } } diff --git a/contracts/test/token_rules/TokenRulesPassingGlobalConstraint.sol b/contracts/token/UtilityTokenRequiredInterface.sol similarity index 57% rename from contracts/test/token_rules/TokenRulesPassingGlobalConstraint.sol rename to contracts/token/UtilityTokenRequiredInterface.sol index 1b659fe..45cbbde 100644 --- a/contracts/test/token_rules/TokenRulesPassingGlobalConstraint.sol +++ b/contracts/token/UtilityTokenRequiredInterface.sol @@ -1,6 +1,7 @@ -pragma solidity ^0.4.23; +/* solhint-disable-next-line compiler-fixed */ +pragma solidity ^0.5.0; -// Copyright 2018 OpenST Ltd. +// Copyright 2017 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,25 +15,16 @@ pragma solidity ^0.4.23; // See the License for the specific language governing permissions and // limitations under the License. -import "../../GlobalConstraintInterface.sol"; +/** + * @title Provides the minimum required interface for utility token. + */ +interface UtilityTokenRequiredInterface { -contract TokenRulesPassingGlobalConstraint is GlobalConstraintInterface -{ /* External Functions */ - function check( - address _from, - address[] _transfersTo, - uint256[] _transfersAmount - ) - external - view - returns (bool) - { - _from; - _transfersTo; - _transfersAmount; + /** + * @notice Returns coGateway address of utility token. + */ + function coGateway() external view returns (address); - return true; - } } diff --git a/contracts/truffle/Migrations.sol b/contracts/truffle/Migrations.sol index 44eb67c..cd03ac5 100644 --- a/contracts/truffle/Migrations.sol +++ b/contracts/truffle/Migrations.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.5.0; contract Migrations { diff --git a/external/gnosis/safe-contracts b/external/gnosis/safe-contracts new file mode 160000 index 0000000..427d6f7 --- /dev/null +++ b/external/gnosis/safe-contracts @@ -0,0 +1 @@ +Subproject commit 427d6f7e779431333c54bcb4d4cde31e4d57ce96 diff --git a/images/ExecuteRuleSequenceDiagram.png b/images/ExecuteRuleSequenceDiagram.png new file mode 100644 index 0000000..3ca85e3 Binary files /dev/null and b/images/ExecuteRuleSequenceDiagram.png differ diff --git a/images/SessionKeyAuthorizationSequenceDiagram.png b/images/SessionKeyAuthorizationSequenceDiagram.png new file mode 100644 index 0000000..fbb664f Binary files /dev/null and b/images/SessionKeyAuthorizationSequenceDiagram.png differ diff --git a/npm_package/build_package.js b/npm_package/build_package.js new file mode 100755 index 0000000..7eac63b --- /dev/null +++ b/npm_package/build_package.js @@ -0,0 +1,79 @@ +#!/usr/bin/env node + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @file This file runs as part of the npm packaging process. + * + * It reads a set number of contracts from the truffle build directory and + * extracts ABI and BIN of each contract. The extracted information is added to + * a new object that is finally serialized to disk. That JSON file will be + * exported by this package. + */ + +const fs = require('fs'); +const path = require('path'); +const { contractNames } = require('./contract_names.json'); + +/** + * Retrieves the contract's metadata (abi & bin) from the provided file path. + * @param {String} contractPath Contract's file path. + * + * @returns {Object} Contract's abi and bin. + */ +function retrieveContractMetaData(contractPath) { + if (!fs.existsSync(contractPath)) { + throw new Error( + `Cannot read file ${contractPath}.`, + ); + } + + const contractFile = fs.readFileSync(contractPath); + const metaData = JSON.parse(contractFile); + + const contract = {}; + contract.abi = metaData.abi; + + if (metaData.bytecode !== '0x') { + contract.bin = metaData.bytecode; + } + + return contract; +} + +const contracts = { + openst: {}, + gnosis: {}, +}; + +contractNames.openst.forEach((contract) => { + const contractPath = path.join( + __dirname, + `../build/contracts/${contract}.json`, + ); + + contracts.openst[contract] = retrieveContractMetaData(contractPath); +}); + +contractNames.gnosis.forEach((contract) => { + const contractPath = path.join( + __dirname, + `../external/gnosis/safe-contracts/build/contracts/${contract}.json`, + ); + + contracts.gnosis[contract] = retrieveContractMetaData(contractPath); +}); + +fs.writeFileSync(path.join(__dirname, './dist/contracts.json'), JSON.stringify(contracts)); diff --git a/npm_package/contract_names.json b/npm_package/contract_names.json new file mode 100644 index 0000000..08d88ae --- /dev/null +++ b/npm_package/contract_names.json @@ -0,0 +1,22 @@ +{ + "contractNames": { + "openst": [ + "DelayedRecoveryModule", + "Proxy", + "ProxyFactory", + "UserWalletFactory", + "PriceOracleInterface", + "PricerRule", + "TokenHolder", + "TokenRules", + "EIP20TokenInterface", + "Organization", + "OrganizationInterface", + "Organized" + ], + "gnosis": [ + "CreateAndAddModules", + "GnosisSafe" + ] + } +} diff --git a/contracts/GlobalConstraintInterface.sol b/npm_package/dist/index.js similarity index 61% rename from contracts/GlobalConstraintInterface.sol rename to npm_package/dist/index.js index 59c5f82..c88cc6d 100644 --- a/contracts/GlobalConstraintInterface.sol +++ b/npm_package/dist/index.js @@ -1,6 +1,4 @@ -pragma solidity ^0.4.23; - -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,12 +11,14 @@ pragma solidity ^0.4.23; // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +// +// ---------------------------------------------------------------------------- +// +// http://www.simpletoken.org/ +// +// ---------------------------------------------------------------------------- + +// eslint-disable-next-line import/no-unresolved +const contracts = require('./contracts.json'); -interface GlobalConstraintInterface -{ - function check( - address _from, - address[] _transfersTo, - uint256[] _transfersAmount - ) external view returns (bool); -} +module.exports = contracts; diff --git a/npm_package/test/index.js b/npm_package/test/index.js new file mode 100644 index 0000000..c6fcb2c --- /dev/null +++ b/npm_package/test/index.js @@ -0,0 +1,11 @@ +const assert = require('assert'); +const contracts = require('@openst/openst-contracts'); +const { contractNames } = require('../contract_names.json'); + +contractNames.openst.forEach((name) => { + assert(contracts.openst[name].abi !== undefined); +}); + +contractNames.gnosis.forEach((name) => { + assert(contracts.gnosis[name].abi !== undefined); +}); diff --git a/npm_package/test/run_npm_package_test.sh b/npm_package/test/run_npm_package_test.sh new file mode 100755 index 0000000..159f027 --- /dev/null +++ b/npm_package/test/run_npm_package_test.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +script_dir_path="$(cd "$(dirname "$0")" && pwd)" +root_dir="${script_dir_path}/../.." + +echo "Switching to root directory." +cd "${root_dir}" || exit 1 + +echo "Executing \"npm pack\"." +npm pack || exit 1 + +echo "Switching back to the script directory." +cd "${script_dir_path}" || exit 1 + +echo "Retrieving package version." +version=$(jq '.version' <"${root_dir}/package.json") +temp="${version%\"}" +version="${temp#\"}" +[[ "${version}" == "null" ]] && exit 1 + +echo "Copying npm tarball into the test directory." +cp "${root_dir}/openst-openst-contracts-${version}.tgz" . || exit 1 + +echo "Initiating npm project for test." +npm init -y || exit 1 +npm install assert || exit 1 + +echo "Installing openst-contract npm package into newly created project." +npm install "openst-openst-contracts-${version}.tgz" || exit 1 + +echo "Running ${script_dir_path}/index.js" +node "${script_dir_path}/index.js" || exit 1 + +echo "Cleaning up generated files." +rm -r "${script_dir_path}/node_modules" || exit 1 +rm openst-openst-contracts-${version}.tgz || exit 1 +rm package.json || exit 1 +rm package-lock.json || exit 1 + +echo "Successully Passed." diff --git a/package.json b/package.json index 5a88b18..00702fd 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,59 @@ { - "name": "@openstfoundation/openst-contracts", - "version": "0.9.4", - "description": "", - "keywords": [ - "openst", - "OST", - "simpletoken" - ], - "homepage": "https://openst.org", - "author": "OpenST Foundation Ltd.", - "license": "LGPL-3.0", - "repository": { - "type": "git", - "url": "https://github.com/OpenSTFoundation/openst-contracts.git" - }, - "bugs": { - "url": "https://github.com/OpenSTFoundation/openst-contracts/issues" - }, - "scripts": {}, - "devDependencies": { - "abi-decoder": "1.2.0", - "assert": "1.4.1", - "bignumber.js": "4.1.0", - "bn.js": "4.11.8", - "eslint": "5.5.0", - "eslint-config-airbnb-base": "13.1.0", - "eslint-plugin-import": "2.14.0", - "ethereumjs-util": "5.2.0", - "ganache-cli": "6.1.8", - "solc": "0.4.23", - "solidity-coverage": "0.5.11", - "truffle": "5.0.0-beta.0", - "web3": "1.0.0-beta.36" - } + "name": "@openst/openst-contracts", + "version": "0.10.0-rc.3", + "description": "Openst contracts provide ABIs and BINs for EVM smart contracts to enable developers to program token economies.", + "author": "OpenST Foundation Ltd.", + "license": "Apache-2.0", + "keywords": [ + "ethereum", + "openst", + "token economy", + "token holder", + "token rules", + "recovery" + ], + "homepage": "https://openst.org", + "repository": { + "type": "git", + "url": "https://github.com/OpenST/openst-contracts.git" + }, + "bugs": { + "url": "https://github.com/OpenST/openst-contracts/issues" + }, + "files": [ + "npm_package/dist" + ], + "main": "./npm_package/dist/index.js", + "scripts": { + "update": "git submodule update --init --recursive && npm i && cd ./external/gnosis/safe-contracts && npm ci && cd -", + "compile": "./node_modules/.bin/truffle compile --all --reset", + "compile:gnosis": "cd ./external/gnosis/safe-contracts && ./node_modules/.bin/truffle compile --all --reset && cd -", + "compile-all": "npm-run-all compile compile:gnosis", + "lint:js": "find ./test -name \"*.js\" | xargs ./node_modules/.bin/eslint", + "lint:js:fix": "npm run lint:js -- --fix", + "lint:sol": "./node_modules/.bin/solium --dir contracts/", + "lint:sol:fix": "npm run lint:sol -- --fix", + "ganache-cli": "./tools/run_ganache_cli.sh", + "test:contracts": "./node_modules/.bin/truffle test", + "build-package": "node ./npm_package/build_package.js", + "test:package": "./npm_package/test/run_npm_package_test.sh", + "prepack": "npm-run-all compile-all build-package", + "build": "npm-run-all update lint:sol lint:js compile-all test:contracts build-package test:package" + }, + "devDependencies": { + "abi-decoder": "1.2.0", + "assert": "1.4.1", + "bn.js": "4.11.8", + "eslint": "5.5.0", + "eslint-config-airbnb-base": "13.1.0", + "eslint-plugin-import": "2.14.0", + "ethereumjs-util": "5.2.0", + "ethlint": "^1.2.2", + "ganache-cli": "6.1.8", + "npm-run-all": "^4.1.5", + "nsp": "^3.2.1", + "solc": "0.5.0", + "truffle": "beta", + "web3": "1.0.0-beta.36" + } } diff --git a/test/delayed_recovery_module/abort_recovery_by_controller.js b/test/delayed_recovery_module/abort_recovery_by_controller.js new file mode 100644 index 0000000..2d301b4 --- /dev/null +++ b/test/delayed_recovery_module/abort_recovery_by_controller.js @@ -0,0 +1,278 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const EthUtils = require('ethereumjs-util'); +const Utils = require('../test_lib/utils.js'); +const { Event } = require('../test_lib/event_decoder.js'); +const RecoveryModuleUtils = require('./utils.js'); +const { AccountProvider } = require('../test_lib/utils.js'); + +async function prepare(accountProvider) { + const { + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const prevOwner = accountProvider.get(); + const oldOwner = accountProvider.get(); + const newOwner = accountProvider.get(); + + const { + signature, + } = RecoveryModuleUtils.signInitiateRecovery( + recoveryModule.address, + prevOwner, + oldOwner, + newOwner, + recoveryOwnerPrivateKey, + ); + + await recoveryModule.initiateRecovery( + prevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: recoveryControllerAddress }, + ); + + return { + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + }; +} + +contract('DelayedRecoveryModule::abortRecoveryByController', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if called by non-controller address.', async () => { + const { + recoveryControllerAddress, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + const nonControllerAddress = accountProvider.get(); + assert.notEqual( + nonControllerAddress, + recoveryControllerAddress, + ); + + await Utils.expectRevert( + recoveryModule.abortRecoveryByController( + prevOwner, + oldOwner, + newOwner, + { from: nonControllerAddress }, + ), + 'Should revert as called by non-controller address.', + 'Only recovery controller is allowed to call.', + ); + }); + + it('Reverts if there is no active recovery process.', async () => { + const { + recoveryControllerAddress, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const prevOwner = accountProvider.get(); + const oldOwner = accountProvider.get(); + const newOwner = accountProvider.get(); + + assert.isNotOk( + (await recoveryModule.activeRecoveryInfo.call()).initiated, + ); + + await Utils.expectRevert( + recoveryModule.abortRecoveryByController( + prevOwner, + oldOwner, + newOwner, + { from: recoveryControllerAddress }, + ), + 'Should revert as there is no active recovery process.', + 'There is no active recovery.', + ); + }); + + it('Reverts if an abort request is not for the active one.', async () => { + const { + recoveryControllerAddress, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + const anotherPrevOwner = accountProvider.get(); + const anotherOldOwner = accountProvider.get(); + const anotherNewOwner = accountProvider.get(); + + await Utils.expectRevert( + recoveryModule.abortRecoveryByController( + anotherPrevOwner, + oldOwner, + newOwner, + { from: recoveryControllerAddress }, + ), + 'Should revert as the abort request is not for the active recovery.', + 'The execution request\'s data does not match with the active one.', + ); + + await Utils.expectRevert( + recoveryModule.abortRecoveryByController( + prevOwner, + anotherOldOwner, + newOwner, + { from: recoveryControllerAddress }, + ), + 'Should revert as the abort request is not for the active recovery.', + 'The execution request\'s data does not match with the active one.', + ); + + await Utils.expectRevert( + recoveryModule.abortRecoveryByController( + prevOwner, + oldOwner, + anotherNewOwner, + { from: recoveryControllerAddress }, + ), + 'Should revert as the abort request is not for the active recovery.', + 'The execution request\'s data does not match with the active one.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits RecoveryAborted.', async () => { + const { + recoveryControllerAddress, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + const transactionResponse = await recoveryModule.abortRecoveryByController( + prevOwner, + oldOwner, + newOwner, + { from: recoveryControllerAddress }, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'RecoveryAborted', + args: { + _prevOwner: prevOwner, + _oldOwner: oldOwner, + _newOwner: newOwner, + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that recovery is aborted properly.', async () => { + const { + recoveryControllerAddress, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + let activeRecoveryInfo = await recoveryModule.activeRecoveryInfo.call(); + + assert.strictEqual( + activeRecoveryInfo.prevOwner, + prevOwner, + ); + + assert.strictEqual( + activeRecoveryInfo.oldOwner, + oldOwner, + ); + + assert.strictEqual( + activeRecoveryInfo.newOwner, + newOwner, + ); + + assert.isNotOk( + activeRecoveryInfo.executionBlockHeight.eqn(0), + ); + + await recoveryModule.abortRecoveryByController( + prevOwner, + oldOwner, + newOwner, + { from: recoveryControllerAddress }, + ); + + activeRecoveryInfo = await recoveryModule.activeRecoveryInfo.call(); + + assert.strictEqual( + activeRecoveryInfo.prevOwner, + Utils.NULL_ADDRESS, + ); + + assert.strictEqual( + activeRecoveryInfo.oldOwner, + Utils.NULL_ADDRESS, + ); + + assert.strictEqual( + activeRecoveryInfo.newOwner, + Utils.NULL_ADDRESS, + ); + + assert.isOk( + activeRecoveryInfo.executionBlockHeight.eqn(0), + ); + + assert.isNotOk( + activeRecoveryInfo.initiated, + ); + }); + }); +}); diff --git a/test/delayed_recovery_module/abort_recovery_by_owner.js b/test/delayed_recovery_module/abort_recovery_by_owner.js new file mode 100644 index 0000000..4c5f522 --- /dev/null +++ b/test/delayed_recovery_module/abort_recovery_by_owner.js @@ -0,0 +1,353 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const EthUtils = require('ethereumjs-util'); +const Utils = require('../test_lib/utils.js'); +const { Event } = require('../test_lib/event_decoder.js'); +const RecoveryModuleUtils = require('./utils.js'); +const { AccountProvider } = require('../test_lib/utils.js'); + +async function prepare(accountProvider) { + const { + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const prevOwner = accountProvider.get(); + const oldOwner = accountProvider.get(); + const newOwner = accountProvider.get(); + + const { + signature, + } = RecoveryModuleUtils.signInitiateRecovery( + recoveryModule.address, + prevOwner, + oldOwner, + newOwner, + recoveryOwnerPrivateKey, + ); + + await recoveryModule.initiateRecovery( + prevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: recoveryControllerAddress }, + ); + + return { + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + }; +} + +contract('DelayedRecoveryModule::abortRecoveryByOwner', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if there is no active recovery process.', async () => { + const { + recoveryOwnerPrivateKey, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const prevOwner = accountProvider.get(); + const oldOwner = accountProvider.get(); + const newOwner = accountProvider.get(); + + const { + signature, + } = RecoveryModuleUtils.signAbortRecovery( + recoveryModule.address, prevOwner, oldOwner, newOwner, recoveryOwnerPrivateKey, + ); + + assert.isOk( + (await recoveryModule.activeRecoveryInfo.call()).executionBlockHeight.eqn(0), + ); + + await Utils.expectRevert( + recoveryModule.abortRecoveryByOwner( + prevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: accountProvider.get() }, + ), + 'Should revert as there is no active recovery process.', + 'There is no active recovery.', + ); + }); + + it('Reverts if an abort request is not for the active one.', async () => { + const { + recoveryOwnerPrivateKey, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + const anotherPrevOwner = accountProvider.get(); + const anotherOldOwner = accountProvider.get(); + const anotherNewOwner = accountProvider.get(); + + assert.strictEqual( + (await recoveryModule.activeRecoveryInfo.call()).prevOwner, + prevOwner, + ); + + assert.strictEqual( + (await recoveryModule.activeRecoveryInfo.call()).oldOwner, + oldOwner, + ); + + assert.strictEqual( + (await recoveryModule.activeRecoveryInfo.call()).newOwner, + newOwner, + ); + + const { + signature: signature1, + } = RecoveryModuleUtils.signAbortRecovery( + recoveryModule.address, anotherPrevOwner, oldOwner, newOwner, recoveryOwnerPrivateKey, + ); + + await Utils.expectRevert( + recoveryModule.abortRecoveryByOwner( + anotherPrevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature1.r), + EthUtils.bufferToHex(signature1.s), + signature1.v, + { from: accountProvider.get() }, + ), + 'Should revert as the abort request is not for the active recovery.', + 'The execution request\'s data does not match with the active one.', + ); + + const { + signature: signature2, + } = RecoveryModuleUtils.signAbortRecovery( + recoveryModule.address, prevOwner, anotherOldOwner, newOwner, recoveryOwnerPrivateKey, + ); + + await Utils.expectRevert( + recoveryModule.abortRecoveryByOwner( + prevOwner, + anotherOldOwner, + newOwner, + EthUtils.bufferToHex(signature2.r), + EthUtils.bufferToHex(signature2.s), + signature2.v, + { from: accountProvider.get() }, + ), + 'Should revert as the abort request is not for the active recovery.', + 'The execution request\'s data does not match with the active one.', + ); + + const { + signature: signature3, + } = RecoveryModuleUtils.signAbortRecovery( + recoveryModule.address, prevOwner, oldOwner, anotherNewOwner, recoveryOwnerPrivateKey, + ); + + await Utils.expectRevert( + recoveryModule.abortRecoveryByOwner( + prevOwner, + oldOwner, + anotherNewOwner, + EthUtils.bufferToHex(signature3.r), + EthUtils.bufferToHex(signature3.s), + signature3.v, + { from: accountProvider.get() }, + ), + 'Should revert as the abort request is not for the active recovery.', + 'The execution request\'s data does not match with the active one.', + ); + }); + + it('Reverts if an abort request is signed by an invalid key.', async () => { + const { + recoveryOwnerPrivateKey, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + const privateKey = '0x038764453ef1dbdf9cfb3923f95d22a8974a1aa2f7351737b46d9ea25aaba50a'; + + assert.notEqual( + recoveryOwnerPrivateKey, + privateKey, + ); + + const { + signature, + } = RecoveryModuleUtils.signAbortRecovery( + recoveryModule.address, prevOwner, oldOwner, newOwner, privateKey, + ); + + await Utils.expectRevert( + recoveryModule.abortRecoveryByOwner( + prevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: accountProvider.get() }, + ), + 'Should revert as the abort request is signed by invalid key.', + 'Invalid signature for recovery owner.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits RecoveryAborted.', async () => { + const { + recoveryOwnerPrivateKey, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + const { + signature, + } = RecoveryModuleUtils.signAbortRecovery( + recoveryModule.address, prevOwner, oldOwner, newOwner, recoveryOwnerPrivateKey, + ); + + const transactionResponse = await recoveryModule.abortRecoveryByOwner( + prevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: accountProvider.get() }, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'RecoveryAborted', + args: { + _prevOwner: prevOwner, + _oldOwner: oldOwner, + _newOwner: newOwner, + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that recovery is aborted properly.', async () => { + const { + recoveryOwnerPrivateKey, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + const { + signature, + } = RecoveryModuleUtils.signAbortRecovery( + recoveryModule.address, prevOwner, oldOwner, newOwner, recoveryOwnerPrivateKey, + ); + + let activeRecoveryInfo = await recoveryModule.activeRecoveryInfo.call(); + + assert.strictEqual( + activeRecoveryInfo.prevOwner, + prevOwner, + ); + + assert.strictEqual( + activeRecoveryInfo.oldOwner, + oldOwner, + ); + + assert.strictEqual( + activeRecoveryInfo.newOwner, + newOwner, + ); + + assert.isNotOk( + activeRecoveryInfo.executionBlockHeight.eqn(0), + ); + + await recoveryModule.abortRecoveryByOwner( + prevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: accountProvider.get() }, + ); + + activeRecoveryInfo = await recoveryModule.activeRecoveryInfo.call(); + + assert.strictEqual( + activeRecoveryInfo.prevOwner, + Utils.NULL_ADDRESS, + ); + + assert.strictEqual( + activeRecoveryInfo.oldOwner, + Utils.NULL_ADDRESS, + ); + + assert.strictEqual( + activeRecoveryInfo.newOwner, + Utils.NULL_ADDRESS, + ); + + assert.isOk( + activeRecoveryInfo.executionBlockHeight.eqn(0), + ); + }); + }); +}); diff --git a/test/delayed_recovery_module/execute_recovery.js b/test/delayed_recovery_module/execute_recovery.js new file mode 100644 index 0000000..9bf9a3e --- /dev/null +++ b/test/delayed_recovery_module/execute_recovery.js @@ -0,0 +1,423 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const EthUtils = require('ethereumjs-util'); +const Utils = require('../test_lib/utils.js'); +const web3 = require('../test_lib/web3.js'); +const { Event } = require('../test_lib/event_decoder.js'); +const RecoveryModuleUtils = require('./utils.js'); +const { AccountProvider } = require('../test_lib/utils.js'); + +async function prepare(accountProvider) { + const { + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryBlockDelay, + moduleManager, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const prevOwner = accountProvider.get(); + const oldOwner = accountProvider.get(); + const newOwner = accountProvider.get(); + + const { + signature, + } = RecoveryModuleUtils.signInitiateRecovery( + recoveryModule.address, + prevOwner, + oldOwner, + newOwner, + recoveryOwnerPrivateKey, + ); + + await recoveryModule.initiateRecovery( + prevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: recoveryControllerAddress }, + ); + + return { + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryBlockDelay, + moduleManager, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + }; +} + +function generateSwapOwnerData( + prevOwner, oldOwner, newOwner, +) { + return web3.eth.abi.encodeFunctionCall( + { + name: 'swapOwner', + type: 'function', + inputs: [ + { + type: 'address', + name: 'prevOwner', + }, + { + type: 'address', + name: 'oldOwner', + }, + { + type: 'address', + name: 'newOwner', + }, + ], + }, + [prevOwner, oldOwner, newOwner], + ); +} + +contract('DelayedRecoveryModule::executeRecovery', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if a caller is not a controller.', async () => { + const { + recoveryBlockDelay, + recoveryModule, + } = await prepare( + accountProvider, + ); + + for (let i = 0; i < recoveryBlockDelay; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Utils.advanceBlock(); + } + + const prevOwner = accountProvider.get(); + const oldOwner = accountProvider.get(); + const newOwner = accountProvider.get(); + + assert.isNotOk( + (await recoveryModule.activeRecoveryInfo.call()).executionBlockHeight.eqn(0), + ); + + await Utils.expectRevert( + recoveryModule.executeRecovery( + prevOwner, + oldOwner, + newOwner, + { from: accountProvider.get() }, + ), + 'Should revert as a caller is not a controller.', + 'Only recovery controller is allowed to call.', + ); + }); + + it('Reverts if there is no active recovery process.', async () => { + const { + recoveryControllerAddress, + recoveryBlockDelay, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + for (let i = 0; i < recoveryBlockDelay; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Utils.advanceBlock(); + } + + const prevOwner = accountProvider.get(); + const oldOwner = accountProvider.get(); + const newOwner = accountProvider.get(); + + assert.isOk( + (await recoveryModule.activeRecoveryInfo.call()).executionBlockHeight.eqn(0), + ); + + await Utils.expectRevert( + recoveryModule.executeRecovery( + prevOwner, + oldOwner, + newOwner, + { from: recoveryControllerAddress }, + ), + 'Should revert as there is no active recovery process.', + 'There is no active recovery.', + ); + }); + + it('Reverts if the execute request is not for the active one.', async () => { + const { + recoveryControllerAddress, + recoveryBlockDelay, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + for (let i = 0; i < recoveryBlockDelay; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Utils.advanceBlock(); + } + + const anotherPrevOwner = accountProvider.get(); + const anotherOldOwner = accountProvider.get(); + const anotherNewOwner = accountProvider.get(); + + await Utils.expectRevert( + recoveryModule.executeRecovery( + anotherPrevOwner, + oldOwner, + newOwner, + { from: recoveryControllerAddress }, + ), + 'Should revert as the execute request is not for the active recovery.', + 'The execution request\'s data does not match with the active one.', + ); + + await Utils.expectRevert( + recoveryModule.executeRecovery( + prevOwner, + anotherOldOwner, + newOwner, + { from: recoveryControllerAddress }, + ), + 'Should revert as the execute request is not for the active recovery.', + 'The execution request\'s data does not match with the active one.', + ); + + await Utils.expectRevert( + recoveryModule.executeRecovery( + prevOwner, + oldOwner, + anotherNewOwner, + { from: recoveryControllerAddress }, + ), + 'Should revert as the execute request is not for the active recovery.', + 'The execution request\'s data does not match with the active one.', + ); + }); + + it('Reverts if required number of blocks to recover was not progressed.', async () => { + const { + recoveryControllerAddress, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + const activeRecoveryInfo = await recoveryModule.activeRecoveryInfo.call(); + + const blockNumber = (await web3.eth.getBlockNumber()); + + const delta = new BN(activeRecoveryInfo.executionBlockHeight) + .sub(new BN(blockNumber)) + .sub(new BN(1)); + + assert.isNotOk( + delta.isNeg(), + ); + + for (let i = 0; i < delta; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Utils.advanceBlock(); + } + + await Utils.expectRevert( + recoveryModule.executeRecovery( + prevOwner, + oldOwner, + newOwner, + { from: recoveryControllerAddress }, + ), + 'Should revert as required number of blocks to recover was not progressed.', + 'Required number of blocks to recover was not progressed.', + ); + }); + + it('Reverts if ModuleManager fails to execute.', async () => { + const { + moduleManager, + recoveryControllerAddress, + recoveryBlockDelay, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + for (let i = 0; i < recoveryBlockDelay; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Utils.advanceBlock(); + } + + moduleManager.makeFail(); + + await Utils.expectRevert( + recoveryModule.executeRecovery( + prevOwner, + oldOwner, + newOwner, + { from: recoveryControllerAddress }, + ), + 'Should revert as the module manager fails to execute.', + 'Recovery execution failed.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits RecoveryExecuted.', async () => { + const { + recoveryControllerAddress, + recoveryBlockDelay, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + for (let i = 0; i < recoveryBlockDelay; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Utils.advanceBlock(); + } + + const transactionResponse = await recoveryModule.executeRecovery( + prevOwner, + oldOwner, + newOwner, + { from: recoveryControllerAddress }, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'RecoveryExecuted', + args: { + _prevOwner: prevOwner, + _oldOwner: oldOwner, + _newOwner: newOwner, + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that recovery is executed properly.', async () => { + const { + recoveryControllerAddress, + recoveryBlockDelay, + moduleManager, + recoveryModule, + prevOwner, + oldOwner, + newOwner, + } = await prepare(accountProvider); + + for (let i = 0; i < recoveryBlockDelay; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Utils.advanceBlock(); + } + + let activeRecoveryInfo = await recoveryModule.activeRecoveryInfo.call(); + + assert.strictEqual( + activeRecoveryInfo.prevOwner, + prevOwner, + ); + + assert.strictEqual( + activeRecoveryInfo.oldOwner, + oldOwner, + ); + + assert.strictEqual( + activeRecoveryInfo.newOwner, + newOwner, + ); + + assert.isNotOk( + activeRecoveryInfo.executionBlockHeight.eqn(0), + ); + + await recoveryModule.executeRecovery( + prevOwner, + oldOwner, + newOwner, + { from: recoveryControllerAddress }, + ); + + assert.strictEqual( + await moduleManager.recordedTo.call(), + moduleManager.address, + ); + + assert.isOk( + (await moduleManager.recordedValue.call()).eqn(0), + ); + + assert.strictEqual( + await moduleManager.recordedData.call(), + generateSwapOwnerData(prevOwner, oldOwner, newOwner), + ); + + assert.isOk( + (await moduleManager.recordedOperation.call()).eqn(0), + ); + + activeRecoveryInfo = await recoveryModule.activeRecoveryInfo.call(); + + assert.strictEqual( + activeRecoveryInfo.prevOwner, + Utils.NULL_ADDRESS, + ); + + assert.strictEqual( + activeRecoveryInfo.oldOwner, + Utils.NULL_ADDRESS, + ); + + assert.strictEqual( + activeRecoveryInfo.newOwner, + Utils.NULL_ADDRESS, + ); + + assert.isOk( + activeRecoveryInfo.executionBlockHeight.eqn(0), + ); + }); + }); +}); diff --git a/test/delayed_recovery_module/initiate_recovery.js b/test/delayed_recovery_module/initiate_recovery.js new file mode 100644 index 0000000..38e3a01 --- /dev/null +++ b/test/delayed_recovery_module/initiate_recovery.js @@ -0,0 +1,309 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const EthUtils = require('ethereumjs-util'); +const Utils = require('../test_lib/utils.js'); +const web3 = require('../test_lib/web3.js'); +const { Event } = require('../test_lib/event_decoder.js'); +const RecoveryModuleUtils = require('./utils.js'); +const { AccountProvider } = require('../test_lib/utils.js'); + +contract('DelayedRecoveryModule::initiateRecovery', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if non-recovery controller calls.', async () => { + const { + recoveryOwnerPrivateKey, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const prevOwner = accountProvider.get(); + const oldOwner = accountProvider.get(); + const newOwner = accountProvider.get(); + + const { + signature, + } = RecoveryModuleUtils.signInitiateRecovery( + recoveryModule.address, + prevOwner, + oldOwner, + newOwner, + recoveryOwnerPrivateKey, + ); + + await Utils.expectRevert( + recoveryModule.initiateRecovery( + prevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { + from: accountProvider.get(), // not a recovery controller's address + }, + ), + 'Should revert as non-recovery controller\'s address calls.', + 'Only recovery controller is allowed to call.', + ); + }); + + it('Reverts if there is an active recovery process.', async () => { + const { + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const prevOwner1 = accountProvider.get(); + const oldOwner1 = accountProvider.get(); + const newOwner1 = accountProvider.get(); + + const { + signature: signature1, + } = RecoveryModuleUtils.signInitiateRecovery( + recoveryModule.address, + prevOwner1, + oldOwner1, + newOwner1, + recoveryOwnerPrivateKey, + ); + + await recoveryModule.initiateRecovery( + prevOwner1, + oldOwner1, + newOwner1, + EthUtils.bufferToHex(signature1.r), + EthUtils.bufferToHex(signature1.s), + signature1.v, + { from: recoveryControllerAddress }, + ); + + await Utils.expectRevert( + recoveryModule.initiateRecovery( + prevOwner1, + oldOwner1, + newOwner1, + EthUtils.bufferToHex(signature1.r), + EthUtils.bufferToHex(signature1.s), + signature1.v, + { + from: recoveryControllerAddress, + }, + ), + 'Should revert as there is an active recovery process.', + 'There is an active recovery.', + ); + + const prevOwner2 = accountProvider.get(); + const oldOwner2 = accountProvider.get(); + const newOwner2 = accountProvider.get(); + + const { + signature: signature2, + } = RecoveryModuleUtils.signInitiateRecovery( + recoveryModule.address, + prevOwner2, + oldOwner2, + newOwner2, + recoveryOwnerPrivateKey, + ); + + await Utils.expectRevert( + recoveryModule.initiateRecovery( + prevOwner2, + oldOwner2, + newOwner2, + EthUtils.bufferToHex(signature2.r), + EthUtils.bufferToHex(signature2.s), + signature2.v, + { + from: recoveryControllerAddress, + }, + ), + 'Should revert as there is an active recovery process.', + 'There is an active recovery.', + ); + }); + + it('Reverts if recovery message is not signed by owner.', async () => { + const { + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const prevOwner = accountProvider.get(); + const oldOwner = accountProvider.get(); + const newOwner = accountProvider.get(); + + const privateKey = '0x038764453ef1dbdf9cfb3923f95d22a8974a1aa2f7351737b46d9ea25aaba50a'; + + assert.notStrictEqual( + recoveryOwnerPrivateKey, + privateKey, + ); + + const { + signature, + } = RecoveryModuleUtils.signInitiateRecovery( + recoveryModule.address, + prevOwner, + oldOwner, + newOwner, + privateKey, + ); + + await Utils.expectRevert( + recoveryModule.initiateRecovery( + prevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: recoveryControllerAddress }, + ), + 'Should revert as recovery message is not signed by the recovery owner.', + 'Invalid signature for recovery owner.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits RecoveryInitiated.', async () => { + const { + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const prevOwner = accountProvider.get(); + const oldOwner = accountProvider.get(); + const newOwner = accountProvider.get(); + + const { + signature, + } = RecoveryModuleUtils.signInitiateRecovery( + recoveryModule.address, + prevOwner, + oldOwner, + newOwner, + recoveryOwnerPrivateKey, + ); + + const transactionResponse = await recoveryModule.initiateRecovery( + prevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: recoveryControllerAddress }, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'RecoveryInitiated', + args: { + _prevOwner: prevOwner, + _oldOwner: oldOwner, + _newOwner: newOwner, + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that recovery is initiated properly.', async () => { + const { + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryBlockDelay, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const prevOwner = accountProvider.get(); + const oldOwner = accountProvider.get(); + const newOwner = accountProvider.get(); + + const { + signature, + } = RecoveryModuleUtils.signInitiateRecovery( + recoveryModule.address, + prevOwner, + oldOwner, + newOwner, + recoveryOwnerPrivateKey, + ); + + await recoveryModule.initiateRecovery( + prevOwner, + oldOwner, + newOwner, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: recoveryControllerAddress }, + ); + const blockNumber = (await web3.eth.getBlockNumber()); + + const activeRecoveryInfo = await recoveryModule.activeRecoveryInfo.call(); + + assert.strictEqual( + activeRecoveryInfo.prevOwner, + prevOwner, + ); + + assert.strictEqual( + activeRecoveryInfo.oldOwner, + oldOwner, + ); + + assert.strictEqual( + activeRecoveryInfo.newOwner, + newOwner, + ); + + assert.isOk( + (activeRecoveryInfo.executionBlockHeight).eqn(blockNumber + recoveryBlockDelay), + ); + }); + }); +}); diff --git a/test/delayed_recovery_module/reset_recovery_owner.js b/test/delayed_recovery_module/reset_recovery_owner.js new file mode 100644 index 0000000..a0fbc64 --- /dev/null +++ b/test/delayed_recovery_module/reset_recovery_owner.js @@ -0,0 +1,224 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const EthUtils = require('ethereumjs-util'); +const Utils = require('../test_lib/utils.js'); +const { Event } = require('../test_lib/event_decoder.js'); +const RecoveryModuleUtils = require('./utils.js'); +const { AccountProvider } = require('../test_lib/utils.js'); + +const NEW_RECOVERY_OWNER_ADDRESS = '0x798c2Eaa13BaD12215ba71A3b71471042945B6fE'; + +contract('DelayedRecoveryModule::resetRecoveryOwner', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if non-recovery controller calls.', async () => { + const { + recoveryOwnerAddress, + recoveryOwnerPrivateKey, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const { + signature, + } = RecoveryModuleUtils.signResetRecoveryOwner( + recoveryModule.address, + recoveryOwnerAddress, + NEW_RECOVERY_OWNER_ADDRESS, + recoveryOwnerPrivateKey, + ); + + await Utils.expectRevert( + recoveryModule.resetRecoveryOwner( + NEW_RECOVERY_OWNER_ADDRESS, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { + from: accountProvider.get(), // not a recovery controller's address + }, + ), + 'Should revert as non-recovery controller\'s address calls.', + 'Only recovery controller is allowed to call.', + ); + }); + + it('Reverts if a new recovery owner address is null.', async () => { + const { + recoveryOwnerAddress, + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const { + signature, + } = RecoveryModuleUtils.signResetRecoveryOwner( + recoveryModule.address, + recoveryOwnerAddress, + Utils.NULL_ADDRESS, + recoveryOwnerPrivateKey, + ); + + await Utils.expectRevert( + recoveryModule.resetRecoveryOwner( + Utils.NULL_ADDRESS, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: recoveryControllerAddress }, + ), + 'Should revert as the new recovery owner address is null.', + 'New recovery owner\'s address is null.', + ); + }); + + it('Reverts if the message is not signed by the current owner key.', async () => { + const { + recoveryOwnerAddress, + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const privateKey = '0x038764453ef1dbdf9cfb3923f95d22a8974a1aa2f7351737b46d9ea25aaba50a'; + + assert.notStrictEqual( + recoveryOwnerPrivateKey, + privateKey, + ); + + const { + signature, + } = RecoveryModuleUtils.signResetRecoveryOwner( + recoveryModule.address, + recoveryOwnerAddress, + NEW_RECOVERY_OWNER_ADDRESS, + privateKey, + ); + + await Utils.expectRevert( + recoveryModule.resetRecoveryOwner( + NEW_RECOVERY_OWNER_ADDRESS, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: recoveryControllerAddress }, + ), + 'Should revert as the message is not signed by the recovery owner.', + 'Invalid signature for recovery owner.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits ResetRecoveryOwner.', async () => { + const { + recoveryOwnerAddress, + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const { + signature, + } = RecoveryModuleUtils.signResetRecoveryOwner( + recoveryModule.address, + recoveryOwnerAddress, + NEW_RECOVERY_OWNER_ADDRESS, + recoveryOwnerPrivateKey, + ); + + const transactionResponse = await recoveryModule.resetRecoveryOwner( + NEW_RECOVERY_OWNER_ADDRESS, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: recoveryControllerAddress }, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'ResetRecoveryOwner', + args: { + _oldRecoveryOwner: recoveryOwnerAddress, + _newRecoveryOwner: NEW_RECOVERY_OWNER_ADDRESS, + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that resetting recovery owner\'s address is done properly.', async () => { + const { + recoveryOwnerAddress, + recoveryOwnerPrivateKey, + recoveryControllerAddress, + recoveryModule, + } = await RecoveryModuleUtils.createRecoveryModule( + accountProvider, + ); + + const { + signature, + } = RecoveryModuleUtils.signResetRecoveryOwner( + recoveryModule.address, + recoveryOwnerAddress, + NEW_RECOVERY_OWNER_ADDRESS, + recoveryOwnerPrivateKey, + ); + + assert.strictEqual( + await recoveryModule.recoveryOwner.call(), + recoveryOwnerAddress, + ); + + await recoveryModule.resetRecoveryOwner( + NEW_RECOVERY_OWNER_ADDRESS, + EthUtils.bufferToHex(signature.r), + EthUtils.bufferToHex(signature.s), + signature.v, + { from: recoveryControllerAddress }, + ); + + assert.strictEqual( + await recoveryModule.recoveryOwner.call(), + NEW_RECOVERY_OWNER_ADDRESS, + ); + }); + }); +}); diff --git a/test/delayed_recovery_module/setup.js b/test/delayed_recovery_module/setup.js new file mode 100644 index 0000000..ddf586c --- /dev/null +++ b/test/delayed_recovery_module/setup.js @@ -0,0 +1,187 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const Utils = require('../test_lib/utils.js'); +const DelayedRecoveryModuleUtils = require('./utils.js'); +const web3 = require('../test_lib/web3.js'); +const { AccountProvider } = require('../test_lib/utils.js'); + +const DelayedRecoveryModule = artifacts.require('DelayedRecoveryModule'); + +contract('DelayedRecoveryModule::setup', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if recovery owner\'s address is null.', async () => { + const recoveryModule = await DelayedRecoveryModule.new(); + + await Utils.expectRevert( + recoveryModule.setup( + Utils.NULL_ADDRESS, // recovery owner's address + accountProvider.get(), // recovery controller's address + DelayedRecoveryModuleUtils.BLOCK_RECOVERY_DELAY, // recovery block delays + ), + 'Should revert as recovery owner\'s address is null.', + 'Recovery owner\'s address is null.', + ); + }); + + it('Reverts if recovery controllers\'s address is null.', async () => { + const recoveryModule = await DelayedRecoveryModule.new(); + + await Utils.expectRevert( + recoveryModule.setup( + accountProvider.get(), // recovery owner's address + Utils.NULL_ADDRESS, // recovery controller's address + DelayedRecoveryModuleUtils.BLOCK_RECOVERY_DELAY, // recovery block delays + ), + 'Should revert as recovery controlers\'s address is null.', + 'Recovery controller\'s address is null.', + ); + }); + + it('Reverts if recovery block delay is 0.', async () => { + const recoveryModule = await DelayedRecoveryModule.new(); + + await Utils.expectRevert( + recoveryModule.setup( + accountProvider.get(), // recovery owner's address + accountProvider.get(), // recovery controller's address + 0, // recovery block delays + ), + 'Should revert as recovery block delay is 0.', + 'Recovery block delay is 0.', + ); + }); + + it('Reverts if called second time.', async () => { + const recoveryModule = await DelayedRecoveryModule.new(); + + await recoveryModule.setup( + accountProvider.get(), // recovery owner's address + accountProvider.get(), // recovery controller's address + DelayedRecoveryModuleUtils.BLOCK_RECOVERY_DELAY, // recovery block delays + ); + + await Utils.expectRevert( + recoveryModule.setup( + accountProvider.get(), // recovery owner's address + accountProvider.get(), // recovery controller's address + DelayedRecoveryModuleUtils.BLOCK_RECOVERY_DELAY, // recovery block delays + ), + 'Should revert as setup is called second time.', + 'Domain separator was already set.', + ); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that passed arguments are set correctly.', async () => { + const recoveryModule = await DelayedRecoveryModule.new(); + + const recoveryOwner = accountProvider.get(); + const recoveryController = accountProvider.get(); + const recoveryBlockDelay = DelayedRecoveryModuleUtils.BLOCK_RECOVERY_DELAY; + + const moduleManager = accountProvider.get(); + + await recoveryModule.setup( + recoveryOwner, + recoveryController, + recoveryBlockDelay, + { + from: moduleManager, + }, + ); + + assert.strictEqual( + await recoveryModule.domainSeparator.call(), + DelayedRecoveryModuleUtils.hashRecoveryModuleDomainSeparator( + recoveryModule.address, + ), + ); + + assert.strictEqual( + await recoveryModule.recoveryOwner.call(), + recoveryOwner, + ); + + assert.strictEqual( + await recoveryModule.recoveryController.call(), + recoveryController, + ); + + assert.isOk( + (await recoveryModule.recoveryBlockDelay.call()).eqn( + recoveryBlockDelay, + ), + ); + + assert.strictEqual( + await recoveryModule.manager.call(), + moduleManager, + ); + + const activeRecoveryInfo = await recoveryModule.activeRecoveryInfo.call(); + + assert.strictEqual( + activeRecoveryInfo.prevOwner, + Utils.NULL_ADDRESS, + ); + + assert.strictEqual( + activeRecoveryInfo.oldOwner, + Utils.NULL_ADDRESS, + ); + + assert.strictEqual( + activeRecoveryInfo.newOwner, + Utils.NULL_ADDRESS, + ); + + assert.isOk( + (activeRecoveryInfo.executionBlockHeight).eqn(0), + ); + + assert.isNotOk( + activeRecoveryInfo.initiated, + ); + }); + + it('Checks storage elements order to assure reserved ' + + 'slot for proxy is valid.', async () => { + const recoveryModule = await DelayedRecoveryModule.new(); + + const recoveryOwner = accountProvider.get(); + const recoveryController = accountProvider.get(); + const recoveryBlockDelay = DelayedRecoveryModuleUtils.BLOCK_RECOVERY_DELAY; + + await recoveryModule.setup( + recoveryOwner, + recoveryController, + recoveryBlockDelay, + ); + + + assert.strictEqual( + (await web3.eth.getStorageAt(recoveryModule.address, 0)), + '0x00', + ); + }); + }); +}); diff --git a/test/delayed_recovery_module/utils.js b/test/delayed_recovery_module/utils.js new file mode 100644 index 0000000..c50f3e3 --- /dev/null +++ b/test/delayed_recovery_module/utils.js @@ -0,0 +1,332 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const EthUtils = require('ethereumjs-util'); +const web3 = require('../test_lib/web3.js'); +const Utils = require('../test_lib/utils.js'); + +const DelayedRecoveryModule = artifacts.require('DelayedRecoveryModule'); +const GnosisSafeModuleManagerSpy = artifacts.require('GnosisSafeModuleManagerSpy'); + +// @todo [Pro]: Enable this once figured out how to test! +// const BLOCK_RECOVERY_DELAY = 4 * 84600; +const BLOCK_RECOVERY_DELAY = 50; + +const RECOERY_MODULE_DOMAIN_SEPARATOR_TYPEHASH = web3.utils.keccak256( + 'EIP712Domain(address verifyingContract)', +); + +const INITIATE_RECOVERY_STRUCT_TYPEHASH = web3.utils.keccak256( + 'InitiateRecoveryStruct(address prevOwner,address oldOwner,address newOwner)', +); + +const EXECUTE_RECOVERY_STRUCT_TYPEHASH = web3.utils.keccak256( + 'ExecuteRecoveryStruct(address prevOwner,address oldOwner,address newOwner)', +); + +const ABORT_RECOVERY_STRUCT_TYPEHASH = web3.utils.keccak256( + 'AbortRecoveryStruct(address prevOwner,address oldOwner,address newOwner)', +); + +const RESET_RECOVERY_OWNER_STRUCT_TYPEHASH = web3.utils.keccak256( + 'ResetRecoveryOwnerStruct(address oldRecoveryOwner,address newRecoveryOwner)', +); + +const RECOVERY_OWNER_ADDRESS = '0xC9EfFC17034eFA68b445db8618294e9500144D96'; +const RECOVERY_OWNER_PRIVATE_KEY = '0xc5dff061ed33bf9fe42f8071d7bf6cd168bec5593e3c19a344ba35d50f37768d'; + +async function createRecoveryModule(accountProvider) { + const recoveryOwnerAddress = RECOVERY_OWNER_ADDRESS; + const recoveryControllerAddress = accountProvider.get(); + const recoveryBlockDelay = BLOCK_RECOVERY_DELAY; + + const moduleManager = await GnosisSafeModuleManagerSpy.new(); + const transactionResponse = await moduleManager.createDelayedRecoveryModule( + recoveryOwnerAddress, + recoveryControllerAddress, + BLOCK_RECOVERY_DELAY, + ); + + const recoveryModuleAddress = Utils.getParamFromTxEvent( + transactionResponse, + moduleManager.address, + 'DelayedRedcoveryModuleCreated', + '_contractAddress', + ); + + const recoveryModule = await DelayedRecoveryModule.at(recoveryModuleAddress); + + return { + recoveryOwnerAddress, + recoveryOwnerPrivateKey: RECOVERY_OWNER_PRIVATE_KEY, + recoveryControllerAddress, + recoveryBlockDelay, + moduleManager, + recoveryModule, + }; +} + +function hashRecoveryModuleDomainSeparator(recoveryModuleAddress) { + return web3.utils.keccak256( + web3.eth.abi.encodeParameters( + [ + 'bytes32', + 'address', + ], + [ + RECOERY_MODULE_DOMAIN_SEPARATOR_TYPEHASH, + recoveryModuleAddress, + ], + ), + ); +} + +function hashRecoveryModuleRecoveryStruct( + structTypeHash, prevOwner, oldOwner, newOwner, +) { + return web3.utils.keccak256( + web3.eth.abi.encodeParameters( + [ + 'bytes32', + 'address', + 'address', + 'address', + ], + [ + structTypeHash, + prevOwner, + oldOwner, + newOwner, + ], + ), + ); +} + +function hashRecoveryModuleInitiateRecoveryStruct( + prevOwner, oldOwner, newOwner, +) { + return hashRecoveryModuleRecoveryStruct( + INITIATE_RECOVERY_STRUCT_TYPEHASH, + prevOwner, + oldOwner, + newOwner, + ); +} + +function hashRecoveryModuleExecuteRecoveryStruct( + prevOwner, oldOwner, newOwner, +) { + return hashRecoveryModuleRecoveryStruct( + EXECUTE_RECOVERY_STRUCT_TYPEHASH, + prevOwner, + oldOwner, + newOwner, + ); +} + +function hashRecoveryModuleAbortRecoveryStruct( + prevOwner, oldOwner, newOwner, +) { + return hashRecoveryModuleRecoveryStruct( + ABORT_RECOVERY_STRUCT_TYPEHASH, + prevOwner, + oldOwner, + newOwner, + ); +} + +function hashRecoveryModuleRecovery( + recoveryModuleAddress, structTypeHash, prevOwner, oldOwner, newOwner, +) { + const recoveryStructHash = hashRecoveryModuleRecoveryStruct( + structTypeHash, + prevOwner, + oldOwner, + newOwner, + ); + + const domainSeparatorHash = hashRecoveryModuleDomainSeparator( + recoveryModuleAddress, + ); + + return web3.utils.soliditySha3( + { t: 'bytes1', v: '0x19' }, + { t: 'bytes1', v: '0x01' }, + { t: 'bytes32', v: domainSeparatorHash }, + { t: 'bytes32', v: recoveryStructHash }, + ); +} + +function hashRecoveryModuleInitiateRecovery( + recoveryModuleAddress, prevOwner, oldOwner, newOwner, +) { + return hashRecoveryModuleRecovery( + recoveryModuleAddress, + INITIATE_RECOVERY_STRUCT_TYPEHASH, prevOwner, oldOwner, newOwner, + ); +} + +function hashRecoveryModuleExecuteRecovery( + recoveryModuleAddress, prevOwner, oldOwner, newOwner, +) { + return hashRecoveryModuleRecovery( + recoveryModuleAddress, + EXECUTE_RECOVERY_STRUCT_TYPEHASH, prevOwner, oldOwner, newOwner, + ); +} + +function hashRecoveryModuleAbortRecovery( + recoveryModuleAddress, prevOwner, oldOwner, newOwner, +) { + return hashRecoveryModuleRecovery( + recoveryModuleAddress, + ABORT_RECOVERY_STRUCT_TYPEHASH, prevOwner, oldOwner, newOwner, + ); +} + +function hashRecoveryModuleResetOwnerStruct( + oldRecoveryOwner, newRecoveryOwner, +) { + return web3.utils.keccak256( + web3.eth.abi.encodeParameters( + [ + 'bytes32', + 'address', + 'address', + ], + [ + RESET_RECOVERY_OWNER_STRUCT_TYPEHASH, + oldRecoveryOwner, + newRecoveryOwner, + ], + ), + ); +} + +function hashRecoveryModuleResetOwner( + recoveryModuleAddress, oldRecoveryOwner, newRecoveryOwner, +) { + const domainSeparatorHash = hashRecoveryModuleDomainSeparator( + recoveryModuleAddress, + ); + + const resetOwnerStructHash = hashRecoveryModuleResetOwnerStruct( + oldRecoveryOwner, newRecoveryOwner, + ); + + return web3.utils.soliditySha3( + { t: 'bytes1', v: '0x19' }, + { t: 'bytes1', v: '0x01' }, + { t: 'bytes32', v: domainSeparatorHash }, + { t: 'bytes32', v: resetOwnerStructHash }, + ); +} + +function signRecovery( + recoveryModuleAddress, structTypeHash, prevOwner, oldOwner, newOwner, recoveryOwnerPrivateKey, +) { + const recoveryHash = hashRecoveryModuleRecovery( + recoveryModuleAddress, structTypeHash, prevOwner, oldOwner, newOwner, + ); + + const signature = EthUtils.ecsign( + EthUtils.toBuffer(recoveryHash), + EthUtils.toBuffer(recoveryOwnerPrivateKey), + ); + + return { + recoveryHash, + signature, + }; +} + +function signInitiateRecovery( + recoveryModuleAddress, prevOwner, oldOwner, newOwner, recoveryOwnerPrivateKey, +) { + return signRecovery( + recoveryModuleAddress, INITIATE_RECOVERY_STRUCT_TYPEHASH, + prevOwner, oldOwner, newOwner, recoveryOwnerPrivateKey, + ); +} + +function signExecuteRecovery( + recoveryModuleAddress, prevOwner, oldOwner, newOwner, recoveryOwnerPrivateKey, +) { + return signRecovery( + recoveryModuleAddress, EXECUTE_RECOVERY_STRUCT_TYPEHASH, + prevOwner, oldOwner, newOwner, recoveryOwnerPrivateKey, + ); +} + +function signAbortRecovery( + recoveryModuleAddress, prevOwner, oldOwner, newOwner, recoveryOwnerPrivateKey, +) { + return signRecovery( + recoveryModuleAddress, ABORT_RECOVERY_STRUCT_TYPEHASH, + prevOwner, oldOwner, newOwner, recoveryOwnerPrivateKey, + ); +} + +function signResetRecoveryOwner( + recoveryModuleAddress, oldRecoveryOwner, newRecoveryOwner, recoveryOwnerPrivateKey, +) { + const resetRecoveryOwnerHash = hashRecoveryModuleResetOwner( + recoveryModuleAddress, oldRecoveryOwner, newRecoveryOwner, + ); + + const signature = EthUtils.ecsign( + EthUtils.toBuffer(resetRecoveryOwnerHash), + EthUtils.toBuffer(recoveryOwnerPrivateKey), + ); + + return { + resetRecoveryOwnerHash, + signature, + }; +} + +module.exports = { + + BLOCK_RECOVERY_DELAY, + + createRecoveryModule, + + hashRecoveryModuleDomainSeparator, + + hashRecoveryModuleInitiateRecoveryStruct, + + hashRecoveryModuleExecuteRecoveryStruct, + + hashRecoveryModuleAbortRecoveryStruct, + + hashRecoveryModuleInitiateRecovery, + + hashRecoveryModuleExecuteRecovery, + + hashRecoveryModuleAbortRecovery, + + hashRecoveryModuleResetOwnerStruct, + + hashRecoveryModuleResetOwner, + + signInitiateRecovery, + + signExecuteRecovery, + + signAbortRecovery, + + signResetRecoveryOwner, +}; diff --git a/test/multisigwallet/confirm_transaction.js b/test/multisigwallet/confirm_transaction.js deleted file mode 100644 index 5e01ae4..0000000 --- a/test/multisigwallet/confirm_transaction.js +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// ---------------------------------------------------------------------------- -// Test: MultiSigWallet::confirmTransaction -// -// http://www.simpletoken.org/ -// -// ---------------------------------------------------------------------------- - -const BN = require('bn.js'); -const utils = require('../test_lib/utils.js'); -const { Event } = require('../test_lib/event_decoder.js'); -const { AccountProvider } = require('../test_lib/utils.js'); -const { MultiSigWalletUtils } = require('./utils.js'); - - -contract('MultiSigWallet::confirmTransaction', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-registered wallet calls.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - await utils.expectRevert( - helper.multisig().confirmTransaction( - transactionID, - { from: accountProvider.get() }, - ), - 'Should revert as non-registered wallet calls.', - 'Only wallet is allowed to call.', - ); - }); - - it('Reverts if transaction ID to confirm does not exist.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - const nonExistingTransactionID = 11; - - await utils.expectRevert( - helper.multisig().confirmTransaction( - nonExistingTransactionID, - { from: helper.wallet(0) }, - ), - 'Should revert as transaction does not exist.', - 'Transaction does not exist.', - ); - }); - - it('Reverts if the wallet has already confirmed the transaction.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - await utils.expectRevert( - helper.multisig().confirmTransaction( - transactionID, - { from: helper.wallet(0) }, - ), - 'Should revert as wallet has already confirmed the transaction.', - 'Transaction is confirmed by the wallet.', - ); - }); - }); - - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits TransactionConfirmed once wallet confirms transaction.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 3, required: 3 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - const transactionResponse = await helper.multisig().confirmTransaction( - transactionID, - { from: helper.wallet(1) }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 1, - ); - - // The first emitted event should be 'TransactionConfirmed'. - Event.assertEqual(events[0], { - name: 'TransactionConfirmed', - args: { - _transactionID: new BN(transactionID), - _wallet: helper.wallet(1), - }, - }); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks that transaction is confirmed by the wallet after ' - + 'successfull call.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - assert.isNotOk( - (await helper.multisig().transactions.call(transactionID)).executed, - 'Transaction should not be confirmed because of ' - + '2-wallets-2-required setup.', - ); - - await helper.multisig().confirmTransaction( - transactionID, { from: helper.wallet(1) }, - ); - - assert.isOk( - (await helper.multisig().transactions.call(transactionID)).executed, - 'Transaction should be confirmed because two wallets ' - + 'has confirmed it.', - ); - }); - }); -}); diff --git a/test/multisigwallet/constructor.js b/test/multisigwallet/constructor.js deleted file mode 100644 index aa47874..0000000 --- a/test/multisigwallet/constructor.js +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -const utils = require('../test_lib/utils.js'); -const { AccountProvider } = require('../test_lib/utils.js'); - -const MultiSigWallet = artifacts.require('MultiSigWallet'); - -contract('MultiSigWallet::constructor', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if wallets array contains null address.', async () => { - const registeredWallet0 = accountProvider.get(); - const nullWallet = utils.NULL_ADDRESS; - - const walletsWithNull = [registeredWallet0, nullWallet]; - - await utils.expectRevert( - MultiSigWallet.new(walletsWithNull, 1), - 'Should revert as wallets array contains null address.', - 'Wallet address is 0.', - ); - }); - - it('Reverts if wallets array contains duplicate entries.', async () => { - const registeredWallet0 = accountProvider.get(); - const walletsWithDuplicate = [registeredWallet0, registeredWallet0]; - - await utils.expectRevert( - MultiSigWallet.new(walletsWithDuplicate, 1), - 'Should revert as wallets array contains duplicate entry.', - 'Duplicate wallet address.', - ); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks that passed arguments are set correctly.', async () => { - const required = 2; - const registeredWallet0 = accountProvider.get(); - const registeredWallet1 = accountProvider.get(); - - const wallets = [registeredWallet0, registeredWallet1]; - - const multisig = await MultiSigWallet.new(wallets, required); - - assert.isOk( - (await multisig.required.call()).eqn(required), - ); - - assert.isOk( - (await multisig.walletCount.call()).eqn(2), - ); - - assert.strictEqual( - await multisig.wallets.call(0), - registeredWallet0, - ); - - assert.strictEqual( - await multisig.wallets.call(1), - registeredWallet1, - ); - - assert.isOk( - await multisig.isWallet.call(registeredWallet0), - ); - - assert.isOk( - await multisig.isWallet.call(registeredWallet1), - ); - - assert.isOk( - (await multisig.required.call()).eqn(required), - 'After submitting a transaction, the transaction count is ' - + 'incremented by one, hence it should be equal to 1', - ); - - assert.isOk( - (await multisig.transactionCount.call()).eqn(0), - ); - }); - }); -}); diff --git a/test/multisigwallet/execute_transaction.js b/test/multisigwallet/execute_transaction.js deleted file mode 100644 index 08d4ff1..0000000 --- a/test/multisigwallet/execute_transaction.js +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// ---------------------------------------------------------------------------- -// Test: MultiSigWallet::executeTransaction -// -// http://www.simpletoken.org/ -// -// ---------------------------------------------------------------------------- - -const BN = require('bn.js'); -const utils = require('../test_lib/utils.js'); -const { Event } = require('../test_lib/event_decoder.js'); -const { AccountProvider } = require('../test_lib/utils.js'); -const { MultiSigWalletUtils } = require('./utils.js'); - -const MultiSigWalletDouble = artifacts.require('MultiSigWalletDouble'); - -contract('MultiSigWallet::executeTransaction', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-registered wallet calls.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - await utils.expectRevert( - helper.multisig().executeTransaction( - transactionID, - { from: accountProvider.get() }, - ), - 'Should revert as non-registered wallet calls.', - 'Only wallet is allowed to call.', - ); - }); - - it('Reverts if transaction ID does not exist.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const nonExistingTransactionID = 11; - - await utils.expectRevert( - helper.multisig().executeTransaction( - nonExistingTransactionID, - { from: helper.wallet(0) }, - ), - 'Should revert as transaction ID does not exist.', - 'Transaction does not exist.', - ); - }); - - it('Reverts if execution requested by wallet that did not confirmed.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - await utils.expectRevert( - helper.multisig().executeTransaction( - transactionID, - { from: helper.wallet(1) }, - ), - ); - }); - - it('Reverts if the transaction was already executed.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - await utils.expectRevert( - helper.multisig().executeTransaction( - transactionID, - { from: helper.wallet(0) }, - ), - ); - }); - }); - - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits TransactionExecutionSucceeded on successfull execution.', async () => { - const required = 1; - - const registeredWallet0 = accountProvider.get(); - - const wallets = [registeredWallet0]; - - const multisigDouble = await MultiSigWalletDouble.new( - wallets, required, - ); - - await multisigDouble.makeFooThrow(); - - const transactionID = await multisigDouble.submitFoo.call( - { from: registeredWallet0 }, - ); - - await multisigDouble.submitFoo( - { from: registeredWallet0 }, - ); - - await multisigDouble.makeFooNotThrow(); - const transactionResponse = await multisigDouble.executeTransaction(transactionID); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 1, - ); - - // The first emitted event should be 'TransactionExecutionSucceeded'. - Event.assertEqual(events[0], { - name: 'TransactionExecutionSucceeded', - args: { - _transactionID: new BN(transactionID), - }, - }); - }); - - it('Emits TransactionExecutionFailed on failure.', async () => { - const required = 1; - - const registeredWallet0 = accountProvider.get(); - - const wallets = [registeredWallet0]; - - const multisigDouble = await MultiSigWalletDouble.new( - wallets, required, - ); - - await multisigDouble.makeFooThrow(); - - const transactionID = await multisigDouble.submitFoo.call( - { from: registeredWallet0 }, - ); - - await multisigDouble.submitFoo( - { from: registeredWallet0 }, - ); - - const transactionResponse = await multisigDouble.executeTransaction( - transactionID, - { from: registeredWallet0 }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 1, - ); - - // The first emitted event should be 'TransactionExecutionFailed'. - Event.assertEqual(events[0], { - name: 'TransactionExecutionFailed', - args: { - _transactionID: new BN(transactionID), - }, - }); - }); - }); - - contract('Execution', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks that request passes successfully.', async () => { - const required = 1; - - const registeredWallet0 = accountProvider.get(); - - const wallets = [registeredWallet0]; - - const multisigDouble = await MultiSigWalletDouble.new( - wallets, required, - ); - - await multisigDouble.makeFooThrow(); - - const transactionID = await multisigDouble.submitFoo.call( - { from: registeredWallet0 }, - ); - - await multisigDouble.submitFoo( - { from: registeredWallet0 }, - ); - - await multisigDouble.makeFooNotThrow(); - - await multisigDouble.executeTransaction( - transactionID, - { from: registeredWallet0 }, - ); - - assert.isOk( - (await multisigDouble.transactions.call(transactionID)).executed, - 'Transaction should pass as foo is set to succeed.', - ); - }); - - it('Checks that request fails.', async () => { - const required = 1; - - const registeredWallet0 = accountProvider.get(); - - const wallets = [registeredWallet0]; - - const multisigDouble = await MultiSigWalletDouble.new( - wallets, required, - ); - - await multisigDouble.makeFooThrow(); - - const transactionID = await multisigDouble.submitFoo.call( - { from: registeredWallet0 }, - ); - - await multisigDouble.submitFoo( - { from: registeredWallet0 }, - ); - - await multisigDouble.executeTransaction( - transactionID, - { from: registeredWallet0 }, - ); - - assert.isNotOk( - (await multisigDouble.transactions.call(transactionID)).executed, - 'Transaction should not be executed in this stage ' - + 'as foo is set to throw.', - ); - }); - }); -}); diff --git a/test/multisigwallet/is_transaction_confirmed.js b/test/multisigwallet/is_transaction_confirmed.js deleted file mode 100644 index 039ae36..0000000 --- a/test/multisigwallet/is_transaction_confirmed.js +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const utils = require('../test_lib/utils.js'); -const { AccountProvider } = require('../test_lib/utils.js'); -const { MultiSigWalletUtils } = require('./utils.js'); - -contract('MultiSigWallet::isTransactionConfirmed', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if transaction ID does not exist.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - const nonExistingTransactionID = 11; - - await utils.expectRevert( - helper.multisig().isTransactionConfirmed.call( - nonExistingTransactionID, - { from: helper.wallet(0) }, - ), - 'Should revert as transaction ID does not exist.', - 'Transaction does not exist.', - ); - }); - - it('Checks transaction confirmation status for 1-wallet-1-required setup.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - assert.isOk( - await helper.multisig().isTransactionConfirmed.call(transactionID), - 'Because of required being 1 the transaction would be immediately ' - + 'confirmed by the submitter.', - ); - }); - - it('Checks transaction confirmation status for 2-wallets-2-required setup.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const addWalletTransactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - assert.isNotOk( - await helper.multisig().isTransactionConfirmed.call(addWalletTransactionID), - 'Because of required being 2 the transaction is not yet confirmed.', - ); - - await helper.multisig().confirmTransaction( - addWalletTransactionID, - { from: helper.wallet(1) }, - ); - - assert.isOk( - await helper.multisig().isTransactionConfirmed.call(addWalletTransactionID), - 'Transaction should be confirmed because the submitter confirms ' - + 'the transaction in the same call, and second wallet has just ' - + 'confirmed it', - ); - - const replaceWalletTransactionID = await helper.submitReplaceWallet( - 1, accountProvider.get(), 0, - ); - - await helper.multisig().confirmTransaction( - replaceWalletTransactionID, - { from: helper.wallet(1) }, - ); - - assert.isOk( - await helper.multisig().isTransactionConfirmed.call(addWalletTransactionID), - 'Despite that the wallet that has confirmed the transaction ' - + 'was replaced with the one that has not yet confirmed it, ' - + 'function returns true because the transaction was executed', - ); - }); -}); diff --git a/test/multisigwallet/revoke_confirmation.js b/test/multisigwallet/revoke_confirmation.js deleted file mode 100644 index 6282481..0000000 --- a/test/multisigwallet/revoke_confirmation.js +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// ---------------------------------------------------------------------------- -// Test: MultiSigWallet::revokeConfirmation -// -// http://www.simpletoken.org/ -// -// ---------------------------------------------------------------------------- - -const BN = require('bn.js'); -const utils = require('../test_lib/utils.js'); -const { Event } = require('../test_lib/event_decoder.js'); -const { AccountProvider } = require('../test_lib/utils.js'); -const { MultiSigWalletUtils } = require('./utils.js'); - -contract('MultiSigWallet::revokeConfirmation', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-registered wallet calls.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - await utils.expectRevert( - helper.multisig().revokeConfirmation( - transactionID, - { - from: accountProvider.get(), - }, - ), - 'Should revert as non-registered wallet calls.', - 'Only wallet is allowed to call.', - ); - }); - - it('Reverts if transaction ID does not exist.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - const nonExistingTransactionID = 11; - - await utils.expectRevert( - helper.multisig().revokeConfirmation( - nonExistingTransactionID, - { from: helper.wallet(0) }, - ), - 'Should revert as transaction ID does not exist.', - 'Transaction does not exist.', - ); - }); - - it('Reverts if revocation request was not confirmed by caller wallet.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - await utils.expectRevert( - helper.multisig().revokeConfirmation( - transactionID, - { from: helper.wallet(1) }, - ), - 'Should revert as caller wallet did not confirm the transaction.', - 'Transaction is not confirmed by the wallet.', - ); - }); - - it('Reverts if revocation is requested for already executed transaction.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - await utils.expectRevert( - helper.multisig().revokeConfirmation( - transactionID, - { from: helper.wallet(0) }, - ), - ); - }); - }); - - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits TransactionConfirmationRevoked on successfull revocation.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - const transactionResponse = await helper.multisig().revokeConfirmation( - transactionID, - { from: helper.wallet(0) }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 1, - ); - - // The first emitted event should be 'TransactionConfirmationRevoked'. - Event.assertEqual(events[0], { - name: 'TransactionConfirmationRevoked', - args: { - _transactionID: new BN(0), - _wallet: helper.wallet(0), - }, - }); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks that wallet confirmation flag is cleared.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const transactionID = await helper.submitAddWallet( - accountProvider.get(), 0, - ); - - assert.isOk( - await helper.multisig().confirmations( - transactionID, helper.wallet(0), - ), - ); - - await helper.multisig().revokeConfirmation( - transactionID, - { from: helper.wallet(0) }, - ); - - assert.isNotOk( - await helper.multisig().confirmations( - transactionID, helper.wallet(0), - ), - ); - }); - }); -}); diff --git a/test/multisigwallet/submit_add_wallet.js b/test/multisigwallet/submit_add_wallet.js deleted file mode 100644 index 1f629fc..0000000 --- a/test/multisigwallet/submit_add_wallet.js +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const BN = require('bn.js'); -const web3 = require('../test_lib/web3.js'); -const utils = require('../test_lib/utils.js'); -const { Event } = require('../test_lib/event_decoder'); -const { AccountProvider } = require('../test_lib/utils.js'); -const { MultiSigWalletUtils } = require('./utils.js'); - -function generateAddWalletData(wallet) { - return web3.eth.abi.encodeFunctionCall( - { - name: 'addWallet', - type: 'function', - inputs: [{ - type: 'address', - name: '', - }], - }, - [wallet], - ); -} - -contract('MultiSigWallet::submitAddWallet', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-registered wallet calls.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - await utils.expectRevert( - helper.multisig().submitAddWallet( - accountProvider.get(), - { from: accountProvider.get() }, - ), - 'Should revert as non-registered wallet calls.', - 'Only wallet is allowed to call.', - ); - }); - - it('Reverts if a null wallet is submitted for addition.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - await utils.expectRevert( - helper.multisig().submitAddWallet( - utils.NULL_ADDRESS, - { from: helper.wallet(0) }, - ), - 'Should revert as the submitted wallet is null.', - 'Wallet address is null.', - ); - }); - - it('Reverts if the submitted wallet already exists.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - await utils.expectRevert( - helper.multisig().submitAddWallet( - helper.wallet(1), - { from: helper.wallet(0) }, - ), - 'Should revert as the submitted wallet already exists.', - 'Wallet exists.', - ); - }); - }); - - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits WalletAdditionSubmitted, TransactionConfirmed ' - + 'TransactionExecutionSucceeded events.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - const walletToAdd = accountProvider.get(); - const transactionResponse = await helper.multisig().submitAddWallet( - walletToAdd, - { from: helper.wallet(0) }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 3, - 'As the requirement is 1, transaction would be executed ' - + 'afterwards, hence WalletAdditionSubmitted, ' - + 'TransactionConfirmed and TransactionExecutionSucceeded ' - + 'would be emitted.', - ); - - - // The first emitted event should be 'WalletAdditionSubmitted'. - Event.assertEqual(events[0], { - name: 'WalletAdditionSubmitted', - args: { - _transactionID: new BN(0), - _wallet: walletToAdd, - }, - }); - - // The second emitted event should be 'TransactionConfirmed', - // because the wallet that submitted a new wallet addition request - // should confirm it afterwards in the same call. - Event.assertEqual(events[1], { - name: 'TransactionConfirmed', - args: { - _transactionID: new BN(0), - _wallet: helper.wallet(0), - }, - }); - - // The third emitted event should be - // 'TransactionExecutionSucceeded', because of the setup - // 1-wallet-1-requirement. - Event.assertEqual(events[2], { - name: 'TransactionExecutionSucceeded', - args: { - _transactionID: new BN(0), - }, - }); - }); - - it('Emits WalletAdditionSubmitted and TransactionConfirmed.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const walletToAdd = accountProvider.get(); - const transactionResponse = await helper.multisig().submitAddWallet( - walletToAdd, - { from: helper.wallet(0) }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 2, - 'As the requirement is 2, transaction would not be executed ' - + 'afterwards, hence WalletAdditionSubmitted and ' - + 'TransactionConfirmed would be only emitted.', - ); - - - // The first emitted event should be 'WalletAdditionSubmitted'. - Event.assertEqual(events[0], { - name: 'WalletAdditionSubmitted', - args: { - _transactionID: new BN(0), - _wallet: walletToAdd, - }, - }); - - // The second emitted event should be 'TransactionConfirmed', - // because the wallet that submitted a new wallet addition request - // should confirm it afterwards in the same call. - Event.assertEqual(events[1], { - name: 'TransactionConfirmed', - args: { - _transactionID: new BN(0), - _wallet: helper.wallet(0), - }, - }); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks state in case of 1-wallet-1-required.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - const walletToAdd = accountProvider.get(); - const transactionID = await helper.submitAddWallet( - walletToAdd, 0, - ); - - assert.isOk( - await helper.multisig().isWallet.call(walletToAdd), - 'Newly submitted wallet would be added because of ' - + '1-wallet-1-required condition.', - ); - - assert.strictEqual( - await helper.multisig().wallets.call(0), - helper.wallet(0), - ); - - assert.strictEqual( - await helper.multisig().wallets.call(1), - walletToAdd, - ); - - assert.isOk( - (await helper.multisig().transactionCount.call()).eqn(1), - 'Transaction count should be increased by one.', - ); - - assert.isOk( - await helper.multisig().confirmations.call( - transactionID, helper.wallet(0), - ), - ); - - const transaction = await helper.multisig().transactions.call(transactionID); - - assert.strictEqual( - transaction.destination, - helper.multisig().address, - ); - - assert.strictEqual( - transaction.data, - generateAddWalletData(walletToAdd), - ); - - assert.isOk( - transaction.executed, - ); - }); - - it('Checks state in case of 2-wallets-2-required.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const walletToAdd = accountProvider.get(); - const transactionID = await helper.submitAddWallet( - walletToAdd, 0, - ); - - assert.isNotOk( - await helper.multisig().isWallet.call(walletToAdd), - 'Newly submitted wallet would *not* be added because of ' - + '2-wallet-1-required condition.', - ); - - assert.strictEqual( - await helper.multisig().wallets.call(0), - helper.wallet(0), - ); - - assert.strictEqual( - await helper.multisig().wallets.call(1), - helper.wallet(1), - ); - - assert.isOk( - (await helper.multisig().transactionCount.call()).eqn(1), - 'Transaction count should be increased by one.', - ); - - assert.isOk( - await helper.multisig().confirmations.call( - transactionID, helper.wallet(0), - ), - ); - - const transaction = await helper.multisig().transactions.call(transactionID); - - assert.strictEqual( - transaction.destination, - helper.multisig().address, - ); - - assert.strictEqual( - transaction.data, - generateAddWalletData(walletToAdd), - ); - - assert.isNotOk( - transaction.executed, - ); - }); - }); -}); diff --git a/test/multisigwallet/submit_remove_wallet.js b/test/multisigwallet/submit_remove_wallet.js deleted file mode 100644 index ff089f1..0000000 --- a/test/multisigwallet/submit_remove_wallet.js +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const BN = require('bn.js'); -const web3 = require('../test_lib/web3.js'); -const utils = require('../test_lib/utils.js'); -const { Event } = require('../test_lib/event_decoder'); -const { AccountProvider } = require('../test_lib/utils.js'); -const { MultiSigWalletUtils } = require('./utils.js'); - -function generateRemoveWalletData(wallet) { - return web3.eth.abi.encodeFunctionCall( - { - name: 'removeWallet', - type: 'function', - inputs: [{ - type: 'address', - name: '', - }], - }, - [wallet], - ); -} - -contract('MultiSigWallet::submitReplaceWallet', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-registered wallet calls.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - await utils.expectRevert( - helper.multisig().submitRemoveWallet( - accountProvider.get(), - { from: accountProvider.get() }, - ), - 'Should revert as non-registered wallet calls.', - 'Only wallet is allowed to call.', - ); - }); - - it('Reverts if wallet to remove does not exist.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - await utils.expectRevert( - helper.multisig().submitRemoveWallet( - accountProvider.get(), - { from: helper.wallet(0) }, - ), - 'Should revert as wallet to remove does not exist.', - 'Wallet does not exist.', - ); - }); - - it('Reverts if last wallet is going to be removed.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - await utils.expectRevert( - helper.multisig().submitRemoveWallet( - helper.wallet(0), - { - from: helper.wallet(0), - }, - ), - 'Should revert as last wallet is submitted for removal', - 'Last wallet cannot be submitted for removal.', - ); - }); - }); - - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits WalletRemovalSubmitted, TransactionConfirmed ' - + 'TransactionExecutionSucceeded events.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 1 }, - ); - - const transactionResponse = await helper.multisig().submitRemoveWallet( - helper.wallet(1), - { from: helper.wallet(0) }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 3, - 'As requirement is 1, transaction would be executed ' - + 'afterwards, hence WalletRemovalSubmitted, ' - + 'TransactionConfirmed and TransactionExecutionSucceeded ' - + 'would be emitted.', - ); - - - // The first emitted event should be 'WalletRemovalSubmitted'. - Event.assertEqual(events[0], { - name: 'WalletRemovalSubmitted', - args: { - _transactionID: new BN(0), - _wallet: helper.wallet(1), - }, - }); - - // The second emitted event should be 'TransactionConfirmed', - // because the wallet that submitted a wallet removal request - // should confirm it afterwards. - Event.assertEqual(events[1], { - name: 'TransactionConfirmed', - args: { - _transactionID: new BN(0), - _wallet: helper.wallet(0), - }, - }); - - // The third emitted event should be - // 'TransactionExecutionSucceeded', because of the setup - // 2-wallet-1-requirement. - Event.assertEqual(events[2], { - name: 'TransactionExecutionSucceeded', - args: { - _transactionID: new BN(0), - }, - }); - }); - - it('Emits WalletRemovalSubmitted and TransactionConfirmed.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 3, required: 2 }, - ); - - const transactionResponse = await helper.multisig().submitRemoveWallet( - helper.wallet(1), - { from: helper.wallet(0) }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 2, - 'As requirement is 2, transaction would not be executed ' - + 'afterwards, hence only WalletRemovalSubmitted and ' - + 'TransactionConfirmed would be emitted.', - ); - - // The first emitted event should be 'WalletRemovalSubmitted'. - Event.assertEqual(events[0], { - name: 'WalletRemovalSubmitted', - args: { - _transactionID: new BN(0), - _wallet: helper.wallet(1), - }, - }); - - // The second emitted event should be 'TransactionConfirmed', - // because the wallet that submitted a wallet removal request - // should confirm it afterwards. - Event.assertEqual(events[1], { - name: 'TransactionConfirmed', - args: { - _transactionID: new BN(0), - _wallet: helper.wallet(0), - }, - }); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks state in case of 2-wallets-1-required.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 1 }, - ); - - const transactionID = await helper.submitRemoveWallet(1, 0); - - assert.isNotOk( - await helper.multisig().isWallet.call(helper.wallet(1)), - 'Wallet will be removed because of 2-wallets-1-required ' - + 'setup.', - ); - - assert.isOk( - (await helper.multisig().walletCount.call()).eqn(1), - ); - - assert.strictEqual( - await helper.multisig().wallets.call(0), - helper.wallet(0), - ); - - assert.isOk( - (await helper.multisig().required.call()).eqn(1), - ); - - assert.isOk( - await helper.multisig().confirmations( - transactionID, helper.wallet(0), - ), - ); - - assert.isOk( - (await helper.multisig().transactionCount.call()).eqn(1), - ); - - const transaction = await helper.multisig().transactions.call(transactionID); - - assert.strictEqual( - transaction.destination, - helper.multisig().address, - ); - - assert.strictEqual( - transaction.data, - generateRemoveWalletData(helper.wallet(1)), - ); - - assert.strictEqual( - transaction.executed, - true, - ); - }); - - it('Checks state in case of 3-wallets-2-required.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 3, required: 2 }, - ); - - const transactionID = await helper.submitRemoveWallet(1, 0); - - assert.isOk( - await helper.multisig().isWallet.call(helper.wallet(1)), - 'Wallet will not be removed because of 3-wallets-2-required ' - + 'setup.', - ); - - assert.isOk( - (await helper.multisig().walletCount.call()).eqn(3), - ); - - assert.strictEqual( - await helper.multisig().wallets.call(0), - helper.wallet(0), - ); - - assert.strictEqual( - await helper.multisig().wallets.call(1), - helper.wallet(1), - ); - - assert.strictEqual( - await helper.multisig().wallets.call(2), - helper.wallet(2), - ); - - assert.isOk( - (await helper.multisig().required.call()).eqn(2), - ); - - assert.isOk( - await helper.multisig().confirmations( - transactionID, helper.wallet(0), - ), - ); - - assert.isOk( - (await helper.multisig().transactionCount.call()).eqn(1), - ); - - const transaction = await helper.multisig().transactions.call(transactionID); - - assert.strictEqual( - transaction.destination, - helper.multisig().address, - ); - - assert.strictEqual( - transaction.data, - generateRemoveWalletData(helper.wallet(1)), - ); - - assert.strictEqual( - transaction.executed, - false, - ); - }); - - it('Checks that requirement is adjusted after removal.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const transactionID = await helper.submitRemoveWallet(1, 0); - - assert.isOk( - (await helper.multisig().required.call()).eqn(2), - ); - - await helper.multisig().confirmTransaction( - transactionID, - { from: helper.wallet(1) }, - ); - - assert.isOk( - (await helper.multisig().required.call()).eqn(1), - 'After removing the wallet the required should be updated ' - + 'to max wallet count, in this case 1.', - ); - }); - }); -}); diff --git a/test/multisigwallet/submit_replace_wallet.js b/test/multisigwallet/submit_replace_wallet.js deleted file mode 100644 index 0caba44..0000000 --- a/test/multisigwallet/submit_replace_wallet.js +++ /dev/null @@ -1,367 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const BN = require('bn.js'); -const web3 = require('../test_lib/web3.js'); -const utils = require('../test_lib/utils.js'); -const { Event } = require('../test_lib/event_decoder'); -const { AccountProvider } = require('../test_lib/utils.js'); -const { MultiSigWalletUtils } = require('./utils.js'); - -function generateReplaceWalletData(oldWallet, newWallet) { - return web3.eth.abi.encodeFunctionCall( - { - name: 'replaceWallet', - type: 'function', - inputs: [ - { - type: 'address', - name: '', - }, - { - type: 'address', - name: '', - }], - }, - [oldWallet, newWallet], - ); -} - -contract('MultiSigWallet::submitReplaceWallet', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-registered wallet calls.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 1 }, - ); - - await utils.expectRevert( - helper.multisig().submitReplaceWallet( - helper.wallet(1), - accountProvider.get(), - { from: accountProvider.get() }, - ), - 'Should revert as non-registered wallet calls.', - 'Only wallet is allowed to call.', - ); - }); - - it('Reverts if old wallet to replace does not exist.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 1 }, - ); - - await utils.expectRevert( - helper.multisig().submitReplaceWallet( - accountProvider.get(), - accountProvider.get(), - { from: helper.wallet(0) }, - ), - 'Should revert as old wallet to replace does not exist.', - 'Wallet does not exist.', - ); - }); - - it('Reverts if new wallet to add is null.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 1 }, - ); - - await utils.expectRevert( - helper.multisig().submitReplaceWallet( - helper.wallet(1), - utils.NULL_ADDRESS, - { from: helper.wallet(0) }, - ), - 'Should revert as new wallet to add is null.', - 'Wallet address is null.', - ); - }); - - it('Reverts if new wallet to add already exists.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 1 }, - ); - - await utils.expectRevert( - helper.multisig().submitReplaceWallet( - helper.wallet(1), - helper.wallet(1), - { from: helper.wallet(0) }, - ), - 'Should revert as new wallet to add already exists.', - 'Wallet exists.', - ); - }); - }); - - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits WalletReplacementSubmitted, TransactionConfirmed ' - + 'TransactionExecutionSucceeded events.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 1 }, - ); - - const newWalletForReplacement = accountProvider.get(); - const transactionResponse = await helper.multisig().submitReplaceWallet( - helper.wallet(1), - newWalletForReplacement, - { from: helper.wallet(0) }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 3, - 'As requirement is 1, transaction would be executed ' - + 'afterwards, hence WalletReplacementSubmitted, ' - + 'TransactionConfirmed and TransactionExecutionSucceeded ' - + 'would be emitted.', - ); - - // The first emitted event should be 'WalletReplacementSubmitted'. - Event.assertEqual(events[0], { - name: 'WalletReplacementSubmitted', - args: { - _transactionID: new BN(0), - _oldWallet: helper.wallet(1), - _newWallet: newWalletForReplacement, - }, - }); - - // The second emitted event should be 'TransactionConfirmed', as - // the wallet that submits transaction confirms afterwards. - Event.assertEqual(events[1], { - name: 'TransactionConfirmed', - args: { - _transactionID: new BN(0), - _wallet: helper.wallet(0), - }, - }); - - // The third emitted event should be 'TransactionExecutionSucceeded' - // as requirement is 1, hence transaction would be executed - // afterwards. - Event.assertEqual(events[2], { - name: 'TransactionExecutionSucceeded', - args: { - _transactionID: new BN(0), - }, - }); - }); - - it('Emits WalletReplacementSubmitted and TransactionConfirmed.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const newWalletToReplace = accountProvider.get(); - const transactionResponse = await helper.multisig().submitReplaceWallet( - helper.wallet(1), - newWalletToReplace, - { from: helper.wallet(0) }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 2, - 'As requirement is 2, transaction would not be executed ' - + 'afterwards, hence only WalletReplacementSubmitted and ' - + 'TransactionConfirmed would be emitted.', - ); - - // The first emitted event should be 'WalletReplacementSubmitted'. - Event.assertEqual(events[0], { - name: 'WalletReplacementSubmitted', - args: { - _transactionID: new BN(0), - _oldWallet: helper.wallet(1), - _newWallet: newWalletToReplace, - }, - }); - - // The second emitted event should be 'TransactionConfirmed', as - // the wallet that submits transaction confirms afterwards. - Event.assertEqual(events[1], { - name: 'TransactionConfirmed', - args: { - _transactionID: new BN(0), - _wallet: helper.wallet(0), - }, - }); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks state in case of 2-wallets-1-required.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 1 }, - ); - - const newWalletForReplacement = accountProvider.get(); - const transactionID = await helper.submitReplaceWallet( - 1, newWalletForReplacement, 0, - ); - - assert.isOk( - await helper.multisig().isWallet.call(newWalletForReplacement), - 'Wallet replacement should happen immediately, because of ' - + '2-wallets-1-required setup.', - ); - - assert.isNotOk( - await helper.multisig().isWallet.call(helper.wallet(1)), - 'Wallet replacement should happen immediately, because of ' - + '2-wallets-1-required setup.', - ); - - assert.isOk( - (await helper.multisig().walletCount.call()).eqn(2), - 'Wallet count should not change during replace.', - ); - - assert.strictEqual( - await helper.multisig().wallets.call(0), - helper.wallet(0), - ); - - assert.strictEqual( - await helper.multisig().wallets.call(1), - newWalletForReplacement, - ); - - assert.isOk( - (await helper.multisig().required.call()).eqn(1), - 'Required number of confirmation should stay unchanged.', - ); - - assert.isOk( - (await helper.multisig().transactionCount.call()).eqn(1), - 'Transaction count should be increased by one.', - ); - - assert.strictEqual( - await helper.multisig().confirmations.call( - transactionID, helper.wallet(0), - ), - true, - ); - - const transaction = await helper.multisig().transactions.call(transactionID); - - assert.strictEqual( - transaction.destination, - helper.multisig().address, - ); - - assert.strictEqual( - transaction.data, - generateReplaceWalletData( - helper.wallet(1), newWalletForReplacement, - ), - ); - - assert.strictEqual( - transaction.executed, - true, - ); - }); - - it('Checks state in case of 2-wallets-2-required.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 2 }, - ); - - const newWalletForReplacement = accountProvider.get(); - const transactionID = await helper.submitReplaceWallet( - 1, newWalletForReplacement, 0, - ); - - assert.isOk( - await helper.multisig().isWallet.call(helper.wallet(1)), - 'Wallet replacement should not happen immediately, because of ' - + '2-wallets-2-required setup.', - ); - - assert.isNotOk( - await helper.multisig().isWallet.call(newWalletForReplacement), - 'Wallet replacement should not happen immediately, because of ' - + '2-wallets-2-required setup.', - ); - - assert.isOk( - (await helper.multisig().walletCount.call()).eqn(2), - 'Wallet count should not change during replace.', - ); - - assert.strictEqual( - await helper.multisig().wallets.call(0), - helper.wallet(0), - ); - - assert.strictEqual( - await helper.multisig().wallets.call(1), - helper.wallet(1), - ); - - assert.isOk( - (await helper.multisig().required.call()).eqn(2), - 'Required number of confirmation should stay unchanged.', - ); - - assert.isOk( - (await helper.multisig().transactionCount.call()).eqn(1), - 'Transaction count should be increased by one.', - ); - - assert.strictEqual( - await helper.multisig().confirmations.call( - transactionID, helper.wallet(0), - ), - true, - ); - - const transaction = await helper.multisig().transactions.call(transactionID); - - assert.strictEqual( - transaction.destination, - helper.multisig().address, - ); - - assert.strictEqual( - transaction.data, - generateReplaceWalletData( - helper.wallet(1), newWalletForReplacement, - ), - ); - - assert.strictEqual( - transaction.executed, - false, - ); - }); - }); -}); diff --git a/test/multisigwallet/submit_requirement_change.js b/test/multisigwallet/submit_requirement_change.js deleted file mode 100644 index 4c6f718..0000000 --- a/test/multisigwallet/submit_requirement_change.js +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// ---------------------------------------------------------------------------- -// Test: MultiSigWallet::submitRequirementChange -// -// http://www.simpletoken.org/ -// -// ---------------------------------------------------------------------------- - -const BN = require('bn.js'); -const web3 = require('../test_lib/web3.js'); -const utils = require('../test_lib/utils.js'); -const { Event } = require('../test_lib/event_decoder'); -const { AccountProvider } = require('../test_lib/utils.js'); -const { MultiSigWalletUtils } = require('./utils.js'); - - -function generateRequirementChangeData(required) { - return web3.eth.abi.encodeFunctionCall( - { - name: 'changeRequirement', - type: 'function', - inputs: [{ - type: 'uint256', - name: '', - }], - }, - [required], - ); -} - -contract('MultiSigWallet::submitRequirementChange', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-registered wallet calls.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 1 }, - ); - - - await utils.expectRevert( - helper.multisig().submitRequirementChange( - 2, - { from: accountProvider.get() }, - ), - 'Should revert as non-registered wallet calls.', - 'Only wallet is allowed to call.', - ); - }); - - it('Should revert if new requirement equal to 0.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - await utils.expectRevert( - helper.multisig().submitRequirementChange( - 0, - { from: helper.wallet(0) }, - ), - 'Should revert as new requirement is 0.', - 'Requirement validity not fulfilled.', - ); - }); - - it('Should revert if new requirement is bigger than wallets count.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 1, required: 1 }, - ); - - await utils.expectRevert( - helper.multisig().submitRequirementChange( - 2, - { from: helper.wallet(0) }, - ), - 'Should revert as new requirement is 2 and wallet counts is 1.', - 'Requirement validity not fulfilled.', - ); - }); - }); - - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits RequirementChangeSubmitted, TransactionConfirmed ' - + 'TransactionExecutionSucceeded events.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 1 }, - ); - - const newRequired = 2; - - const transactionResponse = await helper.multisig().submitRequirementChange( - newRequired, - { from: helper.wallet(0) }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 3, - 'As requirement is 1, transaction would be executed ' - + 'afterwards, hence RequirementChangeSubmitted, ' - + 'TransactionConfirmed and TransactionExecutionSucceeded ' - + 'would be emitted.', - ); - - // The first emitted event should be 'RequirementChangeSubmitted'. - Event.assertEqual(events[0], { - name: 'RequirementChangeSubmitted', - args: { - _transactionID: new BN(0), - _required: new BN(newRequired), - }, - }); - - // The second emitted event should be 'TransactionConfirmed', as - // the wallet that submits transaction confirms afterwards. - Event.assertEqual(events[1], { - name: 'TransactionConfirmed', - args: { - _transactionID: new BN(0), - _wallet: helper.wallet(0), - }, - }); - - // The third emitted event should be 'TransactionExecutionSucceeded' - // as requirement is 1, hence transaction would be executed - // afterwards. - Event.assertEqual(events[2], { - name: 'TransactionExecutionSucceeded', - args: { - _transactionID: new BN(0), - }, - }); - }); - - it('Emits RequirementChangeSubmitted and TransactionConfirmed.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 3, required: 2 }, - ); - - const newRequired = 3; - - const transactionResponse = await helper.multisig().submitRequirementChange( - newRequired, - { from: helper.wallet(0) }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 2, - 'As requirement is 2, transaction would not be executed ' - + 'afterwards, hence RequirementChangeSubmitted and ' - + 'TransactionConfirmed would be emitted.', - ); - - // The first emitted event should be 'RequirementChangeSubmitted'. - Event.assertEqual(events[0], { - name: 'RequirementChangeSubmitted', - args: { - _transactionID: new BN(0), - _required: new BN(newRequired), - }, - }); - - // The second emitted event should be 'TransactionConfirmed', as - // the wallet that submits transaction confirms afterwards. - Event.assertEqual(events[1], { - name: 'TransactionConfirmed', - args: { - _transactionID: new BN(0), - _wallet: helper.wallet(0), - }, - }); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks state in case of 2-wallets-1-required.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 2, required: 1 }, - ); - - const newRequired = 2; - - const transactionID = await helper.submitRequirementChange( - newRequired, - 0, - ); - - assert.isOk( - (await helper.multisig().required.call()).eqn(newRequired), - 'As required is equal to 1, the transaction would be ' - + 'executed in the same call.', - ); - - assert.isOk( - await helper.multisig().confirmations( - transactionID, helper.wallet(0), - ), - 'Submitter should also confirm.', - ); - - assert.isNotOk( - await helper.multisig().confirmations( - transactionID, helper.wallet(1), - ), - 'Non submitter should not confirm.', - ); - - assert.isOk( - (await helper.multisig().transactionCount.call()).eqn(1), - 'Transaction count should be increased by one.', - ); - - const transaction = await helper.multisig().transactions.call(transactionID); - - assert.strictEqual( - transaction.destination, - helper.multisig().address, - ); - - assert.strictEqual( - transaction.data, - generateRequirementChangeData(newRequired), - ); - - assert.strictEqual( - transaction.executed, - true, - ); - }); - - it('Checks state in case of 3-wallets-2-required.', async () => { - const helper = await new MultiSigWalletUtils( - { accountProvider, walletCount: 3, required: 2 }, - ); - - const required = 2; - const newRequired = 3; - - const transactionID = await helper.submitRequirementChange( - newRequired, 0, - ); - - assert.isOk( - (await helper.multisig().required.call()).eqn(required), - 'As required is equal to 2, the transaction would not be ' - + 'executed in the same call.', - ); - - assert.isOk( - await helper.multisig().confirmations( - transactionID, helper.wallet(0), - ), - 'Submitter should also confirm.', - ); - - assert.isNotOk( - await helper.multisig().confirmations( - transactionID, helper.wallet(1), - ), - 'Non submitter should not confirm.', - ); - - assert.isNotOk( - await helper.multisig().confirmations(transactionID, helper.wallet(2)), - 'Non submitter should not confirm.', - ); - - assert.isOk( - (await helper.multisig().transactionCount.call()).eqn(1), - 'Transaction count should be increased by one.', - ); - - const transaction = await helper.multisig().transactions.call(transactionID); - - assert.strictEqual( - transaction.destination, - helper.multisig().address, - ); - - assert.strictEqual( - transaction.data, - generateRequirementChangeData(newRequired), - ); - - assert.strictEqual( - transaction.executed, - false, - ); - }); - }); -}); diff --git a/test/multisigwallet/utils.js b/test/multisigwallet/utils.js deleted file mode 100644 index 4d4de4f..0000000 --- a/test/multisigwallet/utils.js +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2018 OST.com Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const MultiSigWallet = artifacts.require('MultiSigWallet'); - -class MultiSigWalletUtils { - constructor({ accountProvider, walletCount, required }) { - return ( - async () => { - assert(walletCount > 0); - assert(required > 0); - - this._accountProvider = accountProvider; - this._required = required; - this._walletCount = walletCount; - - this._wallets = []; - - for (let i = 0; i < walletCount; i += 1) { - this._wallets.push(this._accountProvider.get()); - } - - this._multisig = await MultiSigWallet.new(this._wallets, this._required); - - return this; - })(); - } - - multisig() { - return this._multisig; - } - - wallet(index) { - assert(index < this._wallets.length); - assert(index >= 0); - - return this._wallets[index]; - } - - async submitAddWallet(walletToAdd, fromWalletIndex) { - const transactionID = await this._multisig.submitAddWallet.call( - walletToAdd, - { from: this.wallet(fromWalletIndex) }, - ); - - await this._multisig.submitAddWallet( - walletToAdd, - { from: this.wallet(fromWalletIndex) }, - ); - - return transactionID; - } - - async submitRemoveWallet(walletToRemoveIndex, fromWalletIndex) { - const transactionID = await this._multisig.submitRemoveWallet.call( - this.wallet(walletToRemoveIndex), - { from: this.wallet(fromWalletIndex) }, - ); - - await this._multisig.submitRemoveWallet( - this.wallet(walletToRemoveIndex), - { from: this.wallet(fromWalletIndex) }, - ); - - return transactionID; - } - - async submitReplaceWallet(oldWalletIndex, newWallet, fromWalletIndex) { - const transactionID = await this._multisig.submitReplaceWallet.call( - this.wallet(oldWalletIndex), - newWallet, - { from: this.wallet(fromWalletIndex) }, - ); - - await this._multisig.submitReplaceWallet( - this.wallet(oldWalletIndex), - newWallet, - { from: this.wallet(fromWalletIndex) }, - ); - - return transactionID; - } - - async submitRequirementChange(newRequired, fromWalletIndex) { - const transactionID = await this._multisig.submitRequirementChange.call( - newRequired, - { from: this.wallet(fromWalletIndex) }, - ); - - await this._multisig.submitRequirementChange( - newRequired, - { from: this.wallet(fromWalletIndex) }, - ); - - return transactionID; - } -} - -module.exports = { - MultiSigWalletUtils, -}; diff --git a/test/pricer_rule/add_price_oracle.js b/test/pricer_rule/add_price_oracle.js new file mode 100644 index 0000000..903d2e9 --- /dev/null +++ b/test/pricer_rule/add_price_oracle.js @@ -0,0 +1,227 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const web3 = require('../test_lib/web3.js'); +const Utils = require('../test_lib/utils'); +const PricerRuleUtils = require('./utils.js'); +const { AccountProvider } = require('../test_lib/utils'); +const { Event } = require('../test_lib/event_decoder'); + +const PriceOracleFake = artifacts.require('PriceOracleFake'); + +contract('PricerRule::add_price_oracle', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts as a non-organization worker is calling.', async () => { + const { + pricerRule, + priceOracle, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + await Utils.expectRevert( + pricerRule.addPriceOracle( + priceOracle.address, + { + from: accountProvider.get(), + }, + ), + 'Should revert as a non-organization worker is calling.', + 'Only whitelisted workers are allowed to call this method.', + ); + }); + + it('Reverts as the proposed price oracle address is null.', async () => { + const { + organizationWorker, + pricerRule, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + await Utils.expectRevert( + pricerRule.addPriceOracle( + Utils.NULL_ADDRESS, + { from: organizationWorker }, + ), + 'Should revert as the proposed price oracle address is null.', + 'Price oracle address is null.', + ); + }); + + it('Reverts as the proposed price oracle decimals number is invalid.', async () => { + const { + organizationWorker, + baseCurrencyCode, + requiredPriceOracleDecimals, + pricerRule, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + const priceOracle = await PriceOracleFake.new( + web3.utils.stringToHex(baseCurrencyCode), + web3.utils.stringToHex('BTC'), + requiredPriceOracleDecimals + 1, + 1, // price oracle initial price + (await web3.eth.getBlockNumber()) + 10000, // expiration height + ); + + await Utils.expectRevert( + pricerRule.addPriceOracle( + priceOracle.address, + { from: organizationWorker }, + ), + 'Should revert as the proposed price oracle decimals number is invalid.', + 'Price oracle decimals number is difference from the required one.', + ); + }); + + it('Reverts as the proposed price oracle base currency code does not ' + + 'match with pricer base currency.', async () => { + const { + organizationWorker, + baseCurrencyCode, + requiredPriceOracleDecimals, + pricerRule, + quoteCurrencyCode, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + const anotherBaseCurrencyCode = 'BTC'; + assert.notEqual( + baseCurrencyCode, + anotherBaseCurrencyCode, + 'Ensuring that registered base currency code in ' + + 'pricer is different from new one.', + ); + + const priceOracle = await PriceOracleFake.new( + web3.utils.stringToHex(anotherBaseCurrencyCode), + web3.utils.stringToHex(quoteCurrencyCode), + requiredPriceOracleDecimals, + 100, // initial price + (await web3.eth.getBlockNumber()) + 10000, // expiration height + ); + + await Utils.expectRevert( + pricerRule.addPriceOracle( + priceOracle.address, + { from: organizationWorker }, + ), + 'Should revert as the base currency code in pricer is different ' + + 'than in the proposed price oracle.', + 'Price oracle\'s base currency code does not match.', + ); + }); + + it('Reverts as a price oracle with the same pay-currency code exists.', async () => { + const { + organizationWorker, + baseCurrencyCode, + requiredPriceOracleDecimals, + pricerRule, + priceOracle, + quoteCurrencyCode, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + await pricerRule.addPriceOracle( + priceOracle.address, + { from: organizationWorker }, + ); + + const priceOracle2 = await PriceOracleFake.new( + web3.utils.stringToHex(baseCurrencyCode), + web3.utils.stringToHex(quoteCurrencyCode), + requiredPriceOracleDecimals, + 100, // initial price + (await web3.eth.getBlockNumber()) + 10000, // expiration height + ); + + await Utils.expectRevert( + pricerRule.addPriceOracle( + priceOracle2.address, + { from: organizationWorker }, + ), + 'Reverts as a price oracle already exists.', + 'Price oracle already exists.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits PriceOracleAdded.', async () => { + const { + organizationWorker, + pricerRule, + priceOracle, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + const response = await pricerRule.addPriceOracle( + priceOracle.address, + { from: organizationWorker }, + ); + + const events = Event.decodeTransactionResponse( + response, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'PriceOracleAdded', + args: { + _priceOracle: priceOracle.address, + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + it('Checks that price oracle is added.', async () => { + const { + organizationWorker, + pricerRule, + quoteCurrencyCode, + priceOracle, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + let actualPriceOracle = await pricerRule.baseCurrencyPriceOracles.call( + web3.utils.stringToHex(quoteCurrencyCode), + ); + + assert.strictEqual( + actualPriceOracle, + Utils.NULL_ADDRESS, + ); + + await pricerRule.addPriceOracle( + priceOracle.address, + { from: organizationWorker }, + ); + + actualPriceOracle = await pricerRule.baseCurrencyPriceOracles.call( + web3.utils.stringToHex(quoteCurrencyCode), + ); + + assert.strictEqual( + priceOracle.address, + actualPriceOracle, + ); + }); + }); +}); diff --git a/test/pricer_rule/constructor.js b/test/pricer_rule/constructor.js new file mode 100644 index 0000000..b53602c --- /dev/null +++ b/test/pricer_rule/constructor.js @@ -0,0 +1,174 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const web3 = require('../test_lib/web3.js'); +const Utils = require('../test_lib/utils'); +const PricerRuleUtils = require('./utils.js'); +const { AccountProvider } = require('../test_lib/utils'); + +const PricerRule = artifacts.require('PricerRule'); + +contract('PricerRule::constructor', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if the token economy address is null.', async () => { + const { + organization, + } = await PricerRuleUtils.createOrganization(accountProvider); + + await Utils.expectRevert( + PricerRule.new( + organization.address, + Utils.NULL_ADDRESS, // economy token address + web3.utils.stringToHex('OST'), // base currency code + 1, // conversion rate + 0, // conversion rate decimals + 0, // price oracles required decimals number + accountProvider.get(), // token rules + ), + 'Should revert as the economy token address is null.', + 'Token address is null.', + ); + }); + + it('Reverts if the base currency code is empty.', async () => { + const { + organization, + } = await PricerRuleUtils.createOrganization(accountProvider); + + await Utils.expectRevert( + PricerRule.new( + organization.address, + accountProvider.get(), // economy token address + web3.utils.stringToHex(''), // base currency code + 1, // conversion rate + 0, // conversion rate decimals + 0, // price oracles required decimals number + accountProvider.get(), // token rules + ), + 'Should revert as the base currency code is null.', + 'Base currency code is null.', + ); + }); + + it('Reverts if the conversion rate from the base currency to the token is 0.', async () => { + const { + organization, + } = await PricerRuleUtils.createOrganization(accountProvider); + + await Utils.expectRevert( + PricerRule.new( + organization.address, + accountProvider.get(), // economy token address + web3.utils.stringToHex('OST'), // base currency code + 0, // conversion rate + 0, // conversion rate decimals + 0, // price oracles required decimals number + accountProvider.get(), // token rules + ), + 'Should revert as the conversion rate from the base currency to the token is 0.', + 'Conversion rate from the base currency to the token is 0.', + ); + }); + + it('Reverts if the token rules address is null.', async () => { + const { + organization, + } = await PricerRuleUtils.createOrganization(accountProvider); + + await Utils.expectRevert( + PricerRule.new( + organization.address, + accountProvider.get(), // economy token address + web3.utils.stringToHex('OST'), // base currency code + 1, // conversion rate + 0, // conversion rate decimals + 0, // price oracles required decimals number + Utils.NULL_ADDRESS, // token rules + ), + 'Should revert as token rules is null.', + 'Token rules address is null.', + ); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + it('Checks that passed arguments are set correctly.', async () => { + const { + organization, + } = await PricerRuleUtils.createOrganization(accountProvider); + + const tokenDecimals = 10; + const eip20TokenConfig = { + decimals: tokenDecimals, + }; + const token = await PricerRuleUtils.createEIP20Token(eip20TokenConfig); + const tokenRules = accountProvider.get(); + const pricerRule = await PricerRule.new( + organization.address, + token.address, // economy token address + web3.utils.stringToHex('OST'), // base currency code + 10, // conversion rate + 1, // conversion rate decimals + 2, // price oracles required decimals number + tokenRules, // token rules + ); + + assert.strictEqual( + (await pricerRule.organization.call()), + organization.address, + ); + + assert.strictEqual( + (await pricerRule.baseCurrencyCode.call()), + web3.utils.stringToHex('OST'), + ); + + assert.strictEqual( + (await pricerRule.eip20Token.call()), + token.address, + ); + + assert.strictEqual( + web3.utils.hexToString(await pricerRule.baseCurrencyCode.call()), + 'OST', + ); + + assert.isOk( + (await pricerRule.conversionRateFromBaseCurrencyToToken.call()).eqn(10), + ); + + assert.isOk( + (await pricerRule.conversionRateDecimalsFromBaseCurrencyToToken.call()).eqn(1), + ); + + assert.isOk( + (await pricerRule.requiredPriceOracleDecimals.call()).eqn(2), + ); + + assert.isOk( + (await pricerRule.tokenDecimals.call()).eqn(tokenDecimals), + ); + + assert.strictEqual( + (await pricerRule.tokenRules.call()), + tokenRules, + ); + }); + }); +}); diff --git a/test/pricer_rule/pay.js b/test/pricer_rule/pay.js new file mode 100644 index 0000000..6c17017 --- /dev/null +++ b/test/pricer_rule/pay.js @@ -0,0 +1,609 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const web3 = require('../test_lib/web3.js'); +const Utils = require('../test_lib/utils'); +const PricerRuleUtils = require('./utils.js'); +const { AccountProvider } = require('../test_lib/utils'); + +async function prepare(accountProvider, config = {}, eip20TokenConfig = {}) { + const r = await PricerRuleUtils.createTokenEconomy(accountProvider, config, eip20TokenConfig); + + r.fromAddress = accountProvider.get(); + + await r.pricerRule.addPriceOracle( + r.priceOracle.address, + { from: r.organizationWorker }, + ); + + r.tokenRules.allowTransfers( + { from: r.fromAddress }, + ); + + r.spendingLimit = 1000000; + + r.token.approve( + r.tokenRules.address, + r.spendingLimit, + { from: r.fromAddress }, + ); + + return r; +} + +function convertPayCurrencyToToken( + tokenDecimals, amount, price, conversionRate, conversionRateDecimals, +) { + const requiredPriceOracleDecimalsBN = (new BN(10)).pow(new BN(tokenDecimals)); + const conversionRateDecimalsBN = (new BN(10)).pow(conversionRateDecimals); + return ((requiredPriceOracleDecimalsBN + .mul(amount) + .mul(conversionRate)) + .div(price)) + .div(conversionRateDecimalsBN); +} + +contract('PricerRule::pay', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts as a \'from\' address is null.', async () => { + const { + pricerRule, + quoteCurrencyCode, + priceOracleInitialPrice, + } = await prepare(accountProvider); + + await Utils.expectRevert( + pricerRule.pay( + Utils.NULL_ADDRESS, + [accountProvider.get()], // 'to' addresses + [1], // amounts + web3.utils.stringToHex(quoteCurrencyCode), + priceOracleInitialPrice, + { from: accountProvider.get() }, + ), + 'Should revert as address to send amounts is null.', + 'From address is null.', + ); + }); + + it('Reverts as a addresses and amounts arrays\' sizes are not equal.', async () => { + const { + pricerRule, + quoteCurrencyCode, + priceOracleInitialPrice, + fromAddress, + } = await prepare(accountProvider); + + await Utils.expectRevert( + pricerRule.pay( + fromAddress, + [], // 'to' addresses + [1], // amounts + web3.utils.stringToHex(quoteCurrencyCode), + priceOracleInitialPrice, + { from: accountProvider.get() }, + ), + 'Should revert as a addresses and amounts arrays\' sizes are not equal.', + '\'to\' and \'amount\' transfer arrays\' lengths are not equal.', + ); + + await Utils.expectRevert( + pricerRule.pay( + fromAddress, + [accountProvider.get()], // 'to' addresses + [], // amounts + web3.utils.stringToHex(quoteCurrencyCode), + priceOracleInitialPrice, + { from: accountProvider.get() }, + ), + 'Should revert as a addresses and amounts arrays\' sizes are not equal.', + '\'to\' and \'amount\' transfer arrays\' lengths are not equal.', + ); + }); + + it('Reverts as an intended price is not in acceptable margin.', async () => { + const { + organizationWorker, + pricerRule, + quoteCurrencyCode, + priceOracleInitialPrice, + fromAddress, + requiredPriceOracleDecimals, + } = await prepare(accountProvider); + + // $0.002 = 0.002*10^18(in contract) + const acceptanceMargin = (0.002 * (10 ** requiredPriceOracleDecimals)) + .toString(); + await pricerRule.setAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + acceptanceMargin, + { from: organizationWorker }, + ); + + await Utils.expectRevert( + pricerRule.pay( + fromAddress, + [accountProvider.get()], // 'to' addresses + ['2'], // amounts + web3.utils.stringToHex(quoteCurrencyCode), + new BN(priceOracleInitialPrice).add(new BN(acceptanceMargin)).add(new BN(1)), + { from: accountProvider.get() }, + ), + 'Should revert as n intended price is not in the acceptable margin.', + 'Intended price is not in the acceptable margin wrt current price.', + ); + + assert.isOk( + priceOracleInitialPrice - (acceptanceMargin + 1) >= 0, + ); + + await Utils.expectRevert( + pricerRule.pay( + fromAddress, + [accountProvider.get()], // 'to' addresses + [2], // amounts + web3.utils.stringToHex(quoteCurrencyCode), + new BN(priceOracleInitialPrice).sub(new BN(acceptanceMargin).add(new BN(1))), + { from: accountProvider.get() }, + ), + 'Should revert as n intended price is not in the acceptable margin.', + 'Intended price is not in the acceptable margin wrt current price.', + ); + }); + + it('Reverts if an price oracle throws an exception on price request.', async () => { + const { + pricerRule, + quoteCurrencyCode, + priceOracleInitialPrice, + priceOracle, + fromAddress, + } = await prepare(accountProvider); + + const deltaExpirationHeight = 10; + await priceOracle.setPrice( + priceOracleInitialPrice, + (await web3.eth.getBlockNumber()) + deltaExpirationHeight, + ); + + for (let i = 0; i < deltaExpirationHeight; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Utils.advanceBlock(); + } + + await Utils.expectRevert( + pricerRule.pay( + fromAddress, + [accountProvider.get()], // 'to' addresses + [2], // amounts + web3.utils.stringToHex(quoteCurrencyCode), + priceOracleInitialPrice, + { from: accountProvider.get() }, + ), + 'Should revert as the price oracle throws an exception on price request.', + 'Price expiration height is lte to the current block height.', + ); + }); + + it('Reverts if an price oracle returns 0 price.', async () => { + const { + pricerRule, + quoteCurrencyCode, + priceOracleInitialPrice, + priceOracle, + fromAddress, + } = await prepare(accountProvider); + + await priceOracle.setPrice( + 0, + (await web3.eth.getBlockNumber()) + 10000, + ); + + await Utils.expectRevert( + pricerRule.pay( + fromAddress, + [accountProvider.get()], // 'to' addresses + [2], // amounts + web3.utils.stringToHex(quoteCurrencyCode), + priceOracleInitialPrice, + { from: accountProvider.get() }, + ), + 'Should revert as the price oracle returns 0 as price.', + 'Base currency price in pay currency is 0.', + ); + }); + }); + + contract('Positive Paths', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that TokenRules executeTransfers is called with requiredPriceOracleDecimals = 18 and tokenDecimals = 18.', async () => { + const config = { + requiredPriceOracleDecimals: 18, + }; + const eip20TokenConfig = { + decimals: 18, + }; + const { + organizationWorker, + tokenRules, + conversionRate, + conversionRateDecimals, + requiredPriceOracleDecimals, + pricerRule, + quoteCurrencyCode, + priceOracle, + fromAddress, + tokenDecimals, + } = await prepare(accountProvider, config, eip20TokenConfig); + + // $0.02 = 0.02*10^18(in contract) + const oraclePrice = (0.02 * (10 ** requiredPriceOracleDecimals)) + .toString(); + await priceOracle.setPrice( + oraclePrice, + (await web3.eth.getBlockNumber()) + 10000, + ); + // $1 = 1*10^18(in contract) + const acceptanceMargin = (1 * (10 ** requiredPriceOracleDecimals)) + .toString(); + await pricerRule.setAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + acceptanceMargin, + { from: organizationWorker }, + ); + + const to1 = accountProvider.get(); + const to2 = accountProvider.get(); + // Amount1 to transfer: $20 = 20*10^18(in contract) + const amount1BN = (20 * (10 ** requiredPriceOracleDecimals)).toString(); + // Amount2 to transfer: $10 = 10*10^18(in contract) + const amount2BN = (10 * (10 ** requiredPriceOracleDecimals)).toString(); + + const intendedPrice = oraclePrice; // intendedPriceBN is Current PriceOracle price + + await pricerRule.pay( + fromAddress, + [to1, to2], // 'to' addresses + [amount1BN, amount2BN], // amounts + web3.utils.stringToHex(quoteCurrencyCode), + intendedPrice, + { from: accountProvider.get() }, + ); + const convertedAmount1BN = convertPayCurrencyToToken( + tokenDecimals, + new BN(amount1BN), + new BN(oraclePrice), + new BN(conversionRate), + new BN(conversionRateDecimals), + ); + const convertedAmount2BN = convertPayCurrencyToToken( + tokenDecimals, + new BN(amount2BN), + new BN(oraclePrice), + new BN(conversionRate), + new BN(conversionRateDecimals), + ); + const actualFromAddress = await tokenRules.recordedFrom.call(); + + const actualToAddress1 = await tokenRules.recordedTransfersTo.call(0); + const actualToAddress2 = await tokenRules.recordedTransfersTo.call(1); + const actualTransfersToLength = await tokenRules.recordedTransfersToLength.call(); + + const tenPowerTokenDecimals = (new BN(10)).pow(new BN(tokenDecimals)); + // 1000 BTs = 1000*10^18 BTWei + const expectedTransferAmount1 = new BN(1000).mul(tenPowerTokenDecimals); + // 500 BTs = 500*10^18 BTWei + const expectedTransferAmount2 = new BN(500).mul(tenPowerTokenDecimals); + const transferredAmount1 = await tokenRules.recordedTransfersAmount.call(0); + const transferredAmount2 = await tokenRules.recordedTransfersAmount.call(1); + + const actualTransfersAmountLength = await tokenRules.recordedTransfersAmountLength.call(); + + assert.strictEqual( + actualFromAddress, + fromAddress, + ); + + assert.isOk( + actualTransfersToLength.eqn(2), + ); + + assert.strictEqual( + actualToAddress1, + to1, + ); + + assert.strictEqual( + actualToAddress2, + to2, + ); + + assert.isOk( + actualTransfersAmountLength.eqn(2), + ); + + assert.isOk( + transferredAmount1.eq(expectedTransferAmount1), + ); + + assert.isOk( + expectedTransferAmount1.eq(convertedAmount1BN), + ); + + assert.isOk( + transferredAmount2.eq(expectedTransferAmount2), + ); + + assert.isOk( + expectedTransferAmount2.eq(convertedAmount2BN), + ); + }); + + it('Checks that TokenRules executeTransfers is called with requiredPriceOracleDecimals = 5 and tokenDecimals = 18.', async () => { + const config = { + requiredPriceOracleDecimals: 5, + }; + const eip20TokenConfig = { + decimals: 18, + }; + const { + organizationWorker, + tokenRules, + conversionRate, + conversionRateDecimals, + requiredPriceOracleDecimals, + pricerRule, + quoteCurrencyCode, + priceOracle, + fromAddress, + tokenDecimals, + } = await prepare(accountProvider, config, eip20TokenConfig); + + // $0.02 = 0.02*10^5(in contract) + const oraclePrice = (0.02 * (10 ** requiredPriceOracleDecimals)) + .toString(); + await priceOracle.setPrice( + oraclePrice, + (await web3.eth.getBlockNumber()) + 10000, + ); + // $1 = 1*10^5(in contract) + const acceptanceMargin = (1 * (10 ** requiredPriceOracleDecimals)) + .toString(); + await pricerRule.setAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + acceptanceMargin, + { from: organizationWorker }, + ); + + const to1 = accountProvider.get(); + const to2 = accountProvider.get(); + // Amount1 to transfer: $20 = 20*10^5(in contract) + const amount1BN = (20 * (10 ** requiredPriceOracleDecimals)).toString(); + // Amount2 to transfer: $10 = 10*10^5(in contract) + const amount2BN = (10 * (10 ** requiredPriceOracleDecimals)).toString(); + // intendedPriceBN being passed is current PriceOracle price + const intendedPrice = oraclePrice; + + await pricerRule.pay( + fromAddress, + [to1, to2], // 'to' addresses + [amount1BN, amount2BN], // amounts + web3.utils.stringToHex(quoteCurrencyCode), + intendedPrice, + { from: accountProvider.get() }, + ); + const convertedAmount1BN = convertPayCurrencyToToken( + tokenDecimals, + new BN(amount1BN), + new BN(oraclePrice), + new BN(conversionRate), + new BN(conversionRateDecimals), + ); + const convertedAmount2BN = convertPayCurrencyToToken( + tokenDecimals, + new BN(amount2BN), + new BN(oraclePrice), + new BN(conversionRate), + new BN(conversionRateDecimals), + ); + const actualFromAddress = await tokenRules.recordedFrom.call(); + + const actualToAddress1 = await tokenRules.recordedTransfersTo.call(0); + const actualToAddress2 = await tokenRules.recordedTransfersTo.call(1); + const actualTransfersToLength = await tokenRules.recordedTransfersToLength.call(); + + // Number of bt needs to be transferred for a payment shouldn’t depend on + // requiredPriceOracleDecimals. + // requiredPriceOracleDecimals simply decides minimum value in currency(say USD) + // that can be transferred. + // 1000 BTs = 1000*10^18 BTWei + const tenPowerTokenDecimals = (new BN(10)).pow(new BN(tokenDecimals)); + const expectedTransferAmount1 = new BN(1000).mul(tenPowerTokenDecimals); + // 500 BTs = 500*10^18 BTWei + const expectedTransferAmount2 = new BN(500).mul(tenPowerTokenDecimals); + const transferredAmount1 = await tokenRules.recordedTransfersAmount.call(0); + const transferredAmount2 = await tokenRules.recordedTransfersAmount.call(1); + + const actualTransfersAmountLength = await tokenRules.recordedTransfersAmountLength.call(); + + assert.strictEqual( + actualFromAddress, + fromAddress, + ); + + assert.isOk( + actualTransfersToLength.eqn(2), + ); + + assert.strictEqual( + actualToAddress1, + to1, + ); + + assert.strictEqual( + actualToAddress2, + to2, + ); + + assert.isOk( + actualTransfersAmountLength.eqn(2), + ); + + assert.isOk( + expectedTransferAmount1.eq(convertedAmount1BN), + ); + + assert.isOk( + transferredAmount1.eq(expectedTransferAmount1), + ); + + assert.isOk( + expectedTransferAmount2.eq(convertedAmount2BN), + ); + + assert.isOk( + transferredAmount2.eq(expectedTransferAmount2), + ); + }); + + it('Checks that TokenRules executeTransfers is called with requiredPriceOracleDecimals = 18 and tokenDecimals = 5.', async () => { + const config = { + requiredPriceOracleDecimals: 18, + }; + const eip20TokenConfig = { + decimals: 5, + }; + const { + organizationWorker, + tokenRules, + conversionRate, + conversionRateDecimals, + requiredPriceOracleDecimals, + pricerRule, + quoteCurrencyCode, + priceOracle, + fromAddress, + tokenDecimals, + } = await prepare(accountProvider, config, eip20TokenConfig); + + // $0.02 = 0.02*10^18(in contract) + const oraclePrice = (0.02 * (10 ** requiredPriceOracleDecimals)) + .toString(); + await priceOracle.setPrice( + oraclePrice, + (await web3.eth.getBlockNumber()) + 10000, + ); + // $1 = 1*10^18(in contract) + const acceptanceMargin = (1 * (10 ** requiredPriceOracleDecimals)) + .toString(); + await pricerRule.setAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + acceptanceMargin, + { from: organizationWorker }, + ); + + const to1 = accountProvider.get(); + const to2 = accountProvider.get(); + // Amount1 to transfer: $20 = 20*10^18(in contract) + const amount1BN = (20 * (10 ** requiredPriceOracleDecimals)).toString(); + // Amount2 to transfer: $10 = 10*10^18(in contract) + const amount2BN = (10 * (10 ** requiredPriceOracleDecimals)).toString(); + + const intendedPrice = oraclePrice; // intendedPriceBN is Current PriceOracle price + + await pricerRule.pay( + fromAddress, + [to1, to2], // 'to' addresses + [amount1BN, amount2BN], // amounts + web3.utils.stringToHex(quoteCurrencyCode), + intendedPrice, + { from: accountProvider.get() }, + ); + const convertedAmount1BN = convertPayCurrencyToToken( + tokenDecimals, + new BN(amount1BN), + new BN(oraclePrice), + new BN(conversionRate), + new BN(conversionRateDecimals), + ); + const convertedAmount2BN = convertPayCurrencyToToken( + tokenDecimals, + new BN(amount2BN), + new BN(oraclePrice), + new BN(conversionRate), + new BN(conversionRateDecimals), + ); + const actualFromAddress = await tokenRules.recordedFrom.call(); + + const actualToAddress1 = await tokenRules.recordedTransfersTo.call(0); + const actualToAddress2 = await tokenRules.recordedTransfersTo.call(1); + const actualTransfersToLength = await tokenRules.recordedTransfersToLength.call(); + + const tenPowerEIP20TokenDecimal = (new BN(10)).pow(new BN(tokenDecimals)); + // 1000 BTs = 1000*10^5 BTWei + const expectedTransferAmount1 = new BN(1000).mul(tenPowerEIP20TokenDecimal); + // 500 BTs = 500*10^5 BTWei + const expectedTransferAmount2 = new BN(500).mul(tenPowerEIP20TokenDecimal); + const transferredAmount1 = await tokenRules.recordedTransfersAmount.call(0); + const transferredAmount2 = await tokenRules.recordedTransfersAmount.call(1); + + const actualTransfersAmountLength = await tokenRules.recordedTransfersAmountLength.call(); + + assert.strictEqual( + actualFromAddress, + fromAddress, + ); + + assert.isOk( + actualTransfersToLength.eqn(2), + ); + + assert.strictEqual( + actualToAddress1, + to1, + ); + + assert.strictEqual( + actualToAddress2, + to2, + ); + + assert.isOk( + actualTransfersAmountLength.eqn(2), + ); + + assert.isOk( + transferredAmount1.eq(expectedTransferAmount1), + ); + + assert.isOk( + expectedTransferAmount1.eq(convertedAmount1BN), + ); + + assert.isOk( + transferredAmount2.eq(expectedTransferAmount2), + ); + + assert.isOk( + expectedTransferAmount2.eq(convertedAmount2BN), + ); + }); + }); +}); diff --git a/test/pricer_rule/remove_acceptance_margin.js b/test/pricer_rule/remove_acceptance_margin.js new file mode 100644 index 0000000..d5cda93 --- /dev/null +++ b/test/pricer_rule/remove_acceptance_margin.js @@ -0,0 +1,147 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const web3 = require('../test_lib/web3.js'); +const Utils = require('../test_lib/utils'); +const PricerRuleUtils = require('./utils.js'); +const { AccountProvider } = require('../test_lib/utils'); +const { Event } = require('../test_lib/event_decoder'); + +contract('PricerRule::remove_acceptance_margin', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts as a non-organization worker is calling.', async () => { + const { + organizationWorker, + pricerRule, + quoteCurrencyCode, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + await pricerRule.setAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + 11, // acceptance margin + { from: organizationWorker }, + ); + + await Utils.expectRevert( + pricerRule.removeAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + { from: accountProvider.get() }, + ), + 'Should revert as a non-organization worker is calling.', + 'Only whitelisted workers are allowed to call this method.', + ); + }); + + it('Reverts as the specified currency is null.', async () => { + const { + organizationWorker, + pricerRule, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + await Utils.expectRevert( + pricerRule.removeAcceptanceMargin( + web3.utils.stringToHex(''), + { from: organizationWorker }, + ), + 'Should revert as the specified currency is null.', + 'Pay currency code is null.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits AcceptanceMarginRemoved.', async () => { + const { + organizationWorker, + pricerRule, + quoteCurrencyCode, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + const acceptanceMargin = 11; + + await pricerRule.setAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + acceptanceMargin, + { from: organizationWorker }, + ); + + const response = await pricerRule.removeAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + { from: organizationWorker }, + ); + + const events = Event.decodeTransactionResponse( + response, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'AcceptanceMarginRemoved', + args: { + _quoteCurrencyCode: web3.utils.stringToHex(quoteCurrencyCode), + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + it('Checks that acceptance margin is removed.', async () => { + const { + organizationWorker, + pricerRule, + quoteCurrencyCode, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + const acceptanceMargin = 11; + + await pricerRule.setAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + acceptanceMargin, + { from: organizationWorker }, + ); + + let actualAcceptanceMargin = await pricerRule.baseCurrencyPriceAcceptanceMargins.call( + web3.utils.stringToHex(quoteCurrencyCode), + ); + + assert.isOk( + actualAcceptanceMargin.eqn(acceptanceMargin), + ); + + await pricerRule.removeAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + { from: organizationWorker }, + ); + + actualAcceptanceMargin = await pricerRule.baseCurrencyPriceAcceptanceMargins.call( + web3.utils.stringToHex(quoteCurrencyCode), + ); + + assert.isOk( + actualAcceptanceMargin.eqn(0), + ); + }); + }); +}); diff --git a/test/pricer_rule/remove_price_oracle.js b/test/pricer_rule/remove_price_oracle.js new file mode 100644 index 0000000..4102385 --- /dev/null +++ b/test/pricer_rule/remove_price_oracle.js @@ -0,0 +1,146 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const web3 = require('../test_lib/web3.js'); +const Utils = require('../test_lib/utils'); +const PricerRuleUtils = require('./utils.js'); +const { AccountProvider } = require('../test_lib/utils'); +const { Event } = require('../test_lib/event_decoder'); + +contract('PricerRule::remove_price_oracle', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts as a non-organization worker is calling.', async () => { + const { + organizationWorker, + pricerRule, + quoteCurrencyCode, + priceOracle, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + await pricerRule.addPriceOracle( + priceOracle.address, + { from: organizationWorker }, + ); + + await Utils.expectRevert( + pricerRule.removePriceOracle( + web3.utils.stringToHex(quoteCurrencyCode), + { from: accountProvider.get() }, + ), + 'Should revert as a non-organization worker is calling.', + 'Only whitelisted workers are allowed to call this method.', + ); + }); + + it('Reverts as a price oracle does not exist.', async () => { + const { + organizationWorker, + pricerRule, + quoteCurrencyCode, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + await Utils.expectRevert( + pricerRule.removePriceOracle( + web3.utils.stringToHex(quoteCurrencyCode), + { from: organizationWorker }, + ), + 'Should revert as the price oracle already exists.', + 'Price oracle to remove does not exist.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits PriceOracleRemoved.', async () => { + const { + organizationWorker, + pricerRule, + quoteCurrencyCode, + priceOracle, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + await pricerRule.addPriceOracle( + priceOracle.address, + { from: organizationWorker }, + ); + + const response = await pricerRule.removePriceOracle( + web3.utils.stringToHex(quoteCurrencyCode), + { from: organizationWorker }, + ); + + const events = Event.decodeTransactionResponse( + response, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'PriceOracleRemoved', + args: { + _priceOracle: priceOracle.address, + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + it('Checks that price oracle is removed.', async () => { + const { + organizationWorker, + pricerRule, + quoteCurrencyCode, + priceOracle, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + await pricerRule.addPriceOracle( + priceOracle.address, + { from: organizationWorker }, + ); + + let actualPriceOracle = await pricerRule.baseCurrencyPriceOracles.call( + web3.utils.stringToHex(quoteCurrencyCode), + ); + + assert.strictEqual( + actualPriceOracle, + priceOracle.address, + ); + + await pricerRule.removePriceOracle( + web3.utils.stringToHex(quoteCurrencyCode), + { from: organizationWorker }, + ); + + actualPriceOracle = await pricerRule.baseCurrencyPriceOracles.call( + web3.utils.stringToHex(quoteCurrencyCode), + ); + + assert.strictEqual( + actualPriceOracle, + Utils.NULL_ADDRESS, + ); + }); + }); +}); diff --git a/test/pricer_rule/set_acceptance_margin.js b/test/pricer_rule/set_acceptance_margin.js new file mode 100644 index 0000000..d8aa7b8 --- /dev/null +++ b/test/pricer_rule/set_acceptance_margin.js @@ -0,0 +1,137 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const web3 = require('../test_lib/web3.js'); +const Utils = require('../test_lib/utils'); +const PricerRuleUtils = require('./utils.js'); +const { AccountProvider } = require('../test_lib/utils'); +const { Event } = require('../test_lib/event_decoder'); + + +contract('PricerRule::set_acceptance_margin', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts as a non-organization worker is calling.', async () => { + const { + pricerRule, + quoteCurrencyCode, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + await Utils.expectRevert( + pricerRule.setAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + 10, // acceptance margin + { + from: accountProvider.get(), + }, + ), + 'Should revert as a non-organization worker is calling.', + 'Only whitelisted workers are allowed to call this method.', + ); + }); + + it('Reverts as the currency code is null.', async () => { + const { + organizationWorker, + pricerRule, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + await Utils.expectRevert( + pricerRule.setAcceptanceMargin( + web3.utils.stringToHex(''), + 10, // acceptance margin + { from: organizationWorker }, + ), + 'Should revert as the price code is null.', + 'Pay currency code is null.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits AcceptanceMarginSet.', async () => { + const { + organizationWorker, + pricerRule, + quoteCurrencyCode, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + const acceptanceMargin = 11; + + const response = await pricerRule.setAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + acceptanceMargin, + { from: organizationWorker }, + ); + + const events = Event.decodeTransactionResponse( + response, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'AcceptanceMarginSet', + args: { + _quoteCurrencyCode: web3.utils.stringToHex(quoteCurrencyCode), + _acceptanceMargin: new BN(acceptanceMargin), + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + it('Checks that acceptance margin is added if it does not exist.', async () => { + const { + organizationWorker, + pricerRule, + quoteCurrencyCode, + } = await PricerRuleUtils.createTokenEconomy(accountProvider); + + let actualAcceptanceMargin = await pricerRule.baseCurrencyPriceAcceptanceMargins.call( + web3.utils.stringToHex(quoteCurrencyCode), + ); + + assert.isOk( + actualAcceptanceMargin.eqn(0), + ); + + const acceptanceMargin = 11; + + await pricerRule.setAcceptanceMargin( + web3.utils.stringToHex(quoteCurrencyCode), + acceptanceMargin, + { from: organizationWorker }, + ); + + actualAcceptanceMargin = await pricerRule.baseCurrencyPriceAcceptanceMargins.call( + web3.utils.stringToHex(quoteCurrencyCode), + ); + + assert.isOk( + actualAcceptanceMargin.eqn(acceptanceMargin), + ); + }); + }); +}); diff --git a/test/pricer_rule/utils.js b/test/pricer_rule/utils.js new file mode 100644 index 0000000..984894c --- /dev/null +++ b/test/pricer_rule/utils.js @@ -0,0 +1,130 @@ +// Copyright 2018 OST.com Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const web3 = require('../test_lib/web3.js'); + +const EIP20TokenFake = artifacts.require('EIP20TokenFake'); +const TokenRulesSpy = artifacts.require('TokenRulesSpy'); +const Organization = artifacts.require('Organization'); +const PricerRule = artifacts.require('PricerRule'); +const PriceOracleFake = artifacts.require('PriceOracleFake'); + +/** + * Creates an EIP20 instance to be used during TokenRules::executeTransfers + * function's testing with the following defaults: + * - symbol: 'OST' + * - name: 'Open Simple Token' + * - decimals: 1 + * @param config {Object} + * config.decimals Configurable token decimals value. + */ +module.exports.createEIP20Token = async (config) => { + const token = await EIP20TokenFake.new( + 'OST', + 'Open Simple Token', + config.decimals || 1, + ); + + return token; +}; + +module.exports.createOrganization = async (accountProvider) => { + const organizationOwner = accountProvider.get(); + const organizationWorker = accountProvider.get(); + + const organization = await Organization.new( + organizationOwner, + organizationOwner, + [organizationWorker], + (await web3.eth.getBlockNumber()) + 100000, + { from: accountProvider.get() }, + ); + + return { + organization, + organizationOwner, + organizationWorker, + }; +}; + +/** + * Creates and returns the tuple: + * (tokenRules, organizationAddress, token) + * @param config {Object} + * config.requiredPriceOracleDecimals Configurable required price oracle decimals. + * @param eip20TokenConfig {Object} + * config.decimals Configurable token decimals value. + */ +module.exports.createTokenEconomy = async (accountProvider, config = {}, eip20TokenConfig = {}) => { + const { + organization, + organizationOwner, + organizationWorker, + } = await this.createOrganization(accountProvider); + + const tokenDecimals = eip20TokenConfig.decimals; + const token = await this.createEIP20Token(eip20TokenConfig); + + const tokenRules = await TokenRulesSpy.new(); + + const baseCurrencyCode = 'OST'; + + // To derive 1 OST = 1 BT, if conversionRateDecimals = 5, then conversionRate needs to be 10^5. + const conversionRate = 100000; + const conversionRateDecimals = 5; + + const requiredPriceOracleDecimals = config.requiredPriceOracleDecimals || 18; + + const pricerRule = await PricerRule.new( + organization.address, + token.address, + web3.utils.stringToHex(baseCurrencyCode.toString()), + conversionRate, + conversionRateDecimals, + requiredPriceOracleDecimals, + tokenRules.address, + ); + + const quoteCurrencyCode = 'USD'; + const priceOracleInitialPrice = (0.02 * (10 ** requiredPriceOracleDecimals)) + .toString(); + const initialPriceExpirationHeight = (await web3.eth.getBlockNumber()) + 10000; + + const priceOracle = await PriceOracleFake.new( + web3.utils.stringToHex(baseCurrencyCode), + web3.utils.stringToHex(quoteCurrencyCode), + requiredPriceOracleDecimals, + priceOracleInitialPrice, + initialPriceExpirationHeight, + ); + + return { + organization, + organizationOwner, + organizationWorker, + token, + tokenDecimals, + tokenRules, + baseCurrencyCode, + conversionRate, + conversionRateDecimals, + requiredPriceOracleDecimals, + pricerRule, + quoteCurrencyCode, + priceOracleInitialPrice, + priceOracle, + }; +}; diff --git a/test/proxy/constructor.js b/test/proxy/constructor.js new file mode 100644 index 0000000..c9efb2b --- /dev/null +++ b/test/proxy/constructor.js @@ -0,0 +1,79 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const EthUtils = require('ethereumjs-util'); +const Utils = require('../test_lib/utils.js'); +const web3 = require('../test_lib/web3.js'); +const { AccountProvider } = require('../test_lib/utils.js'); + +const ProxyContract = artifacts.require('Proxy'); + +contract('Proxy::constructor', async () => { + contract('Negative Tests', async () => { + it('Reverts if the master copy address is null.', async () => { + await Utils.expectRevert( + ProxyContract.new( + Utils.NULL_ADDRESS, + ), + 'Should revert as the master copy address is null.', + 'Master copy address is null.', + ); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that passed arguments are set correctly.', async () => { + const masterCopyAddress = accountProvider.get(); + + const proxyContract = await ProxyContract.new( + masterCopyAddress, + ); + + assert.strictEqual( + await proxyContract.masterCopy.call(), + masterCopyAddress, + ); + }); + + it('Checks that the master copy address is at 0 position of storage.', async () => { + const masterCopyAddress = accountProvider.get(); + + const proxyContract = await ProxyContract.new( + masterCopyAddress, + ); + + const addressAtStorage0 = await web3.eth.getStorageAt( + proxyContract.address, 0, + ); + + // web3.eth.getStorageAt() function returns an address in lowercase and + // without leading zeros. Hence, before comparing the retrieved address + // without expected ones, we add the leading zeros (till 20 bytes) to + // the retrieved address and convert the expected address (mastercopy) + // to lowercase. + + assert.strictEqual( + Buffer.compare( + EthUtils.setLengthLeft(EthUtils.toBuffer(addressAtStorage0), 20), + EthUtils.toBuffer(masterCopyAddress.toLowerCase()), + ), + 0, + ); + }); + }); +}); diff --git a/test/proxy/fallback.js b/test/proxy/fallback.js new file mode 100644 index 0000000..b6f5cfa --- /dev/null +++ b/test/proxy/fallback.js @@ -0,0 +1,219 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const Utils = require('../test_lib/utils.js'); +const { Event } = require('../test_lib/event_decoder'); +const { AccountProvider } = require('../test_lib/utils.js'); + +const ProxyContract = artifacts.require('Proxy'); +const MasterCopySpy = artifacts.require('MasterCopySpy'); + + +contract('Proxy::fallback', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that initialization through setup does in proxy\'s storage.', async () => { + const mcInitialBalanceConstructor = 11; + const mcInitialBalanceSetup = 22; + + const mc = await MasterCopySpy.new(mcInitialBalanceConstructor); + const proxy = await ProxyContract.new(mc.address); + const pmc = await MasterCopySpy.at(proxy.address); + await pmc.setup(mcInitialBalanceSetup); + + assert.isOk( + (await mc.remainingBalance.call()).eqn(mcInitialBalanceConstructor), + ); + + assert.isOk( + (await pmc.remainingBalance.call()).eqn(mcInitialBalanceSetup), + ); + }); + + it('Checks msg.value and msg.sender correctness.', async () => { + const initialBalance = 22; + + const mc = await MasterCopySpy.new(initialBalance); + const proxy = await ProxyContract.new(mc.address); + const pmc = await MasterCopySpy.at(proxy.address); + await pmc.setup(initialBalance); + + const msgValue = 11; + const msgSender = accountProvider.get(); + const currencyCode = 7; + const beneficiary = accountProvider.get(); + + await pmc.pay( + currencyCode, + beneficiary, + { + value: msgValue, + from: msgSender, + }, + ); + + assert.strictEqual( + await pmc.recordedMsgSender.call(), + msgSender, + ); + + assert.strictEqual( + await mc.recordedMsgSender.call(), + Utils.NULL_ADDRESS, + ); + + assert.isOk( + (await pmc.recordedMsgValue.call()).eqn(msgValue), + ); + + assert.isOk( + (await mc.recordedMsgValue.call()).eqn(0), + ); + }); + + it('Checks that a call through proxy updates only proxy\'s storage.', async () => { + const initialBalance = 22; + + const mc = await MasterCopySpy.new(initialBalance); + const proxy = await ProxyContract.new(mc.address); + const pmc = await MasterCopySpy.at(proxy.address); + await pmc.setup(initialBalance); + + const msgValue = 11; + const currencyCode = 7; + const beneficiary = accountProvider.get(); + + await pmc.pay( + currencyCode, + beneficiary, + { + value: msgValue, + }, + ); + + assert.strictEqual( + await pmc.recordedBeneficiary.call(), + beneficiary, + ); + + assert.isOk( + (await pmc.recordedCurrencyCode.call()).eqn(currencyCode), + ); + + assert.isOk( + (await pmc.remainingBalance.call()).eqn(initialBalance - msgValue), + ); + + assert.strictEqual( + await mc.recordedBeneficiary.call(), + Utils.NULL_ADDRESS, + ); + + assert.isOk( + (await mc.recordedCurrencyCode.call()).eqn(0), + ); + + assert.isOk( + (await mc.remainingBalance.call()).eqn(initialBalance), + ); + }); + + it('Checks that return data is correctly propagated back.', async () => { + const initialBalance = 22; + + const mc = await MasterCopySpy.new(initialBalance); + const proxy = await ProxyContract.new(mc.address); + const pmc = await MasterCopySpy.at(proxy.address); + await pmc.setup(initialBalance); + + const msgValue = 11; + const currencyCode = 7; + const beneficiary = accountProvider.get(); + + const remainingBalance = await pmc.pay.call( + currencyCode, + beneficiary, + { + value: msgValue, + }, + ); + + assert.isOk( + remainingBalance.eqn(initialBalance - msgValue), + ); + }); + + it('Checks that exception is handled properly.', async () => { + const initialBalance = 22; + + const mc = await MasterCopySpy.new(initialBalance); + const proxy = await ProxyContract.new(mc.address); + const pmc = await MasterCopySpy.at(proxy.address); + await pmc.setup(initialBalance); + + const currencyCode = 7; + const beneficiary = accountProvider.get(); + + await Utils.expectRevert( + pmc.pay.call( + currencyCode, + beneficiary, + { + value: initialBalance + 1, + }, + ), + ); + }); + + it('Checks the event emition.', async () => { + const initialBalance = 22; + + const mc = await MasterCopySpy.new(initialBalance); + const proxy = await ProxyContract.new(mc.address); + const pmc = await MasterCopySpy.at(proxy.address); + await pmc.setup(initialBalance); + + const msgValue = 11; + const currencyCode = 7; + const beneficiary = accountProvider.get(); + + const transactionResponse = await pmc.pay( + currencyCode, + beneficiary, + { + value: msgValue, + }, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'Payment', + args: { + _beneficiary: beneficiary, + _amount: new BN(msgValue), + }, + }); + }); +}); diff --git a/test/proxy_factory/create_proxy.js b/test/proxy_factory/create_proxy.js new file mode 100644 index 0000000..8daa21c --- /dev/null +++ b/test/proxy_factory/create_proxy.js @@ -0,0 +1,151 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const Utils = require('../test_lib/utils.js'); +const web3 = require('../test_lib/web3.js'); +const { Event } = require('../test_lib/event_decoder'); +const { AccountProvider } = require('../test_lib/utils.js'); + +const ProxyContract = artifacts.require('Proxy'); +const ProxyFactory = artifacts.require('ProxyFactory'); +const MasterCopySpy = artifacts.require('MasterCopySpy'); + +function generateSetupFunctionData(balance) { + return web3.eth.abi.encodeFunctionCall( + { + name: 'setup', + type: 'function', + inputs: [ + { + type: 'uint256', + name: 'balance', + }, + ], + }, + [balance], + ); +} + + +contract('ProxyFactory::createProxy', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + contract('Negative Tests', async () => { + it('Reverts if master copy address is null.', async () => { + const proxyFactory = await ProxyFactory.new(); + + await Utils.expectRevert( + proxyFactory.createProxy( + Utils.NULL_ADDRESS, + '0x', + ), + 'Should revert as the master copy address is null.', + 'Master copy address is null.', + ); + }); + }); + + contract('Proxy', async () => { + it('Checks that proxy constructor with master copy address is called.', async () => { + const proxyFactory = await ProxyFactory.new(); + + const masterCopy = accountProvider.get(); + + const proxyAddress = await proxyFactory.createProxy.call( + masterCopy, + '0x', + ); + await proxyFactory.createProxy( + masterCopy, + '0x', + ); + + const proxy = await ProxyContract.at(proxyAddress); + + assert.strictEqual( + await proxy.masterCopy.call(), + masterCopy, + ); + }); + + it('Checks that if "data" is non-empty appropriate function on proxy is called.', async () => { + const proxyFactory = await ProxyFactory.new(); + + const initialBalance = 22; + const masterCopy = await MasterCopySpy.new(initialBalance); + + const initialBalanceInSetupCall = 11; + const setupData = generateSetupFunctionData( + initialBalanceInSetupCall, + ); + + const proxyAddress = await proxyFactory.createProxy.call( + masterCopy.address, + setupData, + ); + await proxyFactory.createProxy( + masterCopy.address, + setupData, + ); + + const proxy = await MasterCopySpy.at(proxyAddress); + + assert.isOk( + (await proxy.remainingBalance.call()).eqn(initialBalanceInSetupCall), + ); + }); + }); + + contract('Events', async () => { + it('Checks that ProxyCreated event is emitted on success.', async () => { + const proxyFactory = await ProxyFactory.new(); + + const initialBalance = 22; + const masterCopy = await MasterCopySpy.new(initialBalance); + + const initialBalanceInSetupCall = 11; + const setupData = generateSetupFunctionData( + initialBalanceInSetupCall, + ); + + + const proxyAddress = await proxyFactory.createProxy.call( + masterCopy.address, + setupData, + ); + const transactionResponse = await proxyFactory.createProxy( + masterCopy.address, + setupData, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'ProxyCreated', + args: { + _proxy: proxyAddress, + }, + }); + }); + }); +}); diff --git a/test/safe_math/add.js b/test/safe_math/add.js new file mode 100644 index 0000000..879f6eb --- /dev/null +++ b/test/safe_math/add.js @@ -0,0 +1,42 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const utils = require('../test_lib/utils'); + +const SafeMathLibraryDouble = artifacts.require('SafeMathLibraryDouble'); + + +contract('SafeMath::add', async () => { + it('Checks that addition works correctly.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(5678); + const b = new BN(1234); + + const result = await SafeMath.add.call(a, b); + + assert(result.eq(a.add(b))); + }); + + it('Checks that addition throws on overflow.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = utils.MAX_UINT256; + const b = new BN(1); + await utils.expectRevert(SafeMath.add(a, b)); + }); +}); diff --git a/test/safe_math/div.js b/test/safe_math/div.js new file mode 100644 index 0000000..5688c1c --- /dev/null +++ b/test/safe_math/div.js @@ -0,0 +1,42 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const utils = require('../test_lib/utils'); + +const SafeMathLibraryDouble = artifacts.require('SafeMathLibraryDouble'); + +contract('SafeMath::div', async () => { + it('Checks that that division works correctly.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(5678); + const b = new BN(5678); + + const result = await SafeMath.div.call(a, b); + + assert(result.eq(a.div(b))); + }); + + it('Checks that division throws on zero division.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(5678); + const b = new BN(0); + + await utils.expectRevert(SafeMath.div(a, b)); + }); +}); diff --git a/test/safe_math/mod.js b/test/safe_math/mod.js new file mode 100644 index 0000000..d12e755 --- /dev/null +++ b/test/safe_math/mod.js @@ -0,0 +1,77 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const utils = require('../test_lib/utils'); + +const SafeMathLibraryDouble = artifacts.require('SafeMathLibraryDouble'); + +contract('SafeMath::mod', async () => { + contract('Correct Cases', async () => { + it('Checks correctness when dividend is smaller than divisor.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(284); + const b = new BN(5678); + + const result = await SafeMath.mod.call(a, b); + + assert(result.eq(a.mod(b))); + }); + + it('Check correctness when dividend is equal to divisor.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(5678); + const b = new BN(5678); + + const result = await SafeMath.mod.call(a, b); + + assert(result.eq(a.mod(b))); + }); + + it('Checks correctness when dividend is larger than divisor.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(7000); + const b = new BN(5678); + + const result = await SafeMath.mod.call(a, b); + + assert(result.eq(a.mod(b))); + }); + + it('Checks correctness when dividend is a multiple of divisor.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(17034); // 17034 == 5678 * 3 + const b = new BN(5678); + + const result = await SafeMath.mod.call(a, b); + + assert(result.eq(a.mod(b))); + }); + }); + + it('Checks that modulus reverts on zero division.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(5678); + const b = new BN(0); + + await utils.expectRevert(SafeMath.mod(a, b)); + }); +}); diff --git a/test/safe_math/mul.js b/test/safe_math/mul.js new file mode 100644 index 0000000..2b8aff6 --- /dev/null +++ b/test/safe_math/mul.js @@ -0,0 +1,53 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const utils = require('../test_lib/utils'); + +const SafeMathLibraryDouble = artifacts.require('SafeMathLibraryDouble'); + +contract('SafeMath::mul', async () => { + it('Checks that multiplication of non-zero args works correctly.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(1234); + const b = new BN(5678); + + const result = await SafeMath.mul.call(a, b); + + assert(result.eq(a.mul(b))); + }); + + it('Checks that multiplication of a zero arg works correctly.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(0); + const b = new BN(5678); + + const result = await SafeMath.mul.call(a, b); + + assert(result.eq(a.mul(b))); + }); + + it('Checks that multiplication throws on overflow.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = utils.MAX_UINT256; + const b = new BN(2); + + await utils.expectRevert(SafeMath.mul(a, b)); + }); +}); diff --git a/test/safe_math/sub.js b/test/safe_math/sub.js new file mode 100644 index 0000000..0983721 --- /dev/null +++ b/test/safe_math/sub.js @@ -0,0 +1,42 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const utils = require('../test_lib/utils'); + +const SafeMathLibraryDouble = artifacts.require('SafeMathLibraryDouble'); + +contract('SafeMath::sub', async () => { + it('Checks correctness of substract.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(5678); + const b = new BN(1234); + + const result = await SafeMath.sub.call(a, b); + + assert(result.eq(a.sub(b))); + }); + + it('Checks that throws if subtraction result is negative.', async () => { + const SafeMath = await SafeMathLibraryDouble.new(); + + const a = new BN(1234); + const b = new BN(5678); + + await utils.expectRevert(SafeMath.sub.call(a, b)); + }); +}); diff --git a/test/test_lib/RevertProxy.sol b/test/test_lib/RevertProxy.sol index ec7fbda..c2398a7 100644 --- a/test/test_lib/RevertProxy.sol +++ b/test/test_lib/RevertProxy.sol @@ -1,6 +1,6 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.5.0; -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -111,7 +111,7 @@ contract RevertProxy { * @return `true` if the call was successful and did not revert, `false` if * it reverted. */ - function execute() external returns (bool) { + function execute() external returns (bool, bytes memory) { // solium-disable-next-line security/no-low-level-calls return target.call(data); } diff --git a/test/test_lib/event_decoder.js b/test/test_lib/event_decoder.js index bfc7857..56a2b05 100644 --- a/test/test_lib/event_decoder.js +++ b/test/test_lib/event_decoder.js @@ -1,4 +1,4 @@ -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,64 +12,66 @@ // See the License for the specific language governing permissions and // limitations under the License. +'use strict'; + const web3 = require('../test_lib/web3.js'); class Event { - static decodeTransactionResponse(transactionResponse) { - const events = []; - - assert.isOk(Object.prototype.hasOwnProperty.call( - transactionResponse, 'logs', - )); + static decodeTransactionResponse(transactionResponse) { + const events = []; - const { logs } = transactionResponse; + assert.isOk(Object.prototype.hasOwnProperty.call( + transactionResponse, 'logs', + )); - for (let i = 0; i < logs.length; i += 1) { - events.push({ - name: logs[i].event, - args: logs[i].args, - }); - } + const { logs } = transactionResponse; - return events; + for (let i = 0; i < logs.length; i += 1) { + events.push({ + name: logs[i].event, + args: logs[i].args, + }); } - static assertEqual(actual, expected) { - assert.strictEqual(actual.name, expected.name); - Object.keys(expected.args).forEach((key) => { - if (key !== '0' && key !== '1' && key !== '__length__') { - assert.isOk(Object.hasOwnProperty.call(actual.args, key)); - if (web3.utils.isBN(expected.args[key])) { - assert.isOk(web3.utils.isBN(actual.args[key])); - assert.isOk(expected.args[key].eq(actual.args[key])); - } else { - assert.strictEqual(actual.args[key], expected.args[key]); - } - } - }); + return events; + } - Object.keys(actual.args).forEach((key) => { - if (Number.isNaN(Number.parseInt(key, 10)) && key !== '__length__') { - assert.isOk(Object.hasOwnProperty.call(expected.args, key)); - } - }); - } + static assertEqual(actual, expected) { + assert.strictEqual(actual.name, expected.name); + Object.keys(expected.args).forEach((key) => { + if (key !== '0' && key !== '1' && key !== '__length__') { + assert.isOk(Object.hasOwnProperty.call(actual.args, key)); + if (web3.utils.isBN(expected.args[key])) { + assert.isOk(web3.utils.isBN(actual.args[key])); + assert.isOk(expected.args[key].eq(actual.args[key])); + } else { + assert.strictEqual(actual.args[key], expected.args[key]); + } + } + }); - static assertEqualMulti(actualList, expectedList) { - assert.strictEqual( - actualList.length, - expectedList.length, - 'Length of actual event list and expected ones should be equal.', - ); + Object.keys(actual.args).forEach((key) => { + if (Number.isNaN(Number.parseInt(key, 10)) && key !== '__length__') { + assert.isOk(Object.hasOwnProperty.call(expected.args, key)); + } + }); + } - for (let i = 0; i < actualList.lengh; i += 1) { - const actual = actualList[i]; - const expected = expectedList[i]; - this.assertEqual(actual, expected); - } + static assertEqualMulti(actualList, expectedList) { + assert.strictEqual( + actualList.length, + expectedList.length, + 'Length of actual event list and expected ones should be equal.', + ); + + for (let i = 0; i < actualList.lengh; i += 1) { + const actual = actualList[i]; + const expected = expectedList[i]; + this.assertEqual(actual, expected); } + } } module.exports = { - Event, + Event, }; diff --git a/test/test_lib/utils.js b/test/test_lib/utils.js index d97d3ea..cd35322 100644 --- a/test/test_lib/utils.js +++ b/test/test_lib/utils.js @@ -12,84 +12,212 @@ // See the License for the specific language governing permissions and // limitations under the License. +'use strict'; + +const BN = require('bn.js'); const assert = require('assert'); +const EthUtils = require('ethereumjs-util'); const web3 = require('./web3.js'); const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; -module.exports.NULL_ADDRESS = NULL_ADDRESS; +const MAX_UINT256 = new BN(2).pow(new BN(256)).sub(new BN(1)); + +const NULL_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; + +const generateExTxHash = ( + from, to, data, nonce, callPrefix, +) => web3.utils.soliditySha3( + { + t: 'bytes1', v: '0x19', + }, + { + t: 'bytes1', v: '0x0', + }, + { + t: 'address', v: from, + }, + { + t: 'address', v: to, + }, + { + t: 'uint8', v: 0, + }, + { + t: 'bytes32', v: web3.utils.keccak256(data), + }, + { + t: 'uint256', v: nonce, + }, + { + t: 'uint8', v: 0, + }, + { + t: 'uint8', v: 0, + }, + { + t: 'uint8', v: 0, + }, + { + t: 'bytes4', v: callPrefix, + }, + { + t: 'uint8', v: 0, + }, + { + t: 'bytes32', v: '0x0', + }, +); + +module.exports = { + + NULL_ADDRESS, + + NULL_BYTES32, + + MAX_UINT256, -module.exports.isNullAddress = address => address === NULL_ADDRESS; + isNullAddress: address => address === NULL_ADDRESS, -/** - * Asserts that a call or transaction reverts. - * - * @param {promise} promise The call or transaction. - * @param {string} expectedMessage Optional. If given, the revert message will - * be checked to contain this string. - * - * @throws Will fail an assertion if the call or transaction is not reverted. - */ -module.exports.expectRevert = async ( + /** + * Asserts that a call or transaction reverts. + * + * @param {promise} promise The call or transaction. + * @param {string} expectedMessage Optional. If given, the revert message will + * be checked to contain this string. + * + * @throws Will fail an assertion if the call or transaction is not reverted. + */ + expectRevert: async ( promise, displayMessage, expectedRevertMessage, -) => { + ) => { try { - await promise; + await promise; } catch (error) { - assert( - error.message.search('revert') > -1, - `The contract should revert. Instead: ${error.message}`, - ); - - if (expectedRevertMessage !== undefined) { - if (error.reason !== undefined) { - assert( - expectedRevertMessage === error.reason, - `\nThe contract should revert with:\n\t"${expectedRevertMessage}" ` - + `\ninstead received:\n\t"${error.reason}"\n`, - ); - } else { - assert( - error.message.search(expectedRevertMessage) > -1, - `\nThe contract should revert with:\n\t"${expectedRevertMessage}" ` - + `\ninstead received:\n\t"${error.message}"\n`, - ); - } + assert( + error.message.search('revert') > -1, + `The contract should revert. Instead: ${error.message}`, + ); + + if (expectedRevertMessage !== undefined) { + if (error.reason !== undefined) { + assert( + expectedRevertMessage === error.reason, + `\nThe contract should revert with:\n\t"${expectedRevertMessage}" ` + + `\ninstead received:\n\t"${error.reason}"\n`, + ); + } else { + assert( + error.message.search(expectedRevertMessage) > -1, + `\nThe contract should revert with:\n\t"${expectedRevertMessage}" ` + + `\ninstead received:\n\t"${error.message}"\n`, + ); } + } - return; + return; } assert(false, displayMessage); -}; + }, -module.exports.advanceBlock = () => new Promise((resolve, reject) => { + advanceBlock: () => new Promise((resolve, reject) => { web3.currentProvider.send({ - jsonrpc: '2.0', - method: 'evm_mine', - id: new Date().getTime(), + jsonrpc: '2.0', + method: 'evm_mine', + id: new Date().getTime(), }, (err) => { - if (err) { - return reject(err); - } + if (err) { + return reject(err); + } - const newBlockHash = web3.eth.getBlock('latest').hash; + const newBlockHash = web3.eth.getBlock('latest').hash; - return resolve(newBlockHash); + return resolve(newBlockHash); }); -}); + }), -/** Receives accounts list and gives away each time one. */ -module.exports.AccountProvider = class AccountProvider { + /** Receives accounts list and gives away each time one. */ + AccountProvider: class AccountProvider { constructor(accounts) { - this.accounts = accounts; - this.index = 0; + this.accounts = accounts; + this.index = 0; } get() { - assert(this.index < this.accounts.length); - const account = this.accounts[this.index]; - this.index += 1; - return account; + assert(this.index < this.accounts.length); + const account = this.accounts[this.index]; + this.index += 1; + return account; } + }, + + generateExTx: ( + from, to, data, nonce, callPrefix, sessionPrivateKey, + ) => { + const exTxHash = generateExTxHash( + from, to, data, nonce, callPrefix, + ); + + const exTxSignature = EthUtils.ecsign( + EthUtils.toBuffer(exTxHash), + EthUtils.toBuffer(sessionPrivateKey), + ); + + return { exTxHash, exTxSignature }; + }, + + verifyCallPrefixConstant(methodName, callPrefix, contractName) { + const contract = artifacts.require(contractName); + + let methodConcat = methodName.concat('('); + let input; + let abiMethod; + + for (let i = 0; i < contract.abi.length; i += 1) { + abiMethod = contract.abi[i]; + if (abiMethod.name === methodName) { + for (let j = 0; j < abiMethod.inputs.length - 1; j += 1) { + input = abiMethod.inputs[j].type; + methodConcat = methodConcat.concat(input, ','); + } + input = abiMethod.inputs[abiMethod.inputs.length - 1].type; + methodConcat += input; + } + } + methodConcat = methodConcat.concat(')'); + + const expectedPrefix = web3.utils.soliditySha3(methodConcat).substring(0, 10); + + assert.strictEqual(expectedPrefix, callPrefix, `Expected ${methodName} callprefix is ${callPrefix} but got ${expectedPrefix}`); + }, + + getParamFromTxEvent: ( + transaction, contractAddress, eventName, paramName, + ) => { + assert( + typeof transaction === 'object' + && !Array.isArray(transaction) + && transaction !== null, + ); + assert(eventName !== ''); + assert(contractAddress !== ''); + + const { logs } = transaction; + + const filteredLogs = logs.filter( + l => l.event === eventName && l.address === contractAddress, + ); + + assert( + filteredLogs.length === 1, + 'Too many entries found after filtering the logs.', + ); + + const param = filteredLogs[0].args[paramName]; + + assert(typeof param !== 'undefined'); + + return param; + }, }; diff --git a/test/test_lib/web3.js b/test/test_lib/web3.js index 80be2e2..efcf37a 100644 --- a/test/test_lib/web3.js +++ b/test/test_lib/web3.js @@ -1,4 +1,4 @@ -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +'use strict'; const Web3 = require('web3'); const web3 = new Web3( - new Web3.providers.WebsocketProvider('ws://localhost:8545'), + new Web3.providers.WebsocketProvider('ws://localhost:8545'), ); module.exports = web3; diff --git a/test/token_holder/authorize_session.js b/test/token_holder/authorize_session.js new file mode 100644 index 0000000..0e09b88 --- /dev/null +++ b/test/token_holder/authorize_session.js @@ -0,0 +1,330 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const web3 = require('../test_lib/web3.js'); +const utils = require('../test_lib/utils.js'); +const { TokenHolderUtils } = require('./utils.js'); +const { Event } = require('../test_lib/event_decoder'); +const { AccountProvider } = require('../test_lib/utils.js'); + +const sessionPublicKey1 = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; +const sessionPublicKey2 = '0xBB04e8665d3C53B7dB4E7e468E5B5813714ade82'; + +async function prepare( + accountProvider, + spendingLimit, deltaExpirationHeight, + sessionPublicKeyToAuthorize, +) { + const { utilityToken } = await TokenHolderUtils.createUtilityMockToken(); + + const { tokenRules } = await TokenHolderUtils.createMockTokenRules(); + + const { + tokenHolderOwnerAddress, + tokenHolder, + } = await TokenHolderUtils.createTokenHolder( + accountProvider, + utilityToken, tokenRules, + spendingLimit, deltaExpirationHeight, + sessionPublicKeyToAuthorize, + ); + + await TokenHolderUtils.authorizeSessionKey( + tokenHolder, tokenHolderOwnerAddress, + sessionPublicKeyToAuthorize, spendingLimit, deltaExpirationHeight, + ); + + return { + utilityToken, + tokenRules, + tokenHolderOwnerAddress, + tokenHolder, + }; +} + +contract('TokenHolder::authorizeSession', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if non-owner address calls.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 11 /* spendingLimit */, + 51 /* deltaExpirationHeight */, + sessionPublicKey1, + ); + + const spendingLimit2 = 12; + const deltaExpirationHeight2 = 52; + await utils.expectRevert( + tokenHolder.authorizeSession( + sessionPublicKey2, + spendingLimit2, + (await web3.eth.getBlockNumber()) + deltaExpirationHeight2, + { + from: accountProvider.get(), + }, + ), + 'Should revert as non-owner address calls.', + 'Only owner is allowed to call.', + ); + }); + + it('Reverts as session key to authorize is null.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 11 /* spendingLimit */, + 51 /* deltaExpirationHeight */, + sessionPublicKey1, + ); + + const spendingLimit2 = 12; + const deltaExpirationHeight2 = 52; + await utils.expectRevert( + tokenHolder.authorizeSession( + utils.NULL_ADDRESS, + spendingLimit2, + (await web3.eth.getBlockNumber()) + deltaExpirationHeight2, + { from: tokenHolderOwnerAddress }, + ), + 'Should revert as session key to authorize is null.', + 'Key address is null.', + ); + }); + + it('Reverts if session key to authorize is authorized and active.', async () => { + const spendingLimit1 = 11; + const deltaExpirationHeight1 = 51; + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + spendingLimit1, + deltaExpirationHeight1, + sessionPublicKey1, + ); + + await utils.expectRevert( + tokenHolder.authorizeSession( + sessionPublicKey1, + spendingLimit1, + (await web3.eth.getBlockNumber()) + deltaExpirationHeight1, + { from: tokenHolderOwnerAddress }, + ), + 'Should revert as key to revoke was already revoked.', + 'Key exists.', + ); + }); + + it('Reverts if session key to authorize was authorized and revoked.', async () => { + const spendingLimit1 = 11; + const deltaExpirationHeight1 = 51; + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + spendingLimit1, + deltaExpirationHeight1, + sessionPublicKey1, + ); + + await tokenHolder.revokeSession( + sessionPublicKey1, + { from: tokenHolderOwnerAddress }, + ); + + await utils.expectRevert( + tokenHolder.authorizeSession( + sessionPublicKey1, + spendingLimit1, + (await web3.eth.getBlockNumber()) + deltaExpirationHeight1, + { from: tokenHolderOwnerAddress }, + ), + 'Should revert as key to revoke was already revoked.', + 'Key exists.', + ); + }); + + it('Reverts if session key to authorize was authorized and expired.', async () => { + const deltaExpirationHeight1 = 51; + + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 11 /* spendingLimit */, + deltaExpirationHeight1, + sessionPublicKey1, + ); + + for (let i = 0; i < deltaExpirationHeight1; i += 1) { + // eslint-disable-next-line no-await-in-loop + await utils.advanceBlock(); + } + + const spendingLimit2 = 12; + const deltaExpirationHeight2 = 52; + await utils.expectRevert( + tokenHolder.authorizeSession( + sessionPublicKey1, + spendingLimit2, + (await web3.eth.getBlockNumber()) + deltaExpirationHeight2, + { from: tokenHolderOwnerAddress }, + ), + 'Should revert as key to revoke was already revoked.', + 'Key exists.', + ); + }); + + it('Reverts as expiration height is lte to the current block number.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 10 /* spendingLimit */, + 50 /* deltaExpirationHeight */, + sessionPublicKey1, + ); + + const spendingLimit2 = 12; + await utils.expectRevert( + tokenHolder.authorizeSession( + sessionPublicKey2, + spendingLimit2, + (await web3.eth.getBlockNumber()), + { from: tokenHolderOwnerAddress }, + ), + 'Should .', + 'Expiration height is lte to the current block height.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits SessionAuthorized event.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 11 /* spendingLimit */, + 51 /* deltaExpirationHeight */, + sessionPublicKey1, + ); + + const spendingLimit2 = 12; + const deltaExpirationHeight2 = 52; + const transactionResponse = await tokenHolder.authorizeSession( + sessionPublicKey2, + spendingLimit2, + (await web3.eth.getBlockNumber()) + deltaExpirationHeight2, + { from: tokenHolderOwnerAddress }, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + // The only emitted event should be 'SessionAuthorized'. + Event.assertEqual(events[0], { + name: 'SessionAuthorized', + args: { + _sessionKey: sessionPublicKey2, + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that key is stored correctly after authorization.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 10 /* spendingLimit */, + 50 /* deltaExpirationHeight */, + sessionPublicKey1, + ); + + let keyData = await tokenHolder.sessionKeys.call(sessionPublicKey2); + + assert.isOk( + // TokenHolder.AuthorizationStatus.NOT_AUTHORIZED == 0 + keyData.session.eqn(0), + ); + + assert.isOk( + keyData.spendingLimit.eqn(0), + ); + + assert.isOk( + keyData.expirationHeight.eqn(0), + ); + + assert.isOk( + keyData.nonce.eqn(0), + ); + + const spendingLimit2 = 12; + const deltaExpirationHeight2 = 52; + const expirationHeight2 = (await web3.eth.getBlockNumber()) + + deltaExpirationHeight2; + await tokenHolder.authorizeSession( + sessionPublicKey2, + spendingLimit2, + expirationHeight2, + { from: tokenHolderOwnerAddress }, + ); + + keyData = await tokenHolder.sessionKeys.call(sessionPublicKey2); + + assert.isOk( + keyData.session.eqn(2), + ); + + assert.isOk( + keyData.spendingLimit.eqn(spendingLimit2), + ); + + assert.isOk( + keyData.expirationHeight.eqn(expirationHeight2), + ); + + assert.isOk( + keyData.nonce.eqn(0), + ); + }); + }); +}); diff --git a/test/token_holder/constructor.js b/test/token_holder/constructor.js deleted file mode 100644 index 7099423..0000000 --- a/test/token_holder/constructor.js +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -const utils = require('../test_lib/utils.js'); -const { AccountProvider } = require('../test_lib/utils.js'); - -const TokenHolder = artifacts.require('TokenHolder'); - -contract('TokenHolder::constructor', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if token address is null.', async () => { - const required = 1; - const registeredWallet0 = accountProvider.get(); - - const wallets = [registeredWallet0]; - - const tokenAddress = utils.NULL_ADDRESS; - const tokenRulesAddress = accountProvider.get(); - - await utils.expectRevert( - TokenHolder.new( - tokenAddress, - tokenRulesAddress, - wallets, - required, - ), - 'Should revert as token address is null.', - 'Token contract address is null.', - ); - }); - - it('Reverts if token rules is null.', async () => { - const required = 1; - const registeredWallet0 = accountProvider.get(); - - const wallets = [registeredWallet0]; - - const tokenAddress = accountProvider.get(); - const tokenRulesAddress = utils.NULL_ADDRESS; - - await utils.expectRevert( - TokenHolder.new( - tokenAddress, - tokenRulesAddress, - wallets, - required, - ), - 'Should revert as token rules address is null.', - 'TokenRules contract address is null.', - ); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks that passed arguments are set correctly.', async () => { - const required = 1; - const registeredWallet0 = accountProvider.get(); - - const wallets = [registeredWallet0]; - - const tokenAddress = accountProvider.get(); - const tokenRulesAddress = accountProvider.get(); - - const tokenHolder = await TokenHolder.new( - tokenAddress, - tokenRulesAddress, - wallets, - required, - ); - - assert.strictEqual( - (await tokenHolder.token.call()), - tokenAddress, - ); - - assert.strictEqual( - (await tokenHolder.tokenRules.call()), - tokenRulesAddress, - ); - }); - }); -}); diff --git a/test/token_holder/execute_redemption.js b/test/token_holder/execute_redemption.js new file mode 100644 index 0000000..5154ac1 --- /dev/null +++ b/test/token_holder/execute_redemption.js @@ -0,0 +1,848 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const EthUtils = require('ethereumjs-util'); +const web3 = require('../test_lib/web3.js'); +const Utils = require('../test_lib/utils.js'); +const { TokenHolderUtils } = require('./utils.js'); +const { Event } = require('../test_lib/event_decoder'); +const { AccountProvider } = require('../test_lib/utils.js'); + +const CoGatewaySpy = artifacts.require('CoGatewaySpy.sol'); + +const sessionPublicKey1 = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; +const sessionPrivateKey1 = '0xa8225c01ceeaf01d7bc7c1b1b929037bd4050967c5730c0b854263121b8399f3'; +// const sessionPublicKey2 = '0xBbfd1BF77dA692abc82357aC001415b98d123d17'; +const sessionPrivateKey2 = '0x6817f551bbc3e12b8fe36787ab192c921390d6176a3324ed02f96935a370bc41'; + + +function generateTokenHolderexecuteRedemptionFunctionCallPrefix() { + return web3.eth.abi.encodeFunctionSignature({ + name: 'executeRedemption', + type: 'function', + inputs: [ + { + type: 'address', name: '', + }, + { + type: 'bytes', name: '', + }, + { + type: 'uint256', name: '', + }, + { + type: 'bytes32', name: '', + }, + { + type: 'bytes32', name: '', + }, + { + type: 'uint8', name: '', + }, + ], + }); +} + +function generateCoGatewayRedeemFunctionData( + amount, beneficiary, gasPrice, gasLimit, nonce, hashLock, +) { + return web3.eth.abi.encodeFunctionCall( + { + name: 'redeem', + type: 'function', + inputs: [ + { + type: 'uint256', + name: 'amount', + }, + { + type: 'address', + name: 'beneficiary', + }, + { + type: 'uint256', + name: 'gasPrice', + }, + { + type: 'uint256', + name: 'gasLimit', + }, + { + type: 'uint256', + name: 'nonce', + }, + { + type: 'bytes32', + name: 'hashlock', + }, + ], + }, + [amount, beneficiary, gasPrice, gasLimit, nonce, hashLock], + ); +} + +async function prepare( + accountProvider, + spendingLimit, deltaExpirationHeight, + sessionPublicKeyToAuthorize, +) { + const { utilityToken } = await TokenHolderUtils.createUtilityMockToken(); + + const { tokenRules } = await TokenHolderUtils.createMockTokenRules(); + + const { + tokenHolderOwnerAddress, + tokenHolder, + } = await TokenHolderUtils.createTokenHolder( + accountProvider, + utilityToken, tokenRules, + spendingLimit, deltaExpirationHeight, + sessionPublicKeyToAuthorize, + ); + + await TokenHolderUtils.authorizeSessionKey( + tokenHolder, tokenHolderOwnerAddress, + sessionPublicKeyToAuthorize, spendingLimit, deltaExpirationHeight, + ); + + const coGateway = await CoGatewaySpy.new(); + + await utilityToken.setCoGateway(coGateway.address); + + return { + utilityToken, + tokenRules, + tokenHolderOwnerAddress, + tokenHolder, + coGateway, + }; +} + +async function generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, +) { + const coGatewayRedeemFunctionData = await generateCoGatewayRedeemFunctionData( + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + const { exTxHash, exTxSignature } = await Utils.generateExTx( + tokenHolder.address, + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + generateTokenHolderexecuteRedemptionFunctionCallPrefix(), + sessionPrivateKey, + ); + + return { + coGatewayRedeemFunctionData, + exTxHash, + exTxSignature, + }; +} + +contract('TokenHolder::redeem', async (accounts) => { + contract('Negative Tests', async () => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if ExTx is signed with non-authorized key.', async () => { + const { + tokenHolder, + coGateway, + } = await prepare( + accountProvider, + 50 /* spendingLimit */, + 100 /* deltaExpirationHeight */, + sessionPublicKey1, + ); + + const tokenHolderNonce = 0; + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + const { + coGatewayRedeemFunctionData, + exTxSignature, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey2, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + await Utils.expectRevert( + tokenHolder.executeRedemption( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: 1 /* bounty */, + }, + ), + 'Should revert as ExTx is signed with non-authorized key.', + 'Key\'s session is not equal to contract\'s session window.', + ); + }); + + it('Reverts if ExTx is signed with expired key.', async () => { + const deltaExpirationHeight = 100; + const { + tokenHolder, + coGateway, + } = await prepare( + accountProvider, + 50 /* spendingLimit */, + deltaExpirationHeight, + sessionPublicKey1, + ); + + const tokenHolderNonce = 0; + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + const { + coGatewayRedeemFunctionData, + exTxSignature, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey1, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + for (let i = 0; i < deltaExpirationHeight; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Utils.advanceBlock(); + } + + await Utils.expectRevert( + tokenHolder.executeRedemption( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: 1 /* bounty */, + }, + ), + 'Should revert as ExTx is signed with expired key.', + 'Session key was expired.', + ); + }); + + it('Reverts if ExTx is signed with a revoked key.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + coGateway, + } = await prepare( + accountProvider, + 50, /* spendingLimit */ + 100, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const tokenHolderNonce = 0; + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + const { + coGatewayRedeemFunctionData, + exTxSignature, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey1, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + await tokenHolder.revokeSession( + sessionPublicKey1, + { from: tokenHolderOwnerAddress }, + ); + + await Utils.expectRevert( + tokenHolder.executeRedemption( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: 1 /* bounty */, + }, + ), + 'Should revert as ExTx is signed with a revoked key.', + 'Key\'s session is not equal to contract\'s session window.', + ); + }); + + it('Reverts if ExTx is signed with logged-out key.', async () => { + const { + tokenHolderOwnerAddress, + tokenHolder, + coGateway, + } = await prepare( + accountProvider, + 50 /* spendingLimit */, + 100, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const tokenHolderNonce = 0; + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + const { + coGatewayRedeemFunctionData, + exTxSignature, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey1, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + await tokenHolder.logout( + { from: tokenHolderOwnerAddress }, + ); + + await Utils.expectRevert( + tokenHolder.executeRedemption( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: 1 /* bounty */, + }, + ), + 'Should revert as ExTx is signed with logged-out key.', + 'Key\'s session is not equal to contract\'s session window.', + ); + }); + + it('Reverts if ExTx is signed with an invalid nonce.', async () => { + const { + tokenHolder, + coGateway, + } = await prepare( + accountProvider, + 50, /* spendingLimit */ + 100, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + // Correct nonce is 0. + const tokenHolderInvalidNonce = 1; + + const { + coGatewayRedeemFunctionData: coGatewayRedeemFunctionData0, + exTxSignature: exTxSignature0, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderInvalidNonce, + sessionPrivateKey1, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + await Utils.expectRevert( + tokenHolder.executeRedemption( + coGateway.address, + coGatewayRedeemFunctionData0, + tokenHolderInvalidNonce, + EthUtils.bufferToHex(exTxSignature0.r), + EthUtils.bufferToHex(exTxSignature0.s), + exTxSignature0.v, + { + from: accountProvider.get(), + value: 1 /* bounty */, + }, + ), + 'Should revert as ExTx is signed with an invalid nonce.', + 'Incorrect nonce is specified.', + ); + }); + }); + + contract('Redeem Executed', async () => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that redeem is successfully executed.', async () => { + const { + utilityToken, + tokenHolder, + coGateway, + } = await prepare( + accountProvider, + 50, /* spendingLimit */ + 100, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + const tokenHolderNonce = 0; + + const { + coGatewayRedeemFunctionData, + exTxSignature, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey1, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + const bounty = 1; + + const executionStatus = await tokenHolder.executeRedemption.call( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: bounty, + }, + ); + + assert.isOk(executionStatus); + + const redeemReceipt = await tokenHolder.executeRedemption( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: bounty, + }, + ); + + assert.isOk(redeemReceipt.receipt.status); + + const updatedPayableValue = await coGateway.recordedPayedAmount.call(); + + assert.isOk( + updatedPayableValue.eqn(bounty), + ); + + const allowance = await ( + utilityToken.allowance.call( + tokenHolder.address, coGateway.address, + ) + ); + + assert.strictEqual(allowance.cmp(new BN(0)), 0); + + assert.isOk( + allowance.eqn(0), + ); + }); + }); + + contract('Events', async () => { + const accountProvider = new AccountProvider(accounts); + + it('Emits RuleExecuted event with successful execution status.', async () => { + const { + tokenHolder, + coGateway, + } = await prepare( + accountProvider, + 50, /* spendingLimit */ + 100, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + const tokenHolderNonce = 0; + + const { + coGatewayRedeemFunctionData, + exTxHash, + exTxSignature, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey1, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + const transactionResponse = await tokenHolder.executeRedemption( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: 1, + }, + ); + + const events = Event.decodeTransactionResponse(transactionResponse); + + assert.strictEqual(events.length, 1); + + Event.assertEqual(events[0], { + name: 'RedemptionExecuted', + args: { + _messageHash: exTxHash, + _status: true, + }, + }); + }); + + it('Emits RuleExecuted event with failed execution status.', async () => { + const { + tokenHolder, + coGateway, + } = await prepare( + accountProvider, + 50, /* spendingLimit */ + 100, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + const tokenHolderNonce = 0; + + const { + coGatewayRedeemFunctionData, + exTxHash, + exTxSignature, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey1, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + await coGateway.makeRedemptionToFail(); + + const transactionResponse = await tokenHolder.executeRedemption( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: 1, + }, + ); + + const events = Event.decodeTransactionResponse(transactionResponse); + + assert.strictEqual(events.length, 1); + + Event.assertEqual(events[0], { + name: 'RedemptionExecuted', + args: { + _messageHash: exTxHash, + _status: false, + }, + }); + }); + }); + + contract('Returned Execution Status', async () => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that return value is true in case of successfull execution.', async () => { + const { + tokenHolder, + coGateway, + } = await prepare( + accountProvider, + 50, /* spendingLimit */ + 100, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + const tokenHolderNonce = 0; + + const { + coGatewayRedeemFunctionData, + exTxSignature, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey1, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + const executionStatus = await tokenHolder.executeRedemption.call( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: 1, + }, + ); + + assert.isOk( + executionStatus, + ); + }); + + it('Checks that return value is false in case of failing execution.', async () => { + const { + tokenHolder, + coGateway, + } = await prepare( + accountProvider, + 50, /* spendingLimit */ + 100, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + const tokenHolderNonce = 0; + + const { + coGatewayRedeemFunctionData, + exTxSignature, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey1, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + await coGateway.makeRedemptionToFail(); + + const executionStatus = await tokenHolder.executeRedemption.call( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: 1, + }, + ); + + assert.isNotOk( + executionStatus, + ); + }); + }); + + contract('Nonce handling', async () => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that nonce is incremented in case of successfull execution.', async () => { + const { + tokenHolder, + coGateway, + } = await prepare( + accountProvider, + 50, /* spendingLimit */ + 100, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + const tokenHolderNonce = 0; + + const { + coGatewayRedeemFunctionData, + exTxSignature, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey1, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + await tokenHolder.executeRedemption( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: 1, + }, + ); + + // Checks that nonce is updated. + assert.isOk( + (await tokenHolder.sessionKeys.call(sessionPublicKey1)).nonce.eqn(1), + ); + }); + + it('Checks that return value is false in case of failing execution.', async () => { + const { + tokenHolder, + coGateway, + } = await prepare( + accountProvider, + 50, /* spendingLimit */ + 100, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const amount = 10; + const beneficiary = accountProvider.get(); + const gasPrice = 10; + const gasLimit = 100; + const redeemerNonce = 1; + const hashLock = web3.utils.soliditySha3('hash-lock'); + + const tokenHolderNonce = 0; + + const { + coGatewayRedeemFunctionData, + exTxSignature, + } = await generateCoGatewayRedeemFunctionExTx( + tokenHolder, + tokenHolderNonce, + sessionPrivateKey1, + coGateway, + amount, beneficiary, gasPrice, gasLimit, redeemerNonce, hashLock, + ); + + await coGateway.makeRedemptionToFail(); + + await tokenHolder.executeRedemption( + coGateway.address, + coGatewayRedeemFunctionData, + tokenHolderNonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + from: accountProvider.get(), + value: 1, + }, + ); + + // Checks that nonce is updated. + assert.isOk( + (await tokenHolder.sessionKeys.call(sessionPublicKey1)).nonce.eqn(1), + ); + }); + }); +}); diff --git a/test/token_holder/execute_rule.js b/test/token_holder/execute_rule.js index a75b81f..85b92ef 100644 --- a/test/token_holder/execute_rule.js +++ b/test/token_holder/execute_rule.js @@ -1,4 +1,4 @@ -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,746 +12,970 @@ // See the License for the specific language governing permissions and // limitations under the License. -const BN = require('bn.js'); +'use strict'; + const EthUtils = require('ethereumjs-util'); const web3 = require('../test_lib/web3.js'); const Utils = require('../test_lib/utils.js'); +const { TokenHolderUtils } = require('./utils.js'); const { Event } = require('../test_lib/event_decoder'); const { AccountProvider } = require('../test_lib/utils.js'); -const TokenHolder = artifacts.require('TokenHolder'); -const MockRule = artifacts.require('MockRule'); -const TokenRulesMock = artifacts.require('TokenRulesMock'); -const EIP20TokenMock = artifacts.require('EIP20TokenMock'); +const TokenHolder = artifacts.require('./TokenHolder.sol'); + +const CustomRuleDouble = artifacts.require('CustomRuleDouble'); -const ephemeralPrivateKey1 = '0xa8225c01ceeaf01d7bc7c1b1b929037bd4050967c5730c0b854263121b8399f3'; -const ephemeralKeyAddress1 = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; +const sessionPublicKey1 = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; +const sessionPrivateKey1 = '0xa8225c01ceeaf01d7bc7c1b1b929037bd4050967c5730c0b854263121b8399f3'; +const sessionPublicKey2 = '0xBbfd1BF77dA692abc82357aC001415b98d123d17'; +const sessionPrivateKey2 = '0x6817f551bbc3e12b8fe36787ab192c921390d6176a3324ed02f96935a370bc41'; -const ephemeralPrivateKey2 = '0x634011a05b2f48e2d19aba49a9dbc12766bf7dbd6111ed2abb2621c92e8cfad9'; +function generateTokenHolderAuthorizeSessionFunctionData( + sessionKey, spendingLimit, expirationHeight, +) { + return web3.eth.abi.encodeFunctionCall( + { -function generateMockRulePassActionData(value) { - return web3.eth.abi.encodeFunctionCall( + name: 'authorizeSession', + type: 'function', + inputs: [ { - name: 'pass', - type: 'function', - inputs: [ - { - type: 'address', - name: 'value', - }, - ], + type: 'address', + name: 'sessionKey', }, - [value], - ); -} - -function generateMockRulePassPayableActionData(value) { - return web3.eth.abi.encodeFunctionCall( { - name: 'passPayable', - type: 'function', - inputs: [ - { - type: 'address', - name: 'value', - }, - ], + type: 'uint256', + name: 'spendingLimit', }, - [value], - ); -} - -function generateMockRuleFailActionData(value) { - return web3.eth.abi.encodeFunctionCall( { - name: 'fail', - type: 'function', - inputs: [ - { - type: 'address', - name: 'value', - }, - ], + type: 'uint256', + name: 'expirationHeight', }, - [value], - ); + ], + }, + [sessionKey, spendingLimit, expirationHeight], + ); } -function generateExecuteRuleCallPrefix() { - return web3.eth.abi.encodeFunctionSignature({ - name: 'executeRule', - type: 'function', - inputs: [ - { - type: 'address', name: '', - }, - { - type: 'bytes', name: '', - }, - { - type: 'uint256', name: '', - }, - { - type: 'uint8', name: '', - }, - { - type: 'bytes32', name: '', - }, - { - type: 'bytes32', name: '', - }, - ], - }); -} +function generateUtilityTokenApproveFunctionData(spender, value) { + return web3.eth.abi.encodeFunctionCall( + { -function getExecuteRuleExTxHash( - tokenHolderAddress, ruleAddress, ruleData, nonce, -) { - return web3.utils.soliditySha3( - { - t: 'bytes1', v: '0x19', - }, - { - t: 'bytes1', v: '0x0', - }, - { - t: 'address', v: tokenHolderAddress, - }, - { - t: 'address', v: ruleAddress, - }, - { - t: 'uint8', v: 0, - }, - { - t: 'bytes32', v: web3.utils.keccak256(ruleData), - }, + name: 'approve', + type: 'function', + inputs: [ { - t: 'uint256', v: nonce, + type: 'address', + name: 'spender', }, { - t: 'uint8', v: 0, - }, - { - t: 'uint8', v: 0, - }, - { - t: 'uint8', v: 0, + type: 'uint256', + name: 'value', }, + ], + }, + [spender, value], + ); +} + +function generateMockRulePassFunctionData(value) { + return web3.eth.abi.encodeFunctionCall( + { + name: 'pass', + type: 'function', + inputs: [ { - t: 'bytes4', v: generateExecuteRuleCallPrefix(), + type: 'address', + name: 'value', }, + ], + }, + [value], + ); +} + +function generateMockRulePassPayableFunctionData(value) { + return web3.eth.abi.encodeFunctionCall( + { + name: 'passPayable', + type: 'function', + inputs: [ { - t: 'uint8', v: 0, + type: 'address', + name: 'value', }, + ], + }, + [value], + ); +} + +function generateMockRuleFailFunctionData(value) { + return web3.eth.abi.encodeFunctionCall( + { + name: 'fail', + type: 'function', + inputs: [ { - t: 'bytes32', v: '0x0', + type: 'address', + name: 'value', }, - ); + ], + }, + [value], + ); +} + +function generateTokenHolderExecuteRuleCallPrefix() { + return web3.eth.abi.encodeFunctionSignature({ + name: 'executeRule', + type: 'function', + inputs: [ + { + type: 'address', name: '', + }, + { + type: 'bytes', name: '', + }, + { + type: 'uint256', name: '', + }, + { + type: 'bytes32', name: '', + }, + { + type: 'bytes32', name: '', + }, + { + type: 'uint8', name: '', + }, + ], + }); } -function getExecuteRuleExTxData( - _tokenHolderAddress, _ruleAddress, _ruleData, _nonce, _ephemeralKey, +async function generateTokenHolderAuthorizeSessionFunctionExTx( + tokenHolder, nonce, sessionPrivateKey, + newSessionPublicKey, newSessionSpendingLimit, newSessionExpirationHeight, ) { - const msgHash = getExecuteRuleExTxHash( - _tokenHolderAddress, _ruleAddress, _ruleData, _nonce, - ); + const tokenHolderAuthorizeSessionFunctionData = generateTokenHolderAuthorizeSessionFunctionData( + newSessionPublicKey, newSessionSpendingLimit, newSessionExpirationHeight, + ); + + const { exTxHash, exTxSignature } = Utils.generateExTx( + tokenHolder.address, + tokenHolder.address, + tokenHolderAuthorizeSessionFunctionData, + nonce, + generateTokenHolderExecuteRuleCallPrefix(), + sessionPrivateKey, + ); + + return { + tokenHolderAuthorizeSessionFunctionData, + exTxHash, + exTxSignature, + }; +} - const rsv = EthUtils.ecsign( - EthUtils.toBuffer(msgHash), - EthUtils.toBuffer(_ephemeralKey), - ); +async function generateUtilityTokenApproveFunctionExTx( + tokenHolder, nonce, sessionPrivateKey, + spender, amount, +) { + const utilityTokenApproveFunctionData = generateUtilityTokenApproveFunctionData( + spender, amount, + ); + + const tokenAddress = await tokenHolder.token(); + + const { exTxHash, exTxSignature } = Utils.generateExTx( + tokenHolder.address, + tokenAddress, + utilityTokenApproveFunctionData, + nonce, + generateTokenHolderExecuteRuleCallPrefix(), + sessionPrivateKey, + ); + + return { + utilityTokenApproveFunctionData, + exTxHash, + exTxSignature, + }; +} - return { msgHash, rsv }; +async function generateMockRulePassFunctionExTx( + tokenHolder, nonce, sessionKey, + mockRule, mockRuleValue, +) { + const mockRulePassFunctionData = generateMockRulePassFunctionData( + mockRuleValue, + ); + + const { exTxHash, exTxSignature } = Utils.generateExTx( + tokenHolder.address, + mockRule.address, + mockRulePassFunctionData, + nonce, + generateTokenHolderExecuteRuleCallPrefix(), + sessionKey, + ); + + return { + mockRulePassFunctionData, + exTxHash, + exTxSignature, + }; } -async function createTokenHolder( - accountProvider, _ephemeralKeyAddress, _spendingLimit, _deltaExpirationHeight, +async function generateMockRulePassPayableFunctionExTx( + tokenHolder, nonce, sessionKey, + mockRule, mockRuleValue, ) { - const token = await EIP20TokenMock.new(1, 1, 'OST', 'Open Simple Token', 1); - const tokenRules = await TokenRulesMock.new(); - const required = 1; - const registeredWallet0 = accountProvider.get(); - const wallets = [registeredWallet0]; - - const tokenHolder = await TokenHolder.new( - token.address, - tokenRules.address, - wallets, - required, - ); - - const blockNumber = await web3.eth.getBlockNumber(); - - await tokenHolder.submitAuthorizeSession( - _ephemeralKeyAddress, - _spendingLimit, - blockNumber + _deltaExpirationHeight, - { from: registeredWallet0 }, - ); - - return { - tokenHolder, - registeredWallet0, - }; + const mockRulePassPayableFunctionData = generateMockRulePassPayableFunctionData( + mockRuleValue, + ); + + const { exTxHash, exTxSignature } = Utils.generateExTx( + tokenHolder.address, + mockRule.address, + mockRulePassPayableFunctionData, + nonce, + generateTokenHolderExecuteRuleCallPrefix(), + sessionKey, + ); + + return { + mockRulePassPayableFunctionData, + exTxHash, + exTxSignature, + }; +} + +async function generateMockRuleFailFunctionExTx( + tokenHolder, nonce, sessionKey, + mockRule, mockRuleValue, +) { + const mockRuleFailFunctionData = generateMockRuleFailFunctionData( + mockRuleValue, + ); + + const { exTxHash, exTxSignature } = Utils.generateExTx( + tokenHolder.address, + mockRule.address, + mockRuleFailFunctionData, + nonce, + generateTokenHolderExecuteRuleCallPrefix(), + sessionKey, + ); + + return { + mockRuleFailFunctionData, + exTxHash, + exTxSignature, + }; } -async function preparePassRule( - accountProvider, tokenHolder, nonce, ephemeralKey, +async function prepare( + accountProvider, + spendingLimit, deltaExpirationHeight, + sessionPublicKeyToAuthorize, ) { - const mockRule = await MockRule.new(); - const mockRuleValue = accountProvider.get(); - const mockRulePassActionData = generateMockRulePassActionData( + const { utilityToken } = await TokenHolderUtils.createUtilityMockToken(); + + const { tokenRules } = await TokenHolderUtils.createMockTokenRules(); + + const { + tokenHolderOwnerAddress, + tokenHolder, + } = await TokenHolderUtils.createTokenHolder( + accountProvider, + utilityToken, tokenRules, + spendingLimit, deltaExpirationHeight, + sessionPublicKeyToAuthorize, + ); + + await TokenHolderUtils.authorizeSessionKey( + tokenHolder, tokenHolderOwnerAddress, + sessionPublicKeyToAuthorize, spendingLimit, deltaExpirationHeight, + ); + + return { + utilityToken, + tokenRules, + tokenHolderOwnerAddress, + tokenHolder, + }; +} + +contract('TokenHolder::executeRule', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if ExTx is signed with non-authorized key.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRulePassFunctionData, + exTxSignature, + } = await generateMockRulePassFunctionExTx( + tokenHolder, nonce, sessionPrivateKey2, + mockRule, mockRuleValue, - ); + ); + + await Utils.expectRevert( + tokenHolder.executeRule( + mockRule.address, + mockRulePassFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ), + 'Should revert as ExTx is signed with non-authorized key.', + 'Key\'s session is not equal to contract\'s session window.', + ); + }); - const { msgHash, rsv } = getExecuteRuleExTxData( - tokenHolder.address, - mockRule.address, - mockRulePassActionData, - nonce, - ephemeralKey, - ); + it('Reverts if ExTx is signed with authorized but expired key.', async () => { + const deltaExpirationHeight = 10; + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + deltaExpirationHeight, + sessionPublicKey1, + ); + + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRulePassFunctionData, + exTxSignature, + } = await generateMockRulePassFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + mockRule, + mockRuleValue, + ); + + for (let i = 0; i < deltaExpirationHeight; i += 1) { + // eslint-disable-next-line no-await-in-loop + await Utils.advanceBlock(); + } + + await Utils.expectRevert( + tokenHolder.executeRule( + mockRule.address, + mockRulePassFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ), + 'Should revert as transaction is signed with expired key.', + 'Session key was expired.', + ); + }); - return { + it('Reverts if ExTx is signed with revoked key.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + await tokenHolder.revokeSession( + sessionPublicKey1, + { from: tokenHolderOwnerAddress }, + ); + + const keyData = await tokenHolder.sessionKeys( + sessionPublicKey1, + ); + + assert.isOk( + // AuthorizationStatus.REVOKED == 1 + keyData.session.eqn(1), + ); + + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRulePassFunctionData, + exTxSignature, + } = await generateMockRulePassFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, mockRule, mockRuleValue, - mockRulePassActionData, - msgHash, - rsv, - }; -} + ); + + await Utils.expectRevert( + tokenHolder.executeRule( + mockRule.address, + mockRulePassFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ), + 'Should revert as transaction is signed with revoked key.', + 'Key\'s session is not equal to contract\'s session window.', + ); + }); -async function preparePassPayableRule( - accountProvider, tokenHolder, nonce, ephemeralKey, -) { - const mockRule = await MockRule.new(); - const mockRuleValue = accountProvider.get(); - const mockRulePassActionData = generateMockRulePassPayableActionData( + it('Reverts if ExTx is signed with logged out key.', async () => { + const { + tokenHolderOwnerAddress, + tokenHolder, + } = await prepare( + accountProvider, + 10, // spendingLimit + 10, // deltaExpirationHeight, + sessionPublicKey1, + ); + + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRulePassFunctionData, + exTxSignature, + } = await generateMockRulePassFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + mockRule, mockRuleValue, - ); + ); + + await tokenHolder.logout( + { from: tokenHolderOwnerAddress }, + ); + + await Utils.expectRevert( + tokenHolder.executeRule( + mockRule.address, + mockRulePassFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ), + 'Should revert as ExTx is signed with logged-out key.', + 'Key\'s session is not equal to contract\'s session window.', + ); + }); + + it('Reverts if "to" address is the utility token address.', async () => { + const { + utilityToken, + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const nonce = 0; + + const { + utilityTokenApproveFunctionData, + exTxSignature, + } = await generateUtilityTokenApproveFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + accountProvider.get(), /* spender */ + 1, /* amount */ + ); + + await Utils.expectRevert( + tokenHolder.executeRule( + utilityToken.address, + utilityTokenApproveFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ), + 'Should revert if "to" address is utility token address.', + '\'to\' address is utility token address.', + ); + }); + + it('Reverts if ExTx is signed with a wrong nonce.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + // Correct nonce is 0. + const invalidNonce = 1; + + const mockRule0 = await CustomRuleDouble.new(); + const mockRuleValue0 = accountProvider.get(); + + const { + mockRulePassFunctionData: mockRulePassFunctionData0, + exTxSignature: exTxSignature0, + } = await generateMockRulePassFunctionExTx( + tokenHolder, invalidNonce, sessionPrivateKey1, + mockRule0, + mockRuleValue0, + ); + + await Utils.expectRevert( + tokenHolder.executeRule( + mockRule0.address, + mockRulePassFunctionData0, + invalidNonce, + EthUtils.bufferToHex(exTxSignature0.r), + EthUtils.bufferToHex(exTxSignature0.s), + exTxSignature0.v, + ), + 'Should revert as ExTx is signed with a wrong nonce.', + 'Incorrect nonce is specified.', + ); + }); + + it('Reverts if "to" is TokenHolder address itself', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + const nonce = 0; + + const { + tokenHolderAuthorizeSessionFunctionData, + exTxSignature, + } = await generateTokenHolderAuthorizeSessionFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + sessionPublicKey2, /* new session key. */ + 20, /* spending limit for new session. */ + 200, /* expiration height for new session. */ + ); + + await Utils.expectRevert( + tokenHolder.executeRule( + tokenHolder.address, + tokenHolderAuthorizeSessionFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ), + 'Should revert if "to" address is TokenHolder address itself.', + '\'to\' address is TokenHolder address itself.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits RuleExecuted event with successful execution status.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + // correct nonce is 0. + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRulePassFunctionData, + exTxHash, + exTxSignature, + } = await generateMockRulePassFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + mockRule, mockRuleValue, + ); + + const transactionResponse = await tokenHolder.executeRule( + mockRule.address, + mockRulePassFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'RuleExecuted', + args: { + _messageHash: exTxHash, + _status: true, + }, + }); + }); - const { msgHash, rsv } = getExecuteRuleExTxData( - tokenHolder.address, + it('Emits RuleExecuted event with failed execution status.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + // correct nonce is 0. + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRuleFailFunctionData, + exTxHash, + exTxSignature, + } = await generateMockRuleFailFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + mockRule, mockRuleValue, + ); + + const transactionResponse = await tokenHolder.executeRule( mockRule.address, - mockRulePassActionData, + mockRuleFailFunctionData, nonce, - ephemeralKey, - ); + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'RuleExecuted', + args: { + _messageHash: exTxHash, + _status: false, + }, + }); + }); + }); - return { - mockRule, - mockRuleValue, - mockRulePassActionData, - msgHash, - rsv, - }; -} + contract('Rule Executed', async (accounts) => { + const accountProvider = new AccountProvider(accounts); -async function prepareFailRule( - accountProvider, tokenHolder, nonce, ephemeralKey, -) { - const mockRule = await MockRule.new(); - const mockRuleValue = accountProvider.get(); - const mockRuleFailActionData = generateMockRuleFailActionData( + it('Checks that rule is actually executed.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + // correct nonce is 0. + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRulePassFunctionData, + exTxSignature, + } = await generateMockRulePassFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + mockRule, mockRuleValue, + ); + + await tokenHolder.executeRule( + mockRule.address, + mockRulePassFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ); + + assert.strictEqual( + (await mockRule.recordedValue.call()), mockRuleValue, - ); + ); + }); - const { msgHash, rsv } = getExecuteRuleExTxData( - tokenHolder.address, + it('Checks that payable rule is actually executed.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + // correct nonce is 0. + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRulePassPayableFunctionData, + exTxSignature, + } = await generateMockRulePassPayableFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + mockRule, mockRuleValue, + ); + + const payableValue = 111; + await tokenHolder.executeRule( mockRule.address, - mockRuleFailActionData, + mockRulePassPayableFunctionData, nonce, - ephemeralKey, - ); + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + { + value: payableValue, + }, + ); - return { - mockRule, + assert.strictEqual( + (await mockRule.recordedValue.call()), mockRuleValue, - mockRuleFailActionData, - msgHash, - rsv, - }; -} + ); -contract('TokenHolder::executeRule', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if ExTx is signed with non-authorized key.', async () => { - const spendingLimit = 10; - const deltaExpirationHeight = 50; - - const { tokenHolder } = await createTokenHolder( - accountProvider, - ephemeralKeyAddress1, - spendingLimit, - deltaExpirationHeight, - ); - - const nonce = 1; - const { - mockRule, - mockRulePassActionData, - rsv, - } = await preparePassRule( - accountProvider, - tokenHolder, - nonce, - ephemeralPrivateKey2, - ); - - await Utils.expectRevert( - tokenHolder.executeRule( - mockRule.address, - mockRulePassActionData, - nonce, - rsv.v, - EthUtils.bufferToHex(rsv.r), - EthUtils.bufferToHex(rsv.s), - ), - 'Should revert as ExTx is signed with non-authorized key.', - 'Ephemeral key is not active.', - ); - }); - - it('Reverts if ExTx is signed with authorized but expired key.', async () => { - const spendingLimit = 10; - const deltaExpirationHeight = 10; - const { tokenHolder } = await createTokenHolder( - accountProvider, - ephemeralKeyAddress1, - spendingLimit, - deltaExpirationHeight, - ); - - const nonce = 1; - const { - mockRule, - mockRulePassActionData, - rsv, - } = await preparePassRule( - accountProvider, - tokenHolder, - nonce, - ephemeralPrivateKey2, - ); - - for (let i = 0; i < deltaExpirationHeight; i += 1) { - // eslint-disable-next-line no-await-in-loop - await Utils.advanceBlock(); - } - - await Utils.expectRevert( - tokenHolder.executeRule( - mockRule.address, - mockRulePassActionData, - nonce, - rsv.v, - EthUtils.bufferToHex(rsv.r), - EthUtils.bufferToHex(rsv.s), - ), - 'Should revert as transaction is signed with expired key.', - 'Ephemeral key is not active.', - ); - }); - - it('Reverts if transaction signed with revoked key.', async () => { - const spendingLimit = 10; - const deltaExpirationHeight = 50; - const { - tokenHolder, - registeredWallet0, - } = await createTokenHolder( - accountProvider, - ephemeralKeyAddress1, - spendingLimit, - deltaExpirationHeight, - ); - - await tokenHolder.revokeSession( - ephemeralKeyAddress1, - { from: registeredWallet0 }, - ); - - const keyData = await tokenHolder.ephemeralKeys( - ephemeralKeyAddress1, - ); - - assert.isOk( - // AuthorizationStatus.REVOKED == 2 - keyData.status.eqn(2), - 'Because of 1-wallet-1-required setup key should be revoked.', - ); - - const nonce = 1; - const { - mockRule, - mockRulePassActionData, - rsv, - } = await preparePassRule( - accountProvider, - tokenHolder, - nonce, - ephemeralPrivateKey1, - ); - - await Utils.expectRevert( - tokenHolder.executeRule( - mockRule.address, - mockRulePassActionData, - nonce, - rsv.v, - EthUtils.bufferToHex(rsv.r), - EthUtils.bufferToHex(rsv.s), - ), - 'Should revert as transaction is signed with revoked key.', - 'Ephemeral key is not active.', - ); - }); - - it('Reverts if ExTx is signed with a wrong nonce.', async () => { - const spendingLimit = 10; - const deltaExpirationHeight = 50; - const { tokenHolder } = await createTokenHolder( - accountProvider, - ephemeralKeyAddress1, - spendingLimit, - deltaExpirationHeight, - ); - - // Correct nonce is 1. - const invalidNonce0 = 0; - const { - mockRule: mockRule0, - mockRulePassActionData: mockRulePassActionData0, - rsv: rsv0, - } = await preparePassRule( - accountProvider, - tokenHolder, - invalidNonce0, - ephemeralPrivateKey1, - ); - - await Utils.expectRevert( - tokenHolder.executeRule( - mockRule0.address, - mockRulePassActionData0, - invalidNonce0, - rsv0.v, - EthUtils.bufferToHex(rsv0.r), - EthUtils.bufferToHex(rsv0.s), - ), - 'Should revert as ExTx is signed with a wrong nonce.', - 'The next nonce is not provided.', - ); - - // correct nonce is 1. - const invalidNonce2 = 2; - const { - mockRule: mockRule2, - mockRulePassActionData: mockRulePassActionData2, - rsv: rsv2, - } = await preparePassRule( - accountProvider, - tokenHolder, - invalidNonce2, - ephemeralPrivateKey1, - ); - - await Utils.expectRevert( - tokenHolder.executeRule( - mockRule2.address, - mockRulePassActionData2, - invalidNonce2, - rsv2.v, - EthUtils.bufferToHex(rsv2.r), - EthUtils.bufferToHex(rsv2.s), - ), - 'Should revert as ExTx is signed with a wrong nonce.', - 'The next nonce is not provided.', - ); - }); + assert.isOk( + (await mockRule.recordedPayedAmount.call()).eqn(payableValue), + ); }); + }); + + contract('Returned Execution Status', async (accounts) => { + const accountProvider = new AccountProvider(accounts); - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits RuleExecuted event with successful execution status.', async () => { - const spendingLimit = 10; - const deltaExpirationHeight = 50; - const { tokenHolder } = await createTokenHolder( - accountProvider, - ephemeralKeyAddress1, - spendingLimit, - deltaExpirationHeight, - ); - - const nonce = 1; - const { - mockRule, - mockRulePassActionData, - msgHash, - rsv, - } = await preparePassRule( - accountProvider, - tokenHolder, - nonce, - ephemeralPrivateKey1, - ); - - const transactionResponse = await tokenHolder.executeRule( - mockRule.address, - mockRulePassActionData, - nonce, - rsv.v, - EthUtils.bufferToHex(rsv.r), - EthUtils.bufferToHex(rsv.s), - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 1, - ); - - const passCallPrefix = await mockRule.PASS_CALLPREFIX.call(); - Event.assertEqual(events[0], { - name: 'RuleExecuted', - args: { - _to: mockRule.address, - _functionSelector: passCallPrefix, - _ephemeralKey: ephemeralKeyAddress1, - _nonce: new BN(nonce), - _messageHash: msgHash, - _status: true, - }, - }); - }); - - it('Emits RuleExecuted event with failed execution status.', async () => { - const spendingLimit = 10; - const deltaExpirationHeight = 50; - const { tokenHolder } = await createTokenHolder( - accountProvider, - ephemeralKeyAddress1, - spendingLimit, - deltaExpirationHeight, - ); - - const nonce = 1; - const { - mockRule, - mockRuleFailActionData, - msgHash, - rsv, - } = await prepareFailRule( - accountProvider, - tokenHolder, - nonce, - ephemeralPrivateKey1, - ); - - const transactionResponse = await tokenHolder.executeRule( - mockRule.address, - mockRuleFailActionData, - nonce, - rsv.v, - EthUtils.bufferToHex(rsv.r), - EthUtils.bufferToHex(rsv.s), - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 1, - ); - - Event.assertEqual(events[0], { - name: 'RuleExecuted', - args: { - _to: mockRule.address, - _functionSelector: await mockRule.FAIL_CALLPREFIX.call(), - _ephemeralKey: ephemeralKeyAddress1, - _nonce: new BN(nonce), - _messageHash: msgHash, - // We should check against false here, however - // current version of web3 returns null for false values - // in event log. After updating web3, this test might - // fail and we should use false (as intended). - _status: null, - }, - }); - }); + it('Checks that return value is true in case of successfull execution.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + // correct nonce is 0. + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRulePassFunctionData, + exTxSignature, + } = await generateMockRulePassFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + mockRule, mockRuleValue, + ); + + const executionStatus = await tokenHolder.executeRule.call( + mockRule.address, + mockRulePassFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ); + + assert.isOk( + executionStatus, + ); }); - contract('Rule Executed', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks that rule is actually executed.', async () => { - const spendingLimit = 10; - const deltaExpirationHeight = 50; - const { tokenHolder } = await createTokenHolder( - accountProvider, - ephemeralKeyAddress1, - spendingLimit, - deltaExpirationHeight, - ); - - const nonce = 1; - const { - mockRule, - mockRuleValue, - mockRulePassActionData, - rsv, - } = await preparePassRule( - accountProvider, - tokenHolder, - nonce, - ephemeralPrivateKey1, - ); - - await tokenHolder.executeRule( - mockRule.address, - mockRulePassActionData, - nonce, - rsv.v, - EthUtils.bufferToHex(rsv.r), - EthUtils.bufferToHex(rsv.s), - ); - - assert.strictEqual( - (await mockRule.value.call()), - mockRuleValue, - ); - }); - - it('Checks that payable rule is actually executed.', async () => { - const spendingLimit = 10; - const deltaExpirationHeight = 50; - const { tokenHolder } = await createTokenHolder( - accountProvider, - ephemeralKeyAddress1, - spendingLimit, - deltaExpirationHeight, - ); - - const nonce = 1; - const { - mockRule, - mockRuleValue, - mockRulePassActionData, - rsv, - } = await preparePassPayableRule( - accountProvider, - tokenHolder, - nonce, - ephemeralPrivateKey1, - ); - - const payableValue = 111; - await tokenHolder.executeRule( - mockRule.address, - mockRulePassActionData, - nonce, - rsv.v, - EthUtils.bufferToHex(rsv.r), - EthUtils.bufferToHex(rsv.s), - { - value: payableValue, - }, - ); - - assert.strictEqual( - (await mockRule.value.call()), - mockRuleValue, - ); - - assert.isOk( - (await mockRule.receivedPayableAmount.call()).eqn(payableValue), - ); - }); + it('Checks that return value is false in case of failing execution.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + // correct nonce is 0. + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRuleFailFunctionData, + exTxSignature, + } = await generateMockRuleFailFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + mockRule, mockRuleValue, + ); + + const executionStatus = await tokenHolder.executeRule.call( + mockRule.address, + mockRuleFailFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ); + + assert.isNotOk( + executionStatus, + ); }); + }); + + contract('Nonce handling', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that nonce is incremented in case of successfull execution.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + // correct nonce is 0. + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRulePassFunctionData, + exTxSignature, + } = await generateMockRulePassFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + mockRule, mockRuleValue, + ); + + await tokenHolder.executeRule( + mockRule.address, + mockRulePassFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ); + + // Checks that nonce is updated. + assert.isOk( + (await tokenHolder.sessionKeys.call(sessionPublicKey1)).nonce.eqn(1), + ); + }); + + it('Checks that nonce is incremented in case of failing execution.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10, /* spendingLimit */ + 50, /* deltaExpirationHeight */ + sessionPublicKey1, + ); + + // correct nonce is 0. + const nonce = 0; + + const mockRule = await CustomRuleDouble.new(); + const mockRuleValue = accountProvider.get(); + + const { + mockRuleFailFunctionData, + exTxSignature, + } = await generateMockRuleFailFunctionExTx( + tokenHolder, nonce, sessionPrivateKey1, + mockRule, mockRuleValue, + ); + + await tokenHolder.executeRule( + mockRule.address, + mockRuleFailFunctionData, + nonce, + EthUtils.bufferToHex(exTxSignature.r), + EthUtils.bufferToHex(exTxSignature.s), + exTxSignature.v, + ); + + // Checks that nonce is updated. + assert.isOk( + (await tokenHolder.sessionKeys.call(sessionPublicKey1)).nonce.eqn(1), + ); + }); + }); + + contract('Verify call prefix constants', async () => { + it('Verify EXECUTE_RULE_CALLPREFIX constant', async () => { + const tokenHolder = await TokenHolder.new(); + const tokenHolderExecuteRuleCallPrefix = await tokenHolder.EXECUTE_RULE_CALLPREFIX(); + const methodName = 'executeRule'; + + Utils.verifyCallPrefixConstant(methodName, tokenHolderExecuteRuleCallPrefix, 'TokenHolder'); + }); + + it('Verify EXECUTE_REDEMPTION_CALLPREFIX constant', async () => { + const tokenHolder = await TokenHolder.new(); + const tokenHolderExecuteRuleCallPrefix = await tokenHolder.EXECUTE_REDEMPTION_CALLPREFIX(); + const methodName = 'executeRedemption'; - contract('Returned Execution Status', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks that return value is true in case of successfull execution.', async () => { - const spendingLimit = 10; - const deltaExpirationHeight = 50; - const { tokenHolder } = await createTokenHolder( - accountProvider, - ephemeralKeyAddress1, - spendingLimit, - deltaExpirationHeight, - ); - - const nonce = 1; - const { - mockRule, - mockRulePassActionData, - rsv, - } = await preparePassRule( - accountProvider, - tokenHolder, - nonce, - ephemeralPrivateKey1, - ); - - const executionStatus = await tokenHolder.executeRule.call( - mockRule.address, - mockRulePassActionData, - nonce, - rsv.v, - EthUtils.bufferToHex(rsv.r), - EthUtils.bufferToHex(rsv.s), - ); - - assert.isOk( - executionStatus, - ); - }); - - it('Checks that return value is true in case of failing execution.', async () => { - const spendingLimit = 10; - const deltaExpirationHeight = 50; - const { tokenHolder } = await createTokenHolder( - accountProvider, - ephemeralKeyAddress1, - spendingLimit, - deltaExpirationHeight, - ); - - const nonce = 1; - const { - mockRule, - mockRuleFailActionData, - rsv, - } = await prepareFailRule( - accountProvider, - tokenHolder, - nonce, - ephemeralPrivateKey1, - ); - - const executionStatus = await tokenHolder.executeRule.call( - mockRule.address, - mockRuleFailActionData, - nonce, - rsv.v, - EthUtils.bufferToHex(rsv.r), - EthUtils.bufferToHex(rsv.s), - ); - - assert.isNotOk( - executionStatus, - ); - }); + Utils.verifyCallPrefixConstant(methodName, tokenHolderExecuteRuleCallPrefix, 'TokenHolder'); }); + }); }); diff --git a/test/token_holder/logout.js b/test/token_holder/logout.js new file mode 100644 index 0000000..0a89ff1 --- /dev/null +++ b/test/token_holder/logout.js @@ -0,0 +1,161 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const utils = require('../test_lib/utils.js'); +const { TokenHolderUtils } = require('./utils.js'); +const { Event } = require('../test_lib/event_decoder'); +const { AccountProvider } = require('../test_lib/utils.js'); + +async function prepare( + accountProvider, + spendingLimit, deltaExpirationHeight, +) { + const { utilityToken } = await TokenHolderUtils.createUtilityMockToken(); + + const { tokenRules } = await TokenHolderUtils.createMockTokenRules(); + + const authorizedSessionPublicKey = accountProvider.get(); + + const { + tokenHolderOwnerAddress, + tokenHolder, + } = await TokenHolderUtils.createTokenHolder( + accountProvider, + utilityToken, tokenRules, + spendingLimit, deltaExpirationHeight, + authorizedSessionPublicKey, + ); + + await TokenHolderUtils.authorizeSessionKey( + tokenHolder, tokenHolderOwnerAddress, + authorizedSessionPublicKey, spendingLimit, deltaExpirationHeight, + ); + + return { + utilityToken, + tokenRules, + tokenHolderOwnerAddress, + tokenHolder, + authorizedSessionPublicKey, + }; +} + +contract('TokenHolder::logout', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if is not called by owner.', async () => { + const { + tokenHolder, + } = await prepare( + accountProvider, + 10 /* spendingLimit */, + 50 /* deltaExpirationHeight */, + ); + + await utils.expectRevert( + tokenHolder.logout( + { from: accountProvider.get() }, + ), + 'Should revert as caller is not an owner.', + 'Only owner is allowed to call.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits SessionRevoked event.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 10 /* spendingLimit */, + 50 /* deltaExpirationHeight */, + ); + + let transactionResponse = await tokenHolder.logout( + { from: tokenHolderOwnerAddress }, + ); + + let events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'SessionsLoggedOut', + args: { + _sessionWindow: new BN(2), + }, + }); + + transactionResponse = await tokenHolder.logout( + { from: tokenHolderOwnerAddress }, + ); + + events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'SessionsLoggedOut', + args: { + _sessionWindow: new BN(3), + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that logout request is handled correctly.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 10 /* spendingLimit */, + 50 /* deltaExpirationHeight */, + ); + + assert.isOk( + (await tokenHolder.sessionWindow.call()).eqn(2), + ); + + await tokenHolder.logout( + { from: tokenHolderOwnerAddress }, + ); + + assert.isOk( + (await tokenHolder.sessionWindow.call()).eqn(3), + ); + }); + }); +}); diff --git a/test/token_holder/revoke_session.js b/test/token_holder/revoke_session.js index 71b8d24..c92ad81 100644 --- a/test/token_holder/revoke_session.js +++ b/test/token_holder/revoke_session.js @@ -1,4 +1,4 @@ -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,190 +12,192 @@ // See the License for the specific language governing permissions and // limitations under the License. -const web3 = require('../test_lib/web3.js'); +'use strict'; + const utils = require('../test_lib/utils.js'); +const { TokenHolderUtils } = require('./utils.js'); const { Event } = require('../test_lib/event_decoder'); const { AccountProvider } = require('../test_lib/utils.js'); -const TokenHolder = artifacts.require('TokenHolder'); +const sessionPublicKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; -async function createTokenHolder( - accountProvider, +async function prepare( + accountProvider, + spendingLimit, deltaExpirationHeight, + sessionPublicKeyToAuthorize, ) { - const required = 1; - - const registeredWallet0 = accountProvider.get(); + const { utilityToken } = await TokenHolderUtils.createUtilityMockToken(); - const wallets = [registeredWallet0]; + const { tokenRules } = await TokenHolderUtils.createMockTokenRules(); - const tokenAddress = accountProvider.get(); - const tokenRulesAddress = accountProvider.get(); - - const tokenHolder = await TokenHolder.new( - tokenAddress, tokenRulesAddress, wallets, required, - ); - - return { - tokenHolder, - registeredWallet0, - }; + const { + tokenHolderOwnerAddress, + tokenHolder, + } = await TokenHolderUtils.createTokenHolder( + accountProvider, + utilityToken, tokenRules, + spendingLimit, deltaExpirationHeight, + sessionPublicKeyToAuthorize, + ); + + await TokenHolderUtils.authorizeSessionKey( + tokenHolder, tokenHolderOwnerAddress, + sessionPublicKeyToAuthorize, spendingLimit, deltaExpirationHeight, + ); + + return { + utilityToken, + tokenRules, + tokenHolderOwnerAddress, + tokenHolder, + }; } -async function prepareTokenHolder( - accountProvider, ephemeralKey, -) { - const { - tokenHolder, - registeredWallet0, - } = await createTokenHolder(accountProvider); - - const spendingLimit = 1; - const expirationHeight = (await web3.eth.getBlockNumber()) + 10; - - await tokenHolder.submitAuthorizeSession( - ephemeralKey, - spendingLimit, - expirationHeight, - { - from: registeredWallet0, - }, - ); +contract('TokenHolder::revokeSession', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); - return { + it('Reverts if non-owner address calls.', async () => { + const { tokenHolder, - registeredWallet0, - }; -} - -contract('TokenHolder::revokeSession', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-registered wallet calls.', async () => { - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - - const { - tokenHolder, - } = await prepareTokenHolder(accountProvider, ephemeralKey); - - await utils.expectRevert( - tokenHolder.revokeSession( - ephemeralKey, - { - from: accountProvider.get(), - }, - ), - 'Should revert as non-registered wallet calls.', - 'Only wallet is allowed to call.', - ); - }); - - it('Reverts if key to revoke does not exist.', async () => { - const { - tokenHolder, - registeredWallet0, - } = await createTokenHolder(accountProvider); - - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - - await utils.expectRevert( - tokenHolder.revokeSession( - ephemeralKey, - { from: registeredWallet0 }, - ), - 'Should revert as key to revoke does not exist.', - 'Key is not authorized.', - ); - }); - - it('Reverts if key to revoke is already revoked.', async () => { - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - - const { - tokenHolder, - registeredWallet0, - } = await prepareTokenHolder(accountProvider, ephemeralKey); - - await tokenHolder.revokeSession( - ephemeralKey, - { from: registeredWallet0 }, - ); - - await utils.expectRevert( - tokenHolder.revokeSession( - ephemeralKey, - { from: registeredWallet0 }, - ), - 'Should revert as key to revoke was already revoked.', - 'Key is not authorized.', - ); - }); + } = await prepare( + accountProvider, + 10 /* spendingLimit */, + 50 /* deltaExpirationHeight */, + sessionPublicKey, + ); + + await utils.expectRevert( + tokenHolder.revokeSession( + sessionPublicKey, + { + from: accountProvider.get(), + }, + ), + 'Should revert as non-owner address calls.', + 'Only owner is allowed to call.', + ); }); - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits SessionRevoked event.', async () => { - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - - const { - tokenHolder, - registeredWallet0, - } = await prepareTokenHolder(accountProvider, ephemeralKey); - - const transactionResponse = await tokenHolder.revokeSession( - ephemeralKey, - { from: registeredWallet0 }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 1, - ); - - // The only emitted event should be 'SessionRevoked'. - Event.assertEqual(events[0], { - name: 'SessionRevoked', - args: { - _ephemeralKey: ephemeralKey, - }, - }); - }); + it('Reverts if key to revoke does not exist.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 10 /* spendingLimit */, + 50 /* deltaExpirationHeight */, + sessionPublicKey, + ); + + const nonAuthorizedSessionKey = '0xADdB68e734D215D1fBFc44bBcaE42fAc2047DDec'; + + await utils.expectRevert( + tokenHolder.revokeSession( + nonAuthorizedSessionKey, + { from: tokenHolderOwnerAddress }, + ), + 'Should revert as key to revoke does not exist.', + 'Key is not authorized.', + ); }); - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks that key is revoked after successfull revocation.', async () => { - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - - const { - tokenHolder, - registeredWallet0, - } = await prepareTokenHolder(accountProvider, ephemeralKey); - - let keyData = await tokenHolder.ephemeralKeys.call(ephemeralKey); + it('Reverts if key to revoke is already revoked.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 10 /* spendingLimit */, + 50 /* deltaExpirationHeight */, + sessionPublicKey, + ); + + await tokenHolder.revokeSession( + sessionPublicKey, + { from: tokenHolderOwnerAddress }, + ); + + await utils.expectRevert( + tokenHolder.revokeSession( + sessionPublicKey, + { from: tokenHolderOwnerAddress }, + ), + 'Should revert as key to revoke was already revoked.', + 'Key is not authorized.', + ); + }); + }); - assert.isOk( - // TokenHolder.AuthorizationStatus.AUTHORIZED == 1 - keyData.status.eqn(1), - ); + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); - await tokenHolder.revokeSession( - ephemeralKey, - { from: registeredWallet0 }, - ); + it('Emits SessionRevoked event.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 10 /* spendingLimit */, + 50 /* deltaExpirationHeight */, + sessionPublicKey, + ); + + const transactionResponse = await tokenHolder.revokeSession( + sessionPublicKey, + { from: tokenHolderOwnerAddress }, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + // The only emitted event should be 'SessionRevoked'. + Event.assertEqual(events[0], { + name: 'SessionRevoked', + args: { + _sessionKey: sessionPublicKey, + }, + }); + }); + }); - keyData = await tokenHolder.ephemeralKeys.call(ephemeralKey); + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); - assert.isOk( - // TokenHolder.AuthorizationStatus.REVOKED == 2 - keyData.status.eqn(2), - ); - }); + it('Checks that key is revoked after successfull revocation.', async () => { + const { + tokenHolder, + tokenHolderOwnerAddress, + } = await prepare( + accountProvider, + 10 /* spendingLimit */, + 50 /* deltaExpirationHeight */, + sessionPublicKey, + ); + + let keyData = await tokenHolder.sessionKeys.call(sessionPublicKey); + + assert.isOk( + keyData.session.eqn(2), + ); + + await tokenHolder.revokeSession( + sessionPublicKey, + { from: tokenHolderOwnerAddress }, + ); + + keyData = await tokenHolder.sessionKeys.call(sessionPublicKey); + + assert.isOk( + // TokenHolder.AuthorizationStatus.REVOKED == 1 + keyData.session.eqn(1), + ); }); + }); }); diff --git a/test/token_holder/setup.js b/test/token_holder/setup.js new file mode 100644 index 0000000..1a847ca --- /dev/null +++ b/test/token_holder/setup.js @@ -0,0 +1,354 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const utils = require('../test_lib/utils.js'); +const web3 = require('../test_lib/web3.js'); +const { Event } = require('../test_lib/event_decoder'); +const { AccountProvider } = require('../test_lib/utils.js'); + +const TokenHolder = artifacts.require('TokenHolder'); + +contract('TokenHolder::setup', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if token address is null.', async () => { + const tokenHolder = await TokenHolder.new(); + + const ownerAddress = accountProvider.get(); + const tokenAddress = utils.NULL_ADDRESS; + const tokenRulesAddress = accountProvider.get(); + + await utils.expectRevert( + tokenHolder.setup( + tokenAddress, + tokenRulesAddress, + ownerAddress, + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ), + 'Should revert as token address is null.', + 'Token contract address is null.', + ); + }); + + it('Reverts if token rules is null.', async () => { + const tokenHolder = await TokenHolder.new(); + + const ownerAddress = accountProvider.get(); + const tokenAddress = accountProvider.get(); + const tokenRulesAddress = utils.NULL_ADDRESS; + + await utils.expectRevert( + tokenHolder.setup( + tokenAddress, + tokenRulesAddress, + ownerAddress, + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ), + 'Should revert as token rules address is null.', + 'TokenRules contract address is null.', + ); + }); + + it('Reverts if owner address is null.', async () => { + const tokenHolder = await TokenHolder.new(); + + const ownerAddress = utils.NULL_ADDRESS; + const tokenAddress = accountProvider.get(); + const tokenRulesAddress = accountProvider.get(); + + await utils.expectRevert( + tokenHolder.setup( + tokenAddress, + tokenRulesAddress, + ownerAddress, + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ), + 'Should revert as owner address is null.', + 'Owner address is null.', + ); + }); + + it('Reverts if setup is called second time.', async () => { + const tokenHolder = await TokenHolder.new(); + + const ownerAddress = accountProvider.get(); + const tokenAddress = accountProvider.get(); + const tokenRulesAddress = accountProvider.get(); + + await tokenHolder.setup( + tokenAddress, + tokenRulesAddress, + ownerAddress, + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ); + + await utils.expectRevert( + tokenHolder.setup( + tokenAddress, + tokenRulesAddress, + ownerAddress, + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ), + 'Should revert as setup() is called second time.', + 'Contract has been already setup.', + ); + + // Testing with different inputs. + await utils.expectRevert( + tokenHolder.setup( + accountProvider.get(), + accountProvider.get(), + accountProvider.get(), + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ), + 'Should revert as setup() is called second time.', + 'Contract has been already setup.', + ); + }); + + it('Reverts if session keys arrays have different lengths.', async () => { + const tokenHolder = await TokenHolder.new(); + + const ownerAddress = accountProvider.get(); + const tokenAddress = accountProvider.get(); + const tokenRulesAddress = accountProvider.get(); + + const blockNumber = await web3.eth.getBlockNumber(); + + await utils.expectRevert( + tokenHolder.setup( + tokenAddress, + tokenRulesAddress, + ownerAddress, + [accountProvider.get()], // session key addresses + [1, 2], // session keys' spending limits + [blockNumber + 10], // session keys' expiration heights + ), + 'Should revert as session keys and spending limits arrays lengths are different.', + 'Session keys and spending limits arrays lengths are different.', + ); + + await utils.expectRevert( + tokenHolder.setup( + tokenAddress, + tokenRulesAddress, + ownerAddress, + [accountProvider.get()], // session key addresses + [1], // session keys' spending limits + [blockNumber + 10, blockNumber + 10], // session keys' expiration heights + ), + 'Should revert as session keys and expiration heights arrays lengths are different.', + 'Session keys and expiration heights arrays lengths are different.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits SessionAuthorized event.', async () => { + const tokenHolder = await TokenHolder.new(); + + const blockNumber = await web3.eth.getBlockNumber(); + + const ownerAddress = accountProvider.get(); + const tokenAddress = accountProvider.get(); + const tokenRulesAddress = accountProvider.get(); + + const sessionKeyAddress1 = accountProvider.get(); + const sessionKeySpendingLimit1 = 11; + const sessionKeyExpirationHeight1 = blockNumber + 11; + + const sessionKeyAddress2 = accountProvider.get(); + const sessionKeySpendingLimit2 = 22; + const sessionKeyExpirationHeight2 = blockNumber + 22; + + const sessionKeys = [sessionKeyAddress1, sessionKeyAddress2]; + const sessionKeysSpendingLimits = [ + sessionKeySpendingLimit1, sessionKeySpendingLimit2, + ]; + const sessionKeysExpirationHeights = [ + sessionKeyExpirationHeight1, sessionKeyExpirationHeight2, + ]; + + const transactionResponse = await tokenHolder.setup( + tokenAddress, + tokenRulesAddress, + ownerAddress, + sessionKeys, + sessionKeysSpendingLimits, + sessionKeysExpirationHeights, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 2, + ); + + Event.assertEqual(events[0], { + name: 'SessionAuthorized', + args: { + _sessionKey: sessionKeyAddress1, + }, + }); + + Event.assertEqual(events[1], { + name: 'SessionAuthorized', + args: { + _sessionKey: sessionKeyAddress2, + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that passed arguments are set correctly.', async () => { + const tokenHolder = await TokenHolder.new(); + + const blockNumber = await web3.eth.getBlockNumber(); + + const ownerAddress = accountProvider.get(); + const tokenAddress = accountProvider.get(); + const tokenRulesAddress = accountProvider.get(); + + const sessionKeyAddress1 = accountProvider.get(); + const sessionKeySpendingLimit1 = 11; + const sessionKeyExpirationHeight1 = blockNumber + 11; + + const sessionKeyAddress2 = accountProvider.get(); + const sessionKeySpendingLimit2 = 22; + const sessionKeyExpirationHeight2 = blockNumber + 22; + + const sessionKeys = [sessionKeyAddress1, sessionKeyAddress2]; + const sessionKeysSpendingLimits = [ + sessionKeySpendingLimit1, sessionKeySpendingLimit2, + ]; + const sessionKeysExpirationHeights = [ + sessionKeyExpirationHeight1, sessionKeyExpirationHeight2, + ]; + + await tokenHolder.setup( + tokenAddress, + tokenRulesAddress, + ownerAddress, + sessionKeys, + sessionKeysSpendingLimits, + sessionKeysExpirationHeights, + ); + + assert.strictEqual( + (await tokenHolder.token.call()), + tokenAddress, + ); + + assert.strictEqual( + (await tokenHolder.tokenRules.call()), + tokenRulesAddress, + ); + + assert.strictEqual( + (await tokenHolder.owner.call()), + ownerAddress, + ); + + assert.isOk( + (await tokenHolder.sessionWindow.call()).eqn(2), + ); + + const sessionKeyData1 = await tokenHolder.sessionKeys.call( + sessionKeyAddress1, + ); + + assert.isOk( + sessionKeyData1.spendingLimit.eqn(sessionKeySpendingLimit1), + ); + + assert.isOk( + sessionKeyData1.expirationHeight.eqn(sessionKeyExpirationHeight1), + ); + + assert.isOk( + sessionKeyData1.nonce.eqn(0), + ); + + assert.isOk( + sessionKeyData1.session.eqn(2), // REVOKED + 1 + ); + + const sessionKeyData2 = await tokenHolder.sessionKeys.call( + sessionKeyAddress2, + ); + + assert.isOk( + sessionKeyData2.spendingLimit.eqn(sessionKeySpendingLimit2), + ); + + assert.isOk( + sessionKeyData2.expirationHeight.eqn(sessionKeyExpirationHeight2), + ); + + assert.isOk( + sessionKeyData2.nonce.eqn(0), + ); + + assert.isOk( + sessionKeyData2.session.eqn(2), // REVOKED + 1 + ); + }); + + it('Checks storage elements order to assure reserved ' + + 'slot for proxy is valid.', async () => { + const tokenHolder = await TokenHolder.new(); + + const tokenAddress = accountProvider.get(); + const tokenRulesAddress = accountProvider.get(); + const ownerAddress = accountProvider.get(); + + await tokenHolder.setup( + tokenAddress, + tokenRulesAddress, + ownerAddress, + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ); + + assert.strictEqual( + (await web3.eth.getStorageAt(tokenHolder.address, 0)), + '0x00', + ); + }); + }); +}); diff --git a/test/token_holder/submit_authorize_session.js b/test/token_holder/submit_authorize_session.js deleted file mode 100644 index 851adb7..0000000 --- a/test/token_holder/submit_authorize_session.js +++ /dev/null @@ -1,515 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const BN = require('bn.js'); -const web3 = require('../test_lib/web3.js'); -const Utils = require('../test_lib/utils.js'); -const { Event } = require('../test_lib/event_decoder'); -const { TokenHolderUtils } = require('./utils.js'); -const { AccountProvider } = require('../test_lib/utils.js'); - -const TokenHolder = artifacts.require('TokenHolder'); - -function generateSubmitAuthorizeSessionData( - ephemeralKey, spendingLimit, expirationHeight, -) { - return web3.eth.abi.encodeFunctionCall( - { - name: 'authorizeSession', - type: 'function', - inputs: [ - { - type: 'address', - name: '', - }, - { - type: 'uint256', - name: '', - }, - { - type: 'uint256', - name: '', - }, - ], - }, - [ephemeralKey, spendingLimit, expirationHeight], - ); -} - -async function createTokenHolder( - accountProvider, -) { - const required = 1; - - const registeredWallet0 = accountProvider.get(); - - const wallets = [registeredWallet0]; - - const tokenAddress = accountProvider.get(); - const tokenRulesAddress = accountProvider.get(); - - const tokenHolder = await TokenHolder.new( - tokenAddress, tokenRulesAddress, wallets, required, - ); - - return { - tokenHolder, - registeredWallet0, - }; -} - -contract('TokenHolder::submitAuthorizeSession', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-registered wallet calls.', async () => { - const { - tokenHolder, - } = await createTokenHolder(accountProvider); - - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - const spendingLimit = 1; - const expirationHeight = (await web3.eth.getBlockNumber()) + 10; - - await Utils.expectRevert( - tokenHolder.submitAuthorizeSession( - ephemeralKey, - spendingLimit, - expirationHeight, - { from: accountProvider.get() }, - ), - 'Should revert as non-registered wallet calls.', - 'Only wallet is allowed to call.', - ); - }); - - it('Reverts if key to authorize is null.', async () => { - const { - tokenHolder, - registeredWallet0, - } = await createTokenHolder(accountProvider); - - const spendingLimit = 1; - const expirationHeight = (await web3.eth.getBlockNumber()) + 10; - - await Utils.expectRevert( - tokenHolder.submitAuthorizeSession( - Utils.NULL_ADDRESS, - spendingLimit, - expirationHeight, - { from: registeredWallet0 }, - ), - 'Should revert as key to authorize is null.', - 'Key address is null.', - ); - }); - - it('Reverts if key to authorize already was authorized.', async () => { - const { - tokenHolder, - registeredWallet0, - } = await createTokenHolder(accountProvider); - - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - const spendingLimit = 1; - const expirationHeight = (await web3.eth.getBlockNumber()) + 10; - - await tokenHolder.submitAuthorizeSession( - ephemeralKey, - spendingLimit, - expirationHeight, - { from: registeredWallet0 }, - ); - - await Utils.expectRevert( - tokenHolder.submitAuthorizeSession( - ephemeralKey, - spendingLimit, - expirationHeight, - { from: registeredWallet0 }, - ), - 'Should revert as key to authorize was already authorized.', - 'Key exists.', - ); - }); - - it('Reverts if key to authorize is in revoked state.', async () => { - const { - tokenHolder, - registeredWallet0, - } = await createTokenHolder(accountProvider); - - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - const spendingLimit = 1; - const expirationHeight = (await web3.eth.getBlockNumber()) + 10; - - await tokenHolder.submitAuthorizeSession( - ephemeralKey, - spendingLimit, - expirationHeight, - { from: registeredWallet0 }, - ); - - await tokenHolder.revokeSession( - ephemeralKey, - { from: registeredWallet0 }, - ); - - await Utils.expectRevert( - tokenHolder.submitAuthorizeSession( - ephemeralKey, - spendingLimit, - expirationHeight, - { from: registeredWallet0 }, - ), - 'Should revert as key to authorize was revoked.', - 'Key exists.', - ); - }); - - it('Reverts if key to authorize has expired.', async () => { - const { - tokenHolder, - registeredWallet0, - } = await createTokenHolder(accountProvider); - - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - const spendingLimit = 1; - const expirationHeightDelta = 10; - const blockNumber = await web3.eth.getBlockNumber(); - const expirationHeight = blockNumber + expirationHeightDelta; - - await tokenHolder.submitAuthorizeSession( - ephemeralKey, - spendingLimit, - expirationHeight, - { from: registeredWallet0 }, - ); - - for (let i = 0; i < expirationHeightDelta; i += 1) { - // eslint-disable-next-line no-await-in-loop - await Utils.advanceBlock(); - } - - // Checking that key has expired. - assert.isOk( - (await tokenHolder.ephemeralKeys.call(ephemeralKey)) - .expirationHeight <= (await web3.eth.getBlockNumber()), - ); - - await Utils.expectRevert( - tokenHolder.submitAuthorizeSession( - ephemeralKey, - spendingLimit, - expirationHeight, - { - from: registeredWallet0, - }, - ), - 'Should revert as key to submit has already expired.', - 'Key exists.', - ); - }); - - it('Expiration height is less or equal to the block number', async () => { - const { - tokenHolder, - registeredWallet0, - } = await createTokenHolder(accountProvider); - - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - const spendingLimit = 10; - - await Utils.expectRevert( - tokenHolder.submitAuthorizeSession( - ephemeralKey, - spendingLimit, - (await web3.eth.getBlockNumber()), - { from: registeredWallet0 }, - ), - 'Should revert as expiration heigh is less than equal to the ' - + ' current block height', - 'Expiration height is lte to the current block height.', - ); - }); - }); - - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - // Because of 1-wallet-1-required the submitted authorization - // request is going to be executed immediately, hence 3 events. - it('Emits SessionAuthorizationSubmitted, TransactionConfirmed ' - + 'TransactionExecutionSucceeded events.', async () => { - const required = 1; - - const registeredWallet0 = accountProvider.get(); - - const wallets = [registeredWallet0]; - - const token = accountProvider.get(); - const tokenRules = accountProvider.get(); - - const tokenHolder = await TokenHolder.new( - token, tokenRules, wallets, required, - ); - - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - const spendingLimit = 1; - const expirationHeight = await web3.eth.getBlockNumber() + 50; - - const transactionResponse = await tokenHolder.submitAuthorizeSession( - ephemeralKey, - spendingLimit, - expirationHeight, - { from: registeredWallet0 }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 3, - 'As the requirement is 1, transaction would be executed ' - + 'afterwards, hence SessionAuthorizationSubmitted, ' - + 'TransactionConfirmed and TransactionExecutionSucceeded ' - + 'would be emitted.', - ); - - // The first emitted event should be 'SessionAuthorizationSubmitted'. - Event.assertEqual(events[0], { - name: 'SessionAuthorizationSubmitted', - args: { - _transactionID: new BN(0), - _ephemeralKey: ephemeralKey, - _spendingLimit: new BN(spendingLimit), - _expirationHeight: new BN(expirationHeight), - }, - }); - - // The second emitted event should be 'TransactionConfirmed', - // because the wallet that submitted a session authorization request - // should confirm it afterwards in the same call. - Event.assertEqual(events[1], { - name: 'TransactionConfirmed', - args: { - _transactionID: new BN(0), - _wallet: registeredWallet0, - }, - }); - - // The third emitted event should be - // 'TransactionExecutionSucceeded', because of the setup - // 1-wallet-1-requirement. - Event.assertEqual(events[2], { - name: 'TransactionExecutionSucceeded', - args: { - _transactionID: new BN(0), - }, - }); - }); - - // Because of 2-wallet-2-required the submitted authorization - // request is *not* going to be executed immediately, hence 2 events. - it('Emits SessionAuthorizationSubmitted and TransactionConfirmed', async () => { - const required = 2; - - const registeredWallet0 = accountProvider.get(); - const registeredWallet1 = accountProvider.get(); - - const wallets = [registeredWallet0, registeredWallet1]; - - const token = accountProvider.get(); - const tokenRules = accountProvider.get(); - - const tokenHolder = await TokenHolder.new( - token, tokenRules, wallets, required, - ); - - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - const spendingLimit = 1; - const expirationHeight = await web3.eth.getBlockNumber() + 50; - - const transactionResponse = await tokenHolder.submitAuthorizeSession( - ephemeralKey, - spendingLimit, - expirationHeight, - { from: registeredWallet0 }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 2, - 'As the requirement is 2, transaction would not be executed ' - + 'afterwards, hence SessionAuthorizationSubmitted and ' - + 'TransactionConfirmed would be emitted.', - ); - - // The first emitted event should be 'SessionAuthorizationSubmitted'. - Event.assertEqual(events[0], { - name: 'SessionAuthorizationSubmitted', - args: { - _transactionID: new BN(0), - _ephemeralKey: ephemeralKey, - _spendingLimit: new BN(spendingLimit), - _expirationHeight: new BN(expirationHeight), - }, - }); - - // The second emitted event should be 'TransactionConfirmed', - // because the wallet that submitted a session authorization request - // should confirm it afterwards in the same call. - Event.assertEqual(events[1], { - name: 'TransactionConfirmed', - args: { - _transactionID: new BN(0), - _wallet: registeredWallet0, - }, - }); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks states in case of 1-wallet-1-required.', async () => { - const required = 1; - - const registeredWallet0 = accountProvider.get(); - - const wallets = [registeredWallet0]; - - const token = accountProvider.get(); - const tokenRules = accountProvider.get(); - - const tokenHolder = await TokenHolder.new( - token, tokenRules, wallets, required, - ); - - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - const spendingLimit = 1; - const expirationHeight = await web3.eth.getBlockNumber() + 50; - - const transactionID = await TokenHolderUtils.submitAuthorizeSession( - tokenHolder, - ephemeralKey, - spendingLimit, - expirationHeight, - { from: registeredWallet0 }, - ); - - const keyData = await tokenHolder.ephemeralKeys.call(ephemeralKey); - - assert.isOk( - keyData.spendingLimit.eqn(spendingLimit), - ); - - assert.isOk( - keyData.nonce.eqn(0), - ); - - assert.isOk( - keyData.expirationHeight.eqn(expirationHeight), - ); - - assert.isOk( - // TokenHolder.AuthorizationStatus.AUTHORIZED == 1 - keyData.status.eqn(1), - ); - - const transaction = await tokenHolder.transactions.call( - transactionID, - ); - - assert.strictEqual( - transaction.destination, - tokenHolder.address, - ); - - assert.strictEqual( - transaction.data, - generateSubmitAuthorizeSessionData( - ephemeralKey, spendingLimit, expirationHeight, - ), - ); - - assert.isOk( - transaction.executed, - ); - }); - - it('Checks states in case of 2-walleta-2-required.', async () => { - const required = 2; - - const registeredWallet0 = accountProvider.get(); - const registeredWallet1 = accountProvider.get(); - - const wallets = [registeredWallet0, registeredWallet1]; - - const token = accountProvider.get(); - const tokenRules = accountProvider.get(); - - const tokenHolder = await TokenHolder.new( - token, tokenRules, wallets, required, - ); - - const ephemeralKey = '0x62502C4DF73935D0D10054b0Fb8cC036534C6fb0'; - const spendingLimit = 1; - const expirationHeight = await web3.eth.getBlockNumber() + 50; - - const transactionID = await TokenHolderUtils.submitAuthorizeSession( - tokenHolder, - ephemeralKey, - spendingLimit, - expirationHeight, - { - from: registeredWallet0, - }, - ); - - const keyData = await tokenHolder.ephemeralKeys.call(ephemeralKey); - - assert.isOk( - // TokenHolder.AuthorizationStatus.NOT_AUTHORIZED == 0 - keyData.status.eqn(0), - ); - - const transaction = await tokenHolder.transactions.call( - transactionID, - ); - - assert.strictEqual( - transaction.destination, - tokenHolder.address, - ); - - assert.strictEqual( - transaction.data, - generateSubmitAuthorizeSessionData( - ephemeralKey, spendingLimit, expirationHeight, - ), - ); - - assert.isNotOk( - transaction.executed, - ); - }); - }); -}); diff --git a/test/token_holder/utils.js b/test/token_holder/utils.js index a9663dc..40ef1e1 100644 --- a/test/token_holder/utils.js +++ b/test/token_holder/utils.js @@ -1,21 +1,77 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; + +const TokenHolder = artifacts.require('TokenHolder'); +const TokenRulesSpy = artifacts.require('TokenRulesSpy'); +const UtilityTokenFake = artifacts.require('UtilityTokenFake'); + +const web3 = require('../test_lib/web3.js'); class TokenHolderUtils { - static async submitAuthorizeSession( - tokenHolder, ephemeralKey, spendingLimit, expirationHeight, options, - ) { - const transactionID = await tokenHolder.submitAuthorizeSession.call( - ephemeralKey, spendingLimit, expirationHeight, options, - ); - - await tokenHolder.submitAuthorizeSession( - ephemeralKey, spendingLimit, expirationHeight, options, - ); - - return transactionID; - } + static async createUtilityMockToken() { + const utilityToken = await UtilityTokenFake.new( + 'OST', 'Open Simple Token', 1, + ); + + return { utilityToken }; + } + + static async createMockTokenRules() { + const tokenRules = await TokenRulesSpy.new(); + + return { tokenRules }; + } + + static async createTokenHolder( + accountProvider, + utilityTokenMock, tokenRulesMock, + ) { + const tokenHolderOwnerAddress = accountProvider.get(); + + const tokenHolder = await TokenHolder.new(); + await tokenHolder.setup( + utilityTokenMock.address, + tokenRulesMock.address, + tokenHolderOwnerAddress, + [], + [], + [], + ); + + return { + tokenHolderOwnerAddress, + tokenHolder, + }; + } + + static async authorizeSessionKey( + tokenHolder, tokenHolderOwnerAddress, + sessionPublicKeyToAuthorize, spendingLimit, deltaExpirationHeight, + ) { + const blockNumber = await web3.eth.getBlockNumber(); + + await tokenHolder.authorizeSession( + sessionPublicKeyToAuthorize, + spendingLimit, + blockNumber + deltaExpirationHeight, + { from: tokenHolderOwnerAddress }, + ); + } } module.exports = { - TokenHolderUtils, + TokenHolderUtils, }; diff --git a/test/token_rules/add_global_constraint.js b/test/token_rules/add_global_constraint.js deleted file mode 100644 index 4a7b621..0000000 --- a/test/token_rules/add_global_constraint.js +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const Utils = require('../test_lib/utils.js'); -const { Event } = require('../test_lib/event_decoder'); -const { AccountProvider } = require('../test_lib/utils'); -const TokenRulesUtils = require('./utils.js'); - -contract('TokenRules::addGlobalConstraint', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-organization address is adding constraint.', async () => { - const { - tokenRules, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const constraintAddress = accountProvider.get(); - - const nonOrganizationAddress = accountProvider.get(); - - await Utils.expectRevert( - tokenRules.addGlobalConstraint( - constraintAddress, - { from: nonOrganizationAddress }, - ), - 'Should revert as non-organization address is adding constraint.', - 'Only organization is allowed to call.', - ); - }); - - it('Reverts if null address is added as a constraint.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const constraintAddress = Utils.NULL_ADDRESS; - - await Utils.expectRevert( - tokenRules.addGlobalConstraint( - constraintAddress, - { from: organizationAddress }, - ), - 'Should revert as constraint\'s address to add is null.', - 'Constraint to add is null.', - ); - }); - - it('Reverts if constraint to add already exists.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const constraintAddress = accountProvider.get(); - const existingConstraintAddress = constraintAddress; - - await tokenRules.addGlobalConstraint( - constraintAddress, - { from: organizationAddress }, - ); - - await Utils.expectRevert( - tokenRules.addGlobalConstraint( - existingConstraintAddress, - { from: organizationAddress }, - ), - 'Should revert as constraint to add already exists.', - 'Constraint to add already exists.', - ); - }); - }); - - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits GlobalConstraintAdded when adding constraint.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const constraintAddress = accountProvider.get(); - - const transactionResponse = await tokenRules.addGlobalConstraint( - constraintAddress, - { from: organizationAddress }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 1, - 'Only GlobalConstraintAdded should be emitted.', - ); - - Event.assertEqual(events[0], { - name: 'GlobalConstraintAdded', - args: { - _globalConstraintAddress: constraintAddress, - }, - }); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - it('Checks that constraint exists after adding.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const constraintAddress = accountProvider.get(); - - await tokenRules.addGlobalConstraint( - constraintAddress, - { from: organizationAddress }, - ); - - assert.isOk( - await TokenRulesUtils.constraintExists( - tokenRules, constraintAddress, - ), - ); - }); - }); -}); diff --git a/test/token_rules/allow_transfers.js b/test/token_rules/allow_transfers.js index 765032a..076b23d 100644 --- a/test/token_rules/allow_transfers.js +++ b/test/token_rules/allow_transfers.js @@ -1,4 +1,4 @@ -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,28 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. +'use strict'; + const TokenRulesUtils = require('./utils.js'); const { AccountProvider } = require('../test_lib/utils'); contract('TokenRules::allowTransfers', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - it('Checks that transfer is allowed.', async () => { - const { - tokenRules, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); + const accountProvider = new AccountProvider(accounts); + it('Checks that transfer is allowed.', async () => { + const { + tokenRules, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); - const tokenHolder = accountProvider.get(); + const tokenHolder = accountProvider.get(); - assert.isNotOk( - await tokenRules.allowedTransfers.call(tokenHolder), - ); + assert.isNotOk( + await tokenRules.allowedTransfers.call(tokenHolder), + ); - await tokenRules.allowTransfers( - { from: tokenHolder }, - ); + await tokenRules.allowTransfers( + { from: tokenHolder }, + ); - assert.isOk( - await tokenRules.allowedTransfers.call(tokenHolder), - ); - }); + assert.isOk( + await tokenRules.allowedTransfers.call(tokenHolder), + ); + }); }); diff --git a/test/token_rules/check_global_constraints.js b/test/token_rules/check_global_constraints.js deleted file mode 100644 index 0cbb16d..0000000 --- a/test/token_rules/check_global_constraints.js +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const utils = require('../test_lib/utils.js'); -const { AccountProvider } = require('../test_lib/utils'); -const TokenRulesUtils = require('./utils.js'); - -const PassingConstraint = artifacts.require('TokenRulesPassingGlobalConstraint.sol'); -const FailingConstraint = artifacts.require('TokenRulesFailingGlobalConstraint.sol'); - - -contract('TokenRules::checkGlobalConstraints', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if transfersTo and transfersAmount array lengths are not equal.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const transferTo0 = accountProvider.get(); - const transfersTo = [transferTo0]; - const transfersAmount = []; - - await utils.expectRevert( - tokenRules.checkGlobalConstraints.call( - accountProvider.get(), - transfersTo, - transfersAmount, - { from: organizationAddress }, - ), - 'Should revert as transfers "to" and "amount" arrays length ' - + 'are not equal.', - '\'to\' and \'amount\' transfer arrays\' lengths are not equal.', - ); - }); - }); - contract('Checking Constraints', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Passes if 2 registered constraints are passing.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const transferTo0 = accountProvider.get(); - const transfersTo = [transferTo0]; - const transfersAmount = [10]; - - const passingConstraint1 = await PassingConstraint.new(); - const passingConstraint2 = await PassingConstraint.new(); - - await tokenRules.addGlobalConstraint( - passingConstraint1.address, - { from: organizationAddress }, - ); - - await tokenRules.addGlobalConstraint( - passingConstraint2.address, - { from: organizationAddress }, - ); - - const status = await tokenRules.checkGlobalConstraints.call( - accountProvider.get(), - transfersTo, - transfersAmount, - { from: organizationAddress }, - ); - - assert.isOk( - status, - 'Should pass, as two registered constraints are passing.', - ); - }); - - it('Fails if there is 1 passing and 1 failing constraint.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const transferTo0 = accountProvider.get(); - const transfersTo = [transferTo0]; - const transfersAmount = [10]; - - const passingConstraint1 = await PassingConstraint.new(); - const failingConstraint1 = await FailingConstraint.new(); - - await tokenRules.addGlobalConstraint( - passingConstraint1.address, - { from: organizationAddress }, - ); - - await tokenRules.addGlobalConstraint( - failingConstraint1.address, - { from: organizationAddress }, - ); - - const status = await tokenRules.checkGlobalConstraints.call( - accountProvider.get(), - transfersTo, - transfersAmount, - { from: organizationAddress }, - ); - - assert.isNotOk( - status, - 'Should fail, as there is 1 failing constraint and 1 passing.', - ); - }); - }); -}); diff --git a/test/token_rules/constructor.js b/test/token_rules/constructor.js index 0bdabc8..8b82257 100644 --- a/test/token_rules/constructor.js +++ b/test/token_rules/constructor.js @@ -1,4 +1,4 @@ -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,55 +12,56 @@ // See the License for the specific language governing permissions and // limitations under the License. +'use strict'; + const utils = require('../test_lib/utils.js'); const { AccountProvider } = require('../test_lib/utils'); const TokenRules = artifacts.require('TokenRules'); +const Organization = artifacts.require('Organization'); contract('TokenRules::constructor', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if organization address is null.', async () => { - const organization = utils.NULL_ADDRESS; - const token = accountProvider.get(); + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); - await utils.expectRevert( - TokenRules.new(organization, token), - 'Should revert as organization address is null.', - 'Organization address is null.', - ); - }); - it('Reverts if token is null.', async () => { - const organization = accountProvider.get(); - const token = utils.NULL_ADDRESS; + it('Reverts if token is null.', async () => { + const owner = accountProvider.get(); + const organization = await Organization.new( + owner, owner, [], 0, + { from: accountProvider.get() }, + ); + const token = utils.NULL_ADDRESS; - await utils.expectRevert( - TokenRules.new(organization, token), - 'Should revert as token is null.', - 'Token address is null.', - ); - }); + await utils.expectRevert( + TokenRules.new(organization.address, token), + 'Should revert as token is null.', + 'Token address is null.', + ); }); + }); - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); - it('Checks that passed arguments are set correctly.', async () => { - const organization = accountProvider.get(); - const token = accountProvider.get(); + it('Checks initialization of the contract\'s storage.', async () => { + const owner = accountProvider.get(); + const organization = await Organization.new( + owner, owner, [], 0, + { from: accountProvider.get() }, + ); + const token = accountProvider.get(); - const tokenRules = await TokenRules.new(organization, token); + const tokenRules = await TokenRules.new(organization.address, token); - assert.strictEqual( - (await tokenRules.organization.call()), - organization, - ); + assert.strictEqual( + await tokenRules.token.call(), + token, + ); - assert.strictEqual( - (await tokenRules.token.call()), - token, - ); - }); + assert.strictEqual( + await tokenRules.areDirectTransfersEnabled.call(), + true, + ); }); + }); }); diff --git a/test/token_rules/direct_transfers.js b/test/token_rules/direct_transfers.js new file mode 100644 index 0000000..431b0d2 --- /dev/null +++ b/test/token_rules/direct_transfers.js @@ -0,0 +1,190 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const BN = require('bn.js'); +const utils = require('../test_lib/utils.js'); +const { AccountProvider } = require('../test_lib/utils'); +const TokenRulesUtils = require('./utils.js'); + +async function happyPath(accountProvider) { + const { + tokenRules, + organizationAddress, + token, + worker, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + const tokenHolder = accountProvider.get(); + const spendingLimit = 100; + await token.increaseBalance(tokenHolder, 100); + await token.approve( + tokenRules.address, + spendingLimit, + { from: tokenHolder }, + ); + + await tokenRules.allowTransfers( + { from: tokenHolder }, + ); + + const transferTo0 = accountProvider.get(); + const transferAmount0 = 1; + + const transfersTo = [transferTo0]; + const transfersAmount = [transferAmount0]; + + return { + tokenRules, + organizationAddress, + token, + tokenHolder, + transfersTo, + transfersAmount, + worker, + }; +} + +contract('TokenRules::directTransfers', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if caller\'s account has not allowed transfers.', async () => { + const { + tokenRules, + tokenHolder, + transfersTo, + transfersAmount, + } = await happyPath(accountProvider); + + await tokenRules.disallowTransfers( + { from: tokenHolder }, + ); + + await utils.expectRevert( + tokenRules.directTransfers( + transfersTo, + transfersAmount, + { from: tokenHolder }, + ), + 'Should revert as caller\'s account has not allowed transfers.', + 'Transfers from the address are not allowed.', + ); + }); + + it('Reverts if transfersTo and transferAmount array lengths are not equal.', async () => { + const { + tokenRules, + tokenHolder, + } = await happyPath(accountProvider); + + const transferTo0 = accountProvider.get(); + const transfersTo = [transferTo0]; + const transfersAmount = []; + + await utils.expectRevert( + tokenRules.directTransfers( + transfersTo, + transfersAmount, + { from: tokenHolder }, + ), + 'Should revert as transfers "to" and "amount" arrays length ' + + 'are not equal.', + '\'to\' and \'amount\' transfer arrays\' lengths are not equal.', + ); + }); + }); + + contract('Execution Validity', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that token transferFrom function is called.', async () => { + const { + tokenRules, + token, + tokenHolder, + transfersTo, + transfersAmount, + } = await happyPath(accountProvider); + + // For test validity perspective array should not be empty. + assert(transfersTo.length !== 0); + + const tokenHolderInitialBalance = await token.balanceOf(tokenHolder); + const transfersToInitialBalances = []; + let transfersAmountSum = new BN(0); + for (let i = 0; i < transfersTo.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + const initialBalance = await token.balanceOf.call(transfersTo[i]); + transfersToInitialBalances.push(initialBalance); + + transfersAmountSum = transfersAmountSum.add(new BN(transfersAmount[i])); + } + + // For test validity perspective, we should not fail in this case. + assert(tokenHolderInitialBalance.gte(transfersAmountSum)); + + await tokenRules.directTransfers( + transfersTo, + transfersAmount, + { from: tokenHolder }, + ); + + assert.strictEqual( + (await token.balanceOf(tokenHolder)).cmp( + tokenHolderInitialBalance.sub(transfersAmountSum), + ), + 0, + ); + + for (let i = 0; i < transfersTo.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + const balance = await token.balanceOf.call(transfersTo[i]); + + assert.strictEqual( + balance.cmp( + transfersToInitialBalances[i].add( + new BN(transfersAmount[i]), + ), + ), + 0, + ); + } + }); + + it('Checks that at the end TokenRules disallow transfers.', async () => { + const { + tokenRules, + tokenHolder, + transfersTo, + transfersAmount, + } = await happyPath(accountProvider); + + await tokenRules.allowTransfers( + { from: tokenHolder }, + ); + + await tokenRules.directTransfers( + transfersTo, + transfersAmount, + { from: tokenHolder }, + ); + + assert.isNotOk( + await tokenRules.allowedTransfers.call(tokenHolder), + ); + }); + }); +}); diff --git a/test/token_rules/disable_direct_transfers.js b/test/token_rules/disable_direct_transfers.js new file mode 100644 index 0000000..09e64a5 --- /dev/null +++ b/test/token_rules/disable_direct_transfers.js @@ -0,0 +1,80 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const utils = require('../test_lib/utils.js'); +const { AccountProvider } = require('../test_lib/utils'); +const TokenRulesUtils = require('./utils.js'); + +async function prepare(accountProvider) { + const { + organizationWorker, + tokenRules, + token, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + return { + organizationWorker, + tokenRules, + token, + }; +} + +contract('TokenRules::disableDirectTransfers', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if a caller is not an organization worker.', async () => { + const { + tokenRules, + } = await prepare(accountProvider); + + await utils.expectRevert( + tokenRules.disableDirectTransfers( + { from: accountProvider.get() }, + ), + 'Should revert as a caller is not an organization worker.', + 'Only whitelisted workers are allowed to call this method.', + ); + }); + }); + + contract('Execution Validity', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that direct transfers are successfully disabled.', async () => { + const { + organizationWorker, + tokenRules, + } = await prepare(accountProvider); + + await tokenRules.enableDirectTransfers( + { from: organizationWorker }, + ); + + assert.isOk( + await tokenRules.areDirectTransfersEnabled.call(), + ); + + await tokenRules.disableDirectTransfers( + { from: organizationWorker }, + ); + + assert.isNotOk( + await tokenRules.areDirectTransfersEnabled.call(), + ); + }); + }); +}); diff --git a/test/token_rules/disallow_transfers.js b/test/token_rules/disallow_transfers.js index 4ce7d96..ef3fe80 100644 --- a/test/token_rules/disallow_transfers.js +++ b/test/token_rules/disallow_transfers.js @@ -1,4 +1,4 @@ -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,32 +12,34 @@ // See the License for the specific language governing permissions and // limitations under the License. +'use strict'; + const TokenRulesUtils = require('./utils.js'); const { AccountProvider } = require('../test_lib/utils'); contract('TokenRules::disallowTransfers', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - it('Checks that transfer is disallowed.', async () => { - const { - tokenRules, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const tokenHolder = accountProvider.get(); - - await tokenRules.allowTransfers( - { from: tokenHolder }, - ); - - assert.isOk( - await tokenRules.allowedTransfers.call(tokenHolder), - ); - - await tokenRules.disallowTransfers( - { from: tokenHolder }, - ); - - assert.isNotOk( - await tokenRules.allowedTransfers.call(tokenHolder), - ); - }); + const accountProvider = new AccountProvider(accounts); + it('Checks that transfer is disallowed.', async () => { + const { + tokenRules, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + const tokenHolder = accountProvider.get(); + + await tokenRules.allowTransfers( + { from: tokenHolder }, + ); + + assert.isOk( + await tokenRules.allowedTransfers.call(tokenHolder), + ); + + await tokenRules.disallowTransfers( + { from: tokenHolder }, + ); + + assert.isNotOk( + await tokenRules.allowedTransfers.call(tokenHolder), + ); + }); }); diff --git a/test/token_rules/enable_direct_transfers.js b/test/token_rules/enable_direct_transfers.js new file mode 100644 index 0000000..c1c430f --- /dev/null +++ b/test/token_rules/enable_direct_transfers.js @@ -0,0 +1,80 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const utils = require('../test_lib/utils.js'); +const { AccountProvider } = require('../test_lib/utils'); +const TokenRulesUtils = require('./utils.js'); + +async function prepare(accountProvider) { + const { + organizationWorker, + tokenRules, + token, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + return { + organizationWorker, + tokenRules, + token, + }; +} + +contract('TokenRules::enableDirectTransfers', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if a caller is not an organization worker.', async () => { + const { + tokenRules, + } = await prepare(accountProvider); + + await utils.expectRevert( + tokenRules.enableDirectTransfers( + { from: accountProvider.get() }, + ), + 'Should revert as a caller is not an organization worker.', + 'Only whitelisted workers are allowed to call this method.', + ); + }); + }); + + contract('Execution Validity', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that direct transfers are successfully enabled.', async () => { + const { + organizationWorker, + tokenRules, + } = await prepare(accountProvider); + + await tokenRules.disableDirectTransfers( + { from: organizationWorker }, + ); + + assert.isNotOk( + await tokenRules.areDirectTransfersEnabled.call(), + ); + + await tokenRules.enableDirectTransfers( + { from: organizationWorker }, + ); + + assert.isOk( + await tokenRules.areDirectTransfersEnabled.call(), + ); + }); + }); +}); diff --git a/test/token_rules/execute_transfers.js b/test/token_rules/execute_transfers.js index b2fc63e..d55b358 100644 --- a/test/token_rules/execute_transfers.js +++ b/test/token_rules/execute_transfers.js @@ -1,4 +1,4 @@ -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,256 +12,220 @@ // See the License for the specific language governing permissions and // limitations under the License. +'use strict'; + const BN = require('bn.js'); const utils = require('../test_lib/utils.js'); const { AccountProvider } = require('../test_lib/utils'); const TokenRulesUtils = require('./utils.js'); -const PassingConstraint = artifacts.require('TokenRulesPassingGlobalConstraint.sol'); -const FailingConstraint = artifacts.require('TokenRulesFailingGlobalConstraint.sol'); - async function happyPath(accountProvider) { - const { - tokenRules, - organizationAddress, - token, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const passingConstraint1 = await PassingConstraint.new(); + const { + tokenRules, + organizationAddress, + token, + organizationWorker, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + const ruleAddress0 = accountProvider.get(); + await tokenRules.registerRule( + 'ruleName0', + ruleAddress0, + 'ruleAbi0', + { from: organizationWorker }, + ); + await token.increaseBalance(ruleAddress0, 100); + + const tokenHolder = accountProvider.get(); + const spendingLimit = 100; + await token.increaseBalance(tokenHolder, 100); + await token.approve( + tokenRules.address, + spendingLimit, + { from: tokenHolder }, + ); + + await tokenRules.allowTransfers( + { from: tokenHolder }, + ); + + const transferTo0 = accountProvider.get(); + const transferAmount0 = 1; + + const transfersTo = [transferTo0]; + const transfersAmount = [transferAmount0]; + + return { + tokenRules, + organizationAddress, + token, + tokenHolder, + ruleAddress0, + transfersTo, + transfersAmount, + organizationWorker, + }; +} - await tokenRules.addGlobalConstraint( - passingConstraint1.address, - { from: organizationAddress }, - ); +contract('TokenRules::executeTransfers', async () => { + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + it('Reverts if non-registered rule calls.', async () => { + const { + tokenRules, + tokenHolder, + transfersTo, + transfersAmount, + } = await happyPath(accountProvider); + + const nonRegisteredRuleAddress = accountProvider.get(); + + await utils.expectRevert( + tokenRules.executeTransfers( + tokenHolder, + transfersTo, + transfersAmount, + { + from: nonRegisteredRuleAddress, + }, + ), + 'Should revert as non registered rule is calling.', + 'Only registered rule is allowed to call.', + ); + }); - const ruleAddress0 = accountProvider.get(); - await tokenRules.registerRule( - 'ruleName0', + it('Reverts if "from" account has not allowed transfers.', async () => { + const { + tokenRules, + tokenHolder, ruleAddress0, - 'ruleAbi0', - { from: organizationAddress }, - ); - await token.setBalance(ruleAddress0, 100); - - const tokenHolder = accountProvider.get(); - const spendingLimit = 100; - await token.setBalance(tokenHolder, 100); - await token.approve( - tokenRules.address, - spendingLimit, - { from: tokenHolder }, - ); + transfersTo, + transfersAmount, + } = await happyPath(accountProvider); - await tokenRules.allowTransfers( + await tokenRules.disallowTransfers( { from: tokenHolder }, - ); + ); + + await utils.expectRevert( + tokenRules.executeTransfers( + tokenHolder, + transfersTo, + transfersAmount, + { from: ruleAddress0 }, + ), + 'Should revert as "from" account has not allowed transfers.', + 'Transfers from the address are not allowed.', + ); + }); - const transferTo0 = accountProvider.get(); - const transferAmount0 = 1; + it('Reverts if transfersTo and transferAmount array lengths are not equal.', async () => { + const { + tokenRules, + tokenHolder, + ruleAddress0, + } = await happyPath(accountProvider); + + const transferTo0 = accountProvider.get(); + const transfersTo = [transferTo0]; + const transfersAmount = []; + + await utils.expectRevert( + tokenRules.executeTransfers( + tokenHolder, + transfersTo, + transfersAmount, + { from: ruleAddress0 }, + ), + 'Should revert as transfers "to" and "amount" arrays length ' + + 'are not equal.', + '\'to\' and \'amount\' transfer arrays\' lengths are not equal.', + ); + }); + }); - const transfersTo = [transferTo0]; - const transfersAmount = [transferAmount0]; + contract('Execution Validity', async (accounts) => { + const accountProvider = new AccountProvider(accounts); - return { + it('Checks that token transferFrom function is called.', async () => { + const { tokenRules, - organizationAddress, token, - passingConstraint1, tokenHolder, ruleAddress0, transfersTo, transfersAmount, - }; -} + } = await happyPath(accountProvider); -contract('TokenRules::executeTransfers', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - it('Reverts if non-registered rule calls.', async () => { - const { - tokenRules, - tokenHolder, - transfersTo, - transfersAmount, - } = await happyPath(accountProvider); - - const nonRegisteredRuleAddress = accountProvider.get(); - - await utils.expectRevert( - tokenRules.executeTransfers( - tokenHolder, - transfersTo, - transfersAmount, - { - from: nonRegisteredRuleAddress, - }, - ), - 'Should revert as non registered rule is calling.', - 'Only registered rule is allowed to call.', - ); - }); - - it('Reverts if "from" account has not allowed transfers.', async () => { - const { - tokenRules, - tokenHolder, - ruleAddress0, - transfersTo, - transfersAmount, - } = await happyPath(accountProvider); - - await tokenRules.disallowTransfers( - { from: tokenHolder }, - ); - - await utils.expectRevert( - tokenRules.executeTransfers( - tokenHolder, - transfersTo, - transfersAmount, - { from: ruleAddress0 }, - ), - 'Should revert as "from" account has not allowed transfers.', - 'Transfers from the address are not allowed.', - ); - }); - - it('Reverts if transfersTo and transferAmount array lengths are not equal.', async () => { - const { - tokenRules, - tokenHolder, - ruleAddress0, - } = await happyPath(accountProvider); - - const transferTo0 = accountProvider.get(); - const transfersTo = [transferTo0]; - const transfersAmount = []; - - await utils.expectRevert( - tokenRules.executeTransfers( - tokenHolder, - transfersTo, - transfersAmount, - { from: ruleAddress0 }, - ), - 'Should revert as transfers "to" and "amount" arrays length ' - + 'are not equal.', - '\'to\' and \'amount\' transfer arrays\' lengths are not equal.', - ); - }); - - it('Reverts if constraints do not fulfill.', async () => { - const { - tokenRules, - organizationAddress, - tokenHolder, - ruleAddress0, - transfersTo, - transfersAmount, - } = await happyPath(accountProvider); - - const failingConstraint1 = await FailingConstraint.new(); - - await tokenRules.addGlobalConstraint( - failingConstraint1.address, - { from: organizationAddress }, - ); - - await utils.expectRevert( - tokenRules.executeTransfers( - tokenHolder, - transfersTo, - transfersAmount, - { from: ruleAddress0 }, - ), - 'Should revert as one of the constraints will fail.', - 'Constraints not fullfilled.', - ); - }); + // For test validity perspective array should not be empty. + assert(transfersTo.length !== 0); + + const tokenHolderInitialBalance = await token.balanceOf(tokenHolder); + const transfersToInitialBalances = []; + let transfersAmountSum = new BN(0); + for (let i = 0; i < transfersTo.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + const initialBalance = await token.balanceOf.call(transfersTo[i]); + transfersToInitialBalances.push(initialBalance); + + transfersAmountSum = transfersAmountSum.add(new BN(transfersAmount[i])); + } + + // For test validity perspective, we should not fail in this case. + assert(tokenHolderInitialBalance.gte(transfersAmountSum)); + + await tokenRules.executeTransfers( + tokenHolder, + transfersTo, + transfersAmount, + { from: ruleAddress0 }, + ); + + assert.strictEqual( + (await token.balanceOf(tokenHolder)).cmp( + tokenHolderInitialBalance.sub(transfersAmountSum), + ), + 0, + ); + + for (let i = 0; i < transfersTo.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + const balance = await token.balanceOf.call(transfersTo[i]); + + assert.strictEqual( + balance.cmp( + transfersToInitialBalances[i].add( + new BN(transfersAmount[i]), + ), + ), + 0, + ); + } }); - contract('Execution Validity', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks that token transferFrom function is called.', async () => { - const { - tokenRules, - token, - tokenHolder, - ruleAddress0, - transfersTo, - transfersAmount, - } = await happyPath(accountProvider); - - // For test validity perspective array should not be empty. - assert(transfersTo.length !== 0); - - const tokenHolderInitialBalance = await token.balanceOf(tokenHolder); - const transfersToInitialBalances = []; - let transfersAmountSum = new BN(0); - for (let i = 0; i < transfersTo.length; i += 1) { - // eslint-disable-next-line no-await-in-loop - const initialBalance = await token.balanceOf.call(transfersTo[i]); - transfersToInitialBalances.push(initialBalance); - - transfersAmountSum = transfersAmountSum.add(new BN(transfersAmount[i])); - } - - // For test validity perspective, we should not fail in this case. - assert(tokenHolderInitialBalance.gte(transfersAmountSum)); - - await tokenRules.executeTransfers( - tokenHolder, - transfersTo, - transfersAmount, - { from: ruleAddress0 }, - ); - - assert.strictEqual( - (await token.balanceOf(tokenHolder)).cmp( - tokenHolderInitialBalance.sub(transfersAmountSum), - ), - 0, - ); - - for (let i = 0; i < transfersTo.length; i += 1) { - // eslint-disable-next-line no-await-in-loop - const balance = await token.balanceOf.call(transfersTo[i]); - - assert.strictEqual( - balance.cmp( - transfersToInitialBalances[i].add( - new BN(transfersAmount[i]), - ), - ), - 0, - ); - } - }); - - it('Checks that at the end TokenRules disallow transfers.', async () => { - const { - tokenRules, - tokenHolder, - ruleAddress0, - transfersTo, - transfersAmount, - } = await happyPath(accountProvider); - - await tokenRules.allowTransfers( - { from: tokenHolder }, - ); - - await tokenRules.executeTransfers( - tokenHolder, - transfersTo, - transfersAmount, - { from: ruleAddress0 }, - ); - - assert.isNotOk( - await tokenRules.allowedTransfers.call(tokenHolder), - ); - }); + it('Checks that at the end TokenRules disallow transfers.', async () => { + const { + tokenRules, + tokenHolder, + ruleAddress0, + transfersTo, + transfersAmount, + } = await happyPath(accountProvider); + + await tokenRules.allowTransfers( + { from: tokenHolder }, + ); + + await tokenRules.executeTransfers( + tokenHolder, + transfersTo, + transfersAmount, + { from: ruleAddress0 }, + ); + + assert.isNotOk( + await tokenRules.allowedTransfers.call(tokenHolder), + ); }); + }); }); diff --git a/test/token_rules/register_rule.js b/test/token_rules/register_rule.js index 3e398ab..e3d74a1 100644 --- a/test/token_rules/register_rule.js +++ b/test/token_rules/register_rule.js @@ -1,4 +1,4 @@ -// Copyright 2018 OpenST Ltd. +// Copyright 2019 OpenST Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +'use strict'; + const web3 = require('../test_lib/web3.js'); const utils = require('../test_lib/utils.js'); const { Event } = require('../test_lib/event_decoder'); @@ -20,258 +22,258 @@ const TokenRulesUtils = require('./utils.js'); contract('TokenRules::registerRule', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-organization address calls.', async () => { - const { - tokenRules, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const ruleName = 'A'; - const ruleAddress = accountProvider.get(); - const ruleAbi = `Rule abi of ${ruleName}`; - - const nonOrganizationAddress = accountProvider.get(); - - await utils.expectRevert( - tokenRules.registerRule( - ruleName, - ruleAddress, - ruleAbi, - { from: nonOrganizationAddress }, - ), - 'Should revert as non-organization address calls.', - 'Only organization is allowed to call.', - ); - }); - it('Reverts if rule name is empty.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const ruleName = ''; - const ruleAddress = accountProvider.get(); - const ruleAbi = `Rule abi of ${ruleName}`; - - await utils.expectRevert( - tokenRules.registerRule( - ruleName, - ruleAddress, - ruleAbi, - { from: organizationAddress }, - ), - 'Should revert as rule name is empty.', - 'Rule name is empty.', - ); - }); - it('Reverts if a rule with the same name already registered.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const aRuleName = 'A'; - const aRuleAddress = accountProvider.get(); - const aRuleAbi = `Rule abi of ${aRuleName}`; - - const bRuleName = aRuleName; - const bRuleAddress = accountProvider.get(); - const bRuleAbi = 'Rule abi of B'; - - await tokenRules.registerRule( - aRuleName, - aRuleAddress, - aRuleAbi, - { from: organizationAddress }, - ); - - await utils.expectRevert( - tokenRules.registerRule( - bRuleName, - bRuleAddress, - bRuleAbi, - { from: organizationAddress }, - ), - 'Should revert as a rule with the same name already registered.', - 'Rule with the specified name already exists.', - ); - }); - it('Reverts if rule address is null.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const ruleName = 'A'; - const ruleAddress = utils.NULL_ADDRESS; - const ruleAbi = `Rule abi of ${ruleName}`; - - await utils.expectRevert( - tokenRules.registerRule( - ruleName, - ruleAddress, - ruleAbi, - { from: organizationAddress }, - ), - 'Should revert as rule address is null.', - 'Rule address is null.', - ); - }); - - it('Reverts if rule with the same address already registered.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const aRuleName = 'A'; - const aRuleAddress = accountProvider.get(); - const aRuleAbi = `Rule abi of ${aRuleName}`; - - const bRuleName = 'B'; - const bRuleAddress = aRuleAddress; - const bRuleAbi = `Rule abi of ${bRuleName}`; - - await tokenRules.registerRule( - aRuleName, - aRuleAddress, - aRuleAbi, - { from: organizationAddress }, - ); - - await utils.expectRevert( - tokenRules.registerRule( - bRuleName, - bRuleAddress, - bRuleAbi, - { from: organizationAddress }, - ), - 'Should revert as rule with the specified address already registered.', - 'Rule with the specified address already exists.', - ); - }); - - it('Reverts if rule ABI is empty.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const ruleName = 'A'; - const ruleAddress = accountProvider.get(); - const ruleAbi = ''; - - await utils.expectRevert( - tokenRules.registerRule( - ruleName, - ruleAddress, - ruleAbi, - { from: organizationAddress }, - ), - 'Should revert as rule ABI is empty.', - 'Rule ABI is empty.', - ); - }); + contract('Negative Tests', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Reverts if non organizationWorker address calls.', async () => { + const { + tokenRules, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + const ruleName = 'A'; + const ruleAddress = accountProvider.get(); + const ruleAbi = `Rule abi of ${ruleName}`; + + const nonWorkerAddress = accountProvider.get(); + + await utils.expectRevert( + tokenRules.registerRule( + ruleName, + ruleAddress, + ruleAbi, + { from: nonWorkerAddress }, + ), + 'Should revert as non worker address calls.', + 'Only whitelisted workers are allowed to call this method.', + ); + }); + it('Reverts if rule name is empty.', async () => { + const { + tokenRules, + organizationWorker, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + const ruleName = ''; + const ruleAddress = accountProvider.get(); + const ruleAbi = `Rule abi of ${ruleName}`; + + await utils.expectRevert( + tokenRules.registerRule( + ruleName, + ruleAddress, + ruleAbi, + { from: organizationWorker }, + ), + 'Should revert as rule name is empty.', + 'Rule name is empty.', + ); + }); + it('Reverts if a rule with the same name already registered.', async () => { + const { + tokenRules, + organizationWorker, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + const aRuleName = 'A'; + const aRuleAddress = accountProvider.get(); + const aRuleAbi = `Rule abi of ${aRuleName}`; + + const bRuleName = aRuleName; + const bRuleAddress = accountProvider.get(); + const bRuleAbi = 'Rule abi of B'; + + await tokenRules.registerRule( + aRuleName, + aRuleAddress, + aRuleAbi, + { from: organizationWorker }, + ); + + await utils.expectRevert( + tokenRules.registerRule( + bRuleName, + bRuleAddress, + bRuleAbi, + { from: organizationWorker }, + ), + 'Should revert as a rule with the same name already registered.', + 'Rule with the specified name already exists.', + ); + }); + it('Reverts if rule address is null.', async () => { + const { + tokenRules, + organizationWorker, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + const ruleName = 'A'; + const ruleAddress = utils.NULL_ADDRESS; + const ruleAbi = `Rule abi of ${ruleName}`; + + await utils.expectRevert( + tokenRules.registerRule( + ruleName, + ruleAddress, + ruleAbi, + { from: organizationWorker }, + ), + 'Should revert as rule address is null.', + 'Rule address is null.', + ); }); - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits RuleRegistered event on registering rule.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const aRuleName = 'A'; - const aRuleAddress = accountProvider.get(); - const aRuleAbi = `Rule abi of ${aRuleName}`; - - const transactionResponse = await tokenRules.registerRule( - aRuleName, - aRuleAddress, - aRuleAbi, - { from: organizationAddress }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 1, - 'Only RuleRegistered event should be emitted.', - ); - - Event.assertEqual(events[0], { - name: 'RuleRegistered', - args: { - _ruleName: aRuleName, - _ruleAddress: aRuleAddress, - }, - }); - }); + it('Reverts if rule with the same address already registered.', async () => { + const { + tokenRules, + organizationWorker, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + const aRuleName = 'A'; + const aRuleAddress = accountProvider.get(); + const aRuleAbi = `Rule abi of ${aRuleName}`; + + const bRuleName = 'B'; + const bRuleAddress = aRuleAddress; + const bRuleAbi = `Rule abi of ${bRuleName}`; + + await tokenRules.registerRule( + aRuleName, + aRuleAddress, + aRuleAbi, + { from: organizationWorker }, + ); + + await utils.expectRevert( + tokenRules.registerRule( + bRuleName, + bRuleAddress, + bRuleAbi, + { from: organizationWorker }, + ), + 'Should revert as rule with the specified address already registered.', + 'Rule with the specified address already exists.', + ); }); - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks that rule exists after registration.', async () => { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const aRuleName = 'A'; - const aRuleAddress = accountProvider.get(); - const aRuleAbi = `Rule abi of ${aRuleName}`; - - await tokenRules.registerRule( - aRuleName, - aRuleAddress, - aRuleAbi, - { from: organizationAddress }, - ); - - const ruleIndexByAddress = await tokenRules.rulesByAddress.call( - aRuleAddress, - ); - const ruleIndexByNameHash = await tokenRules.rulesByNameHash.call( - web3.utils.soliditySha3(aRuleName), - ); - - assert.isOk( - ruleIndexByAddress.exists, - ); - - assert.isOk( - ruleIndexByNameHash.exists, - ); - - assert.strictEqual( - ruleIndexByAddress.index.cmp(ruleIndexByNameHash.index), - 0, - ); - - const rule = await tokenRules.rules.call(ruleIndexByAddress.index); - - assert.strictEqual( - rule.ruleName, - aRuleName, - ); - - assert.strictEqual( - rule.ruleAddress, - aRuleAddress, - ); - - assert.strictEqual( - rule.ruleAbi, - aRuleAbi, - ); - }); + it('Reverts if rule ABI is empty.', async () => { + const { + tokenRules, + organizationWorker, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + const ruleName = 'A'; + const ruleAddress = accountProvider.get(); + const ruleAbi = ''; + + await utils.expectRevert( + tokenRules.registerRule( + ruleName, + ruleAddress, + ruleAbi, + { from: organizationWorker }, + ), + 'Should revert as rule ABI is empty.', + 'Rule ABI is empty.', + ); + }); + }); + + contract('Events', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Emits RuleRegistered event on registering rule.', async () => { + const { + tokenRules, + organizationWorker, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + const aRuleName = 'A'; + const aRuleAddress = accountProvider.get(); + const aRuleAbi = `Rule abi of ${aRuleName}`; + + const transactionResponse = await tokenRules.registerRule( + aRuleName, + aRuleAddress, + aRuleAbi, + { from: organizationWorker }, + ); + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + 'Only RuleRegistered event should be emitted.', + ); + + Event.assertEqual(events[0], { + name: 'RuleRegistered', + args: { + _ruleName: aRuleName, + _ruleAddress: aRuleAddress, + }, + }); + }); + }); + + contract('Storage', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + it('Checks that rule exists after registration.', async () => { + const { + tokenRules, + organizationWorker, + } = await TokenRulesUtils.createTokenEconomy(accountProvider); + + const aRuleName = 'A'; + const aRuleAddress = accountProvider.get(); + const aRuleAbi = `Rule abi of ${aRuleName}`; + + await tokenRules.registerRule( + aRuleName, + aRuleAddress, + aRuleAbi, + { from: organizationWorker }, + ); + + const ruleIndexByAddress = await tokenRules.rulesByAddress.call( + aRuleAddress, + ); + const ruleIndexByNameHash = await tokenRules.rulesByNameHash.call( + web3.utils.soliditySha3(aRuleName), + ); + + assert.isOk( + ruleIndexByAddress.exists, + ); + + assert.isOk( + ruleIndexByNameHash.exists, + ); + + assert.strictEqual( + ruleIndexByAddress.index.cmp(ruleIndexByNameHash.index), + 0, + ); + + const rule = await tokenRules.rules.call(ruleIndexByAddress.index); + + assert.strictEqual( + rule.ruleName, + aRuleName, + ); + + assert.strictEqual( + rule.ruleAddress, + aRuleAddress, + ); + + assert.strictEqual( + rule.ruleAbi, + aRuleAbi, + ); }); + }); }); diff --git a/test/token_rules/remove_global_constraint.js b/test/token_rules/remove_global_constraint.js deleted file mode 100644 index b7d995e..0000000 --- a/test/token_rules/remove_global_constraint.js +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const utils = require('../test_lib/utils.js'); -const { Event } = require('../test_lib/event_decoder'); -const { AccountProvider } = require('../test_lib/utils'); -const TokenRulesUtils = require('./utils.js'); - -async function prepareTokenRules(accountProvider) { - const { - tokenRules, - organizationAddress, - } = await TokenRulesUtils.createTokenEconomy(accountProvider); - - const constraintAddress0 = accountProvider.get(); - - await tokenRules.addGlobalConstraint( - constraintAddress0, - { from: organizationAddress }, - ); - - return { - tokenRules, - organizationAddress, - constraintAddress0, - }; -} - -contract('TokenRules::removeGlobalConstraint', async () => { - contract('Negative Tests', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts if non-organization address is removing constraint.', async () => { - const { - tokenRules, - constraintAddress0, - } = await prepareTokenRules(accountProvider); - - const nonOrganizationAddress = accountProvider.get(); - - await utils.expectRevert( - tokenRules.removeGlobalConstraint( - constraintAddress0, - { from: nonOrganizationAddress }, - ), - 'Should revert as non organization address calls.', - 'Only organization is allowed to call.', - ); - }); - - it('Reverts if constraint to remove does not exist.', async () => { - const { - tokenRules, - organizationAddress, - } = await prepareTokenRules(accountProvider); - - const nonExistingConstraintAddress = accountProvider.get(); - - await utils.expectRevert( - tokenRules.removeGlobalConstraint( - nonExistingConstraintAddress, - { from: organizationAddress }, - ), - 'Should revert as constraint to remove does not exist.', - 'Constraint to remove does not exist.', - ); - }); - }); - - contract('Events', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Emits GlobalConstraintRemoved on removing global constraint.', async () => { - const { - tokenRules, - organizationAddress, - constraintAddress0, - } = await prepareTokenRules(accountProvider); - - const transactionResponse = await tokenRules.removeGlobalConstraint( - constraintAddress0, - { from: organizationAddress }, - ); - - const events = Event.decodeTransactionResponse( - transactionResponse, - ); - - assert.strictEqual( - events.length, - 1, - ); - - Event.assertEqual(events[0], { - name: 'GlobalConstraintRemoved', - args: { - _globalConstraintAddress: constraintAddress0, - }, - }); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Checks that constraint does not exist after removing.', async () => { - const { - tokenRules, - organizationAddress, - constraintAddress0, - } = await prepareTokenRules(accountProvider); - - assert.isOk( - await TokenRulesUtils.constraintExists( - tokenRules, constraintAddress0, - ), - ); - - await tokenRules.removeGlobalConstraint( - constraintAddress0, - { from: organizationAddress }, - ); - - assert.isNotOk( - await TokenRulesUtils.constraintExists( - tokenRules, organizationAddress, - ), - ); - }); - }); -}); diff --git a/test/token_rules/utils.js b/test/token_rules/utils.js index 829ba7e..add33b4 100644 --- a/test/token_rules/utils.js +++ b/test/token_rules/utils.js @@ -12,39 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -const EIP20TokenMock = artifacts.require('EIP20TokenMock'); +'use strict'; + +const web3 = require('../test_lib/web3.js'); + +const EIP20TokenFake = artifacts.require('EIP20TokenFake'); const TokenRules = artifacts.require('TokenRules'); +const Organization = artifacts.require('Organization'); /** * Creates an EIP20 instance to be used during TokenRules::executeTransfers * function's testing with the following defaults: - * - conversionRate: 1 - * - conversionRateDecimals: 1 * - symbol: 'OST' * - name: 'Open Simple Token' * - decimals: 1 */ module.exports.createEIP20Token = async () => { - const token = await EIP20TokenMock.new( - 1, 1, 'OST', 'Open Simple Token', 1, - ); + const token = await EIP20TokenFake.new( + 'OST', 'Open Simple Token', 1, + ); - return token; -}; - -/** Returns true if the specified constraint exists, otherwise false. */ -module.exports.constraintExists = async (tokenRules, constraintAddress) => { - const constraintCount = await tokenRules.globalConstraintCount.call(); - - for (let i = 0; i < constraintCount; i += 1) { - // eslint-disable-next-line no-await-in-loop - const c = await tokenRules.globalConstraints.call(i); - if (c === constraintAddress) { - return true; - } - } - - return false; + return token; }; /** @@ -52,16 +40,31 @@ module.exports.constraintExists = async (tokenRules, constraintAddress) => { * (tokenRules, organizationAddress, token) */ module.exports.createTokenEconomy = async (accountProvider) => { - const organizationAddress = accountProvider.get(); - const token = await this.createEIP20Token(); + const organizationOwner = accountProvider.get(); + const organizationWorker = accountProvider.get(); + const organization = await Organization.new( + organizationOwner, + organizationOwner, + [organizationWorker], + (await web3.eth.getBlockNumber()) + 100000, + { from: accountProvider.get() }, + ); + + const token = await this.createEIP20Token(); + + const tokenRules = await TokenRules.new( + organization.address, token.address, + ); + + await tokenRules.enableDirectTransfers({ from: organizationWorker }); - const tokenRules = await TokenRules.new( - organizationAddress, token.address, - ); + const organizationAddress = organization.address; - return { - tokenRules, - organizationAddress, - token, - }; + return { + tokenRules, + token, + organizationAddress, + organizationOwner, + organizationWorker, + }; }; diff --git a/test/transfer_rule/constructor.js b/test/transfer_rule/constructor.js deleted file mode 100644 index f5937de..0000000 --- a/test/transfer_rule/constructor.js +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const web3 = require('../test_lib/web3.js'); -const utils = require('../test_lib/utils.js'); -const { AccountProvider } = require('../test_lib/utils.js'); - -const TransferRule = artifacts.require('TransferRule'); -const tokenRulesMock = artifacts.require('TokenRulesMock'); - - -contract('TransferRule::constructor', async () => { - contract('Negative tests for input parameters:', async (accounts) => { - - const accountProvider = new AccountProvider(accounts); - - it('fails when tokenrules address is null.', async () => { - - await utils.expectRevert(TransferRule.new(utils.NULL_ADDRESS), - 'Should revert as token rule address is null', - 'Token rules address is null.'); - - }); - - contract('Positive test cases', async (accounts) => { - it('sucessfully initializes when passed arguments are set correctly.', async () => { - - const tokenRulesMockInstance = await tokenRulesMock.new(); - - assert.ok(await TransferRule.new(tokenRulesMockInstance.address)); - - }); - }); - }); -}); \ No newline at end of file diff --git a/test/transfer_rule/transfer_from.js b/test/transfer_rule/transfer_from.js deleted file mode 100644 index 8cafcd1..0000000 --- a/test/transfer_rule/transfer_from.js +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2018 OpenST Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const utils = require('../test_lib/utils.js'); -const { AccountProvider } = require('../test_lib/utils.js'); - -const TransferRule = artifacts.require('TransferRule'); -const tokenRulesMock = artifacts.require('TokenRulesMock'); - -contract('TransferRule::transferFrom', async () => { - contract('Negative tests for input parameters:', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Reverts when Token rules address is incorrect.', async () => { - const fromUser = accountProvider.get(); - const toUser = accountProvider.get(); - const amount = 10; - const incorrectTokenRulesAddress = accountProvider.get(); - - const transferRuleInstance = await TransferRule.new(incorrectTokenRulesAddress); - await utils.expectRevert( - transferRuleInstance.transferFrom.call(fromUser, toUser, amount), - ); - }); - }); - - contract('Storage', async (accounts) => { - const accountProvider = new AccountProvider(accounts); - - it('Validates successful transfer.', async () => { - const fromUser = accountProvider.get(); - const toUser = accountProvider.get(); - const amount = 10; - - const tokenRulesMockInstance = await tokenRulesMock.new(); - - const transferRuleInstance = await TransferRule.new(tokenRulesMockInstance.address); - assert.equal(await transferRuleInstance.transferFrom.call(fromUser, toUser, amount), true, 'transferFrom method failed'); - await transferRuleInstance.transferFrom(fromUser, toUser, amount); - - assert.equal(await tokenRulesMockInstance.from.call(), fromUser, 'From address not set correctly'); - assert.equal(await tokenRulesMockInstance.transferTo.call(0), toUser, 'To address not set correctly'); - assert.equal(await tokenRulesMockInstance.transferAmount.call(0), amount, 'Amount is not set correctly'); - }); - }); -}); diff --git a/test/user_wallet_factory/create_user_wallet.js b/test/user_wallet_factory/create_user_wallet.js new file mode 100644 index 0000000..2faca65 --- /dev/null +++ b/test/user_wallet_factory/create_user_wallet.js @@ -0,0 +1,343 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const Utils = require('../test_lib/utils.js'); +const web3 = require('../test_lib/web3.js'); +const { Event } = require('../test_lib/event_decoder'); +const { AccountProvider } = require('../test_lib/utils.js'); + +const ProxyContract = artifacts.require('Proxy'); +const UserWalletFactory = artifacts.require('UserWalletFactory'); +const MasterCopySpy = artifacts.require('MasterCopySpy'); +const TokenHolder = artifacts.require('TokenHolder'); + +function generateMasterCopySpySetupFunctionData(balance) { + return web3.eth.abi.encodeFunctionCall( + { + name: 'setup', + type: 'function', + inputs: [ + { + type: 'uint256', + name: 'balance', + }, + ], + }, + [balance], + ); +} + +contract('UserWalletFactory::createUserWallet', async (accounts) => { + const accountProvider = new AccountProvider(accounts); + + contract('Negative Tests', async () => { + it('Reverts if gnosis safe\'s master copy address is null.', async () => { + const userWalletFactory = await UserWalletFactory.new(); + + await Utils.expectRevert( + userWalletFactory.createUserWallet( + Utils.NULL_ADDRESS, // gnosis safe's master copy + '0x', // gnosis safe's setup data + accountProvider.get(), // token holder's master copy + accountProvider.get(), // token + accountProvider.get(), // token rules + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ), + 'Should revert as the master copy address is null.', + 'Master copy address is null.', + ); + }); + + it('Reverts if token holder\'s master copy address is null.', async () => { + const userWalletFactory = await UserWalletFactory.new(); + + await Utils.expectRevert( + userWalletFactory.createUserWallet( + accountProvider.get(), // gnosis safe's master copy + '0x', // gnosis safe's setup data + Utils.NULL_ADDRESS, // token holder's master copy + accountProvider.get(), // token + accountProvider.get(), // token rules + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ), + 'Should revert as the master copy address is null.', + 'Master copy address is null.', + ); + }); + }); + + contract('User Wallet', async () => { + it('Checks that gnosis safe\'s proxy constructor with the master ' + + 'copy address is called.', async () => { + const userWalletFactory = await UserWalletFactory.new(); + + const gnosisSafeMasterCopy = accountProvider.get(); + + const returnData = await userWalletFactory.createUserWallet.call( + gnosisSafeMasterCopy, + '0x', // gnosis safe's setup data + accountProvider.get(), // token holder's master copy + accountProvider.get(), // token + accountProvider.get(), // token rules + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ); + await userWalletFactory.createUserWallet( + gnosisSafeMasterCopy, + '0x', // gnosis safe's setup data + accountProvider.get(), // token holder's master copy + accountProvider.get(), // token + accountProvider.get(), // token rules + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ); + + const gnosisSafeProxy = await ProxyContract.at(returnData[0]); + + assert.strictEqual( + await gnosisSafeProxy.masterCopy.call(), + gnosisSafeMasterCopy, + ); + }); + + it('Checks that gnosis safe\'s "setup" data is called.', async () => { + const userWalletFactory = await UserWalletFactory.new(); + + const initialBalanceInConstructor = 11; + const gnosisSafeMasterCopy = await MasterCopySpy.new(initialBalanceInConstructor); + + const initialBalanceInSetupCall = 22; + const gnosisSafeSetupData = generateMasterCopySpySetupFunctionData( + initialBalanceInSetupCall, + ); + + const returnData = await userWalletFactory.createUserWallet.call( + gnosisSafeMasterCopy.address, + gnosisSafeSetupData, // gnosis safe's setup data + accountProvider.get(), // token holder's master copy + accountProvider.get(), // token + accountProvider.get(), // token rules + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ); + await userWalletFactory.createUserWallet( + gnosisSafeMasterCopy.address, + gnosisSafeSetupData, // gnosis safe's setup data + accountProvider.get(), // token holder's master copy + accountProvider.get(), // token + accountProvider.get(), // token rules + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ); + + const gnosisSafeProxy = await MasterCopySpy.at(returnData[0]); + + assert.isOk( + (await gnosisSafeMasterCopy.remainingBalance.call()).eqn( + initialBalanceInConstructor, + ), + ); + + assert.isOk( + (await gnosisSafeProxy.remainingBalance.call()).eqn( + initialBalanceInSetupCall, + ), + ); + }); + + it('Checks that token holder\'s proxy constructor with the master ' + + 'copy address is called.', async () => { + const userWalletFactory = await UserWalletFactory.new(); + + const tokenHolderMasterCopy = accountProvider.get(); + + const returnData = await userWalletFactory.createUserWallet.call( + accountProvider.get(), // gnosis safe's master copy + '0x', // gnosis safe's setup data + tokenHolderMasterCopy, // token holder's master copy + accountProvider.get(), // token + accountProvider.get(), // token rules + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ); + await userWalletFactory.createUserWallet( + accountProvider.get(), // gnosis safe's master copy + '0x', // gnosis safe's setup data + tokenHolderMasterCopy, // token holder's master copy + accountProvider.get(), // token + accountProvider.get(), // token rules + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ); + + const tokenHolderProxy = await ProxyContract.at(returnData[1]); + + assert.strictEqual( + await tokenHolderProxy.masterCopy.call(), + tokenHolderMasterCopy, + ); + }); + + it('Checks that token holder\'s "setup" data is called.', async () => { + const userWalletFactory = await UserWalletFactory.new(); + + const gnosisSafeMasterCopy = accountProvider.get(); + + const tokenHolderMasterCopy = await TokenHolder.new(); + const token = accountProvider.get(); + const tokenRules = accountProvider.get(); + + const blockNumber = await web3.eth.getBlockNumber(); + + const sessionKeyAddress = accountProvider.get(); + const sessionKeySpendingLimit = 11; + const sessionKeyExpirationHeight = blockNumber + 11; + + const sessionKeys = [sessionKeyAddress]; + const sessionKeysSpendingLimits = [sessionKeySpendingLimit]; + const sessionKeysExpirationHeights = [sessionKeyExpirationHeight]; + + const returnData = await userWalletFactory.createUserWallet.call( + gnosisSafeMasterCopy, + '0x', // gnosis safe's setup data + tokenHolderMasterCopy.address, // token holder's master copy + token, // token + tokenRules, // token rules + sessionKeys, // session key addresses + sessionKeysSpendingLimits, // session keys' spending limits + sessionKeysExpirationHeights, // session keys' expiration heights + ); + await userWalletFactory.createUserWallet( + gnosisSafeMasterCopy, + '0x', // gnosis safe's setup data + tokenHolderMasterCopy.address, // token holder's master copy + token, // token + tokenRules, // token rules + sessionKeys, // session key addresses + sessionKeysSpendingLimits, // session keys' spending limits + sessionKeysExpirationHeights, // session keys' expiration heights + ); + + const gnosisSafeProxy = returnData[0]; + const tokenHolderProxy = await TokenHolder.at(returnData[1]); + + assert.strictEqual( + await tokenHolderProxy.token.call(), + token, + ); + + assert.strictEqual( + await tokenHolderProxy.tokenRules.call(), + tokenRules, + ); + + assert.strictEqual( + await tokenHolderProxy.owner.call(), + gnosisSafeProxy, + ); + + const sessionKeyData = await tokenHolderProxy.sessionKeys.call( + sessionKeyAddress, + ); + + assert.isOk( + sessionKeyData.spendingLimit.eqn(sessionKeySpendingLimit), + ); + + assert.isOk( + sessionKeyData.expirationHeight.eqn(sessionKeyExpirationHeight), + ); + + assert.isOk( + sessionKeyData.nonce.eqn(0), + ); + + assert.isOk( + sessionKeyData.session.eqn(2), + ); + }); + }); + + contract('Events', async () => { + it('Checks that UserWalletCreated event is emitted on success.', async () => { + const userWalletFactory = await UserWalletFactory.new(); + + const gnosisSafeMasterCopy = accountProvider.get(); + const tokenHolderMasterCopy = accountProvider.get(); + + const returnData = await userWalletFactory.createUserWallet.call( + gnosisSafeMasterCopy, // gnosis safe's master copy + '0x', // gnosis safe's setup data + tokenHolderMasterCopy, // token holder's master copy + accountProvider.get(), // token + accountProvider.get(), // token rules + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ); + const transactionResponse = await userWalletFactory.createUserWallet( + gnosisSafeMasterCopy, // gnosis safe's master copy + '0x', // gnosis safe's setup data + tokenHolderMasterCopy, // token holder's master copy + accountProvider.get(), // token + accountProvider.get(), // token rules + [], // session key addresses + [], // session keys' spending limits + [], // session keys' expiration heights + ); + + const gnosisSafeProxy = returnData[0]; + const tokenHolderProxy = returnData[1]; + + const events = Event.decodeTransactionResponse( + transactionResponse, + ); + + assert.strictEqual( + events.length, + 1, + ); + + Event.assertEqual(events[0], { + name: 'UserWalletCreated', + args: { + _gnosisSafeProxy: gnosisSafeProxy, + _tokenHolderProxy: tokenHolderProxy, + }, + }); + }); + }); + + contract('Verify call prefix constants', async () => { + it('Verify TOKENHOLDER_SETUP_CALLPREFIX constant', async () => { + const userWalletFactory = await UserWalletFactory.new(); + const tokenHolderSetupCallPrefix = await userWalletFactory.TOKENHOLDER_SETUP_CALLPREFIX(); + const methodName = 'setup'; + + Utils.verifyCallPrefixConstant(methodName, tokenHolderSetupCallPrefix, 'TokenHolder'); + }); + }); +}); diff --git a/tools/compile.sh b/tools/compile.sh deleted file mode 100755 index 10891e2..0000000 --- a/tools/compile.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -CONTRACTDIR=./contracts/*.sol -ABIDIRUTILITY=./contracts/abi -BINDIRVALUE=./contracts/bin - -mkdir -p "$ABIDIRUTILITY" -mkdir -p "$BINDIRVALUE" - -for filename in $CONTRACTDIR; do - echo "" - echo "Compiling ${filename}" - echo "" - solc --abi --optimize --optimize-runs 200 --overwrite ${filename} -o $ABIDIRUTILITY - solc --bin --optimize --optimize-runs 200 --overwrite ${filename} -o $BINDIRVALUE -done \ No newline at end of file diff --git a/tools/runGanacheCli.sh b/tools/runGanacheCli.sh deleted file mode 100755 index a90aed2..0000000 --- a/tools/runGanacheCli.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -ganache-cli \ ---accounts=100 \ ---defaultBalanceEther=100 \ ---gasLimit 0xfffffffffff diff --git a/tools/run_ganache_cli.sh b/tools/run_ganache_cli.sh new file mode 100755 index 0000000..51d9c22 --- /dev/null +++ b/tools/run_ganache_cli.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +script_dir_path="$(cd "$(dirname "$0")" && pwd)" + +"${script_dir_path}/../node_modules/.bin/ganache-cli" \ + --accounts=100 \ + --defaultBalanceEther=100 \ + --gasLimit 0xfffffffffff diff --git a/truffle.js b/truffle.js index 4842a17..173d8b4 100644 --- a/truffle.js +++ b/truffle.js @@ -1,27 +1,22 @@ module.exports = { - networks: { - development: { - host: 'localhost', - network_id: '*', - port: 8545, - gas: 12000000, - gasPrice: 0x01, - }, - }, - coverage: { - host: 'localhost', - network_id: '*', - port: 8555, // <-- If you change this, also set the port option in .solcover.js. - gas: 0xfffffffffff, // <-- Use this high gas value - gasPrice: 0x01, // <-- Use this low gas price + networks: { + development: { + host: 'localhost', + network_id: '*', + port: 8545, + gas: 12000000, + gasPrice: 0x01, }, + }, + compilers: { solc: { + version: '0.5.0', + settings: { optimizer: { - enabled: true, - // set to same number of runs as openst-platform - // so that integration tests - // give accurate gas measurements - runs: 200, + enabled: true, + runs: 200, }, + }, }, + }, };