├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── contracts ├── Migrations.sol ├── SwapyExchange.sol ├── investment │ ├── AssetEvents.sol │ ├── AssetLibrary.sol │ └── InvestmentAsset.sol └── token │ └── Token.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package.json ├── sample.env ├── scripts ├── start_testrpc.sh └── test.sh ├── test ├── AssetCall.sol ├── InvestmentAsset.js ├── SwapyExchange.js ├── TestInvestmentAsset_investment.sol ├── TestInvestmentAsset_marketplace.sol ├── TestSwapyExchange_actions.sol ├── TestSwapyExchange_versioning.sol └── helpers │ ├── ThrowProxy.sol │ ├── ether.js │ ├── increaseTime.js │ ├── web3.js │ └── wei.js └── truffle.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | 5 | *~ 6 | *.sw[mnpcod] 7 | *.log 8 | *.tmp 9 | *.tmp.* 10 | log.txt 11 | *.sublime-project 12 | *.sublime-workspace 13 | .vscode/ 14 | npm-debug.log* 15 | 16 | .idea/ 17 | .tmp/ 18 | .versions/ 19 | dist/ 20 | node_modules/ 21 | tmp/ 22 | temp/ 23 | $RECYCLE.BIN/ 24 | build/ 25 | 26 | .DS_Store 27 | .alm 28 | Thumbs.db 29 | UserInterfaceState.xcuserstate 30 | 31 | .env 32 | 33 | # Test coverage 34 | .nyc_output/ 35 | coverage/ 36 | 37 | # ignore lock files 38 | package-lock.json 39 | yarn.lock -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.9.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | group: beta 4 | language: node_js 5 | node_js: 6 | - '8' 7 | matrix: 8 | fast_finish: true 9 | cache: 10 | directories: 11 | - node_modules 12 | notifications: 13 | slack: 14 | rooms: 15 | secure: EwY70tnAecIFoqJ92WZLzXPDJmfDSiHvMPkbaQpb73aLXO3BgrkTGRXYT9AjgX59inaGrtVvp+EQEJfsiQqX1eog70Su8N3Wc6KOLrOy3cP53sGJepC2Fv/RxGLagxWODh79x0wrRpTpcZ8dc2W5HDPUb/WtLlQN2kfg7OAY7b8CQail6uRBTMBED24c+erTa7a0zwIoUr3BTjVZFBR0sOVpHdd4axe54wkTameMOzHObq85f5K1KmNd63/Nq3qm83wDuib9ZFaXR2aqFA4VD+bfZQuH3UJpIPtybUA65i6CjNS9h0ErW9gVKwjBixbZVU95H6SGDd6PWN6Ng0C+SepP1ejG/b8RXI42DBoheJEh9/R3DxKp1kBAi1CO2EEnkAientqBxHZeIrQhJms4brONoygtfRvWejd9a9phIgr1TCA0znQL+pbPc+Ehm6usb+ewh4iXgtHFV/lPP8pwFs3SaL7hDmWLY3Jt/jKnYE+QgG4ZaQA8MbjqwM+2rirXu4iDbD7fdvywLebkm5HOtSKKbcAgSP3OiqN5HZUUbN+H8+sj2U0kFPNymWQwgS8rOI4JG9sN+fLTz5IrdJG0ly43p4WwHtGDq6ZaKaCUXh65JtWJbSq41oJOd48+lq7XHvPI1fVJ4naEfMDUn5H5QWiW4pPwRkTLuxBImou+kcY= 16 | template: 17 | - ":hammer_and_wrench: Build <%{build_url}|#%{build_number}> %{result} in %{duration}\n :octocat: in PR <%{pull_request_url}|#%{pull_request_number}>\n:scroll: (<%{compare_url}|%{commit}>) - _%{commit_message}_\n:godmode: %{author}" 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swapy Exchange Protocol 2 | #### NOTE: The protocol was previously created on a private repository. Since the beginning, we have thought about open sourcing the protocol as the community has much to contribute to it as well as the protocol and the source code may be helpful to other projects too. As the protocol achieved a certain level of maturity we decided to move it to a public repository under Apache 2.0 License (without its history). We invite you all to join us in our dream of a world in which everyone has ACCESS TO CREDIT. The Swapy team is looking forward to your comments, issues, contributions, derivations, and so on. 3 | [![Join the chat at https://gitter.im/swapynetwork/general](https://badges.gitter.im/swapynetwork/general.svg)](https://gitter.im/swapynetwork/general?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 5 | [![Build Status](https://travis-ci.org/SwapyNetwork/swapy-exchange-protocol.svg?branch=master)](https://travis-ci.org/SwapyNetwork/swapy-exchange-protocol) 6 | 7 | ## Table of Contents 8 | 9 | * [Architecture](#architecture) 10 | * [Contracts](#contracts) 11 | * [Setup](#setup) 12 | 13 | ## Architecture 14 | The diagram below describes the flow of fundraising provided by [Swapy Exchange](https://www.swapy.network/) protocol. 15 | ![architecture-diagram-1](https://www.swapy.network/images/diagrams/1-investment-flow.png) 16 | 17 | ## Contracts 18 | 19 | ### [SwapyExchange.sol](https://github.com/swapynetwork/swapy-exchange-protocol/blob/master/contracts/SwapyExchange.sol) 20 | Credit companies can order investment by using the SwapyExchange contract. It works as a factory of InvestmentAsset contract and organizes the protocol versioning. 21 | 22 | ### [InvestmentAsset.sol](https://github.com/swapynetwork/swapy-exchange-protocol/blob/master/contracts/investment/InvestmentAsset.sol) 23 | InvestmentAsset defines a fundraising asset with its value, investor and agreement terms hash. The business rules is delegated to AssetLibrary. 24 | 25 | ### [AssetLibrary.sol](https://github.com/swapynetwork/swapy-exchange-protocol/blob/master/contracts/investment/AssetLibrary.sol) 26 | AssetLibrary centralizes the logical rules of an investment asset, like invest, cancel, refuse and return. These methods are only accessible by the investor or the credit company, according to its functionalities. 27 | 28 | ## Setup 29 | 30 | Install [Node v8.9.1](https://nodejs.org/en/download/releases/) 31 | 32 | [Truffle](http://truffleframework.com/) is used for deployment. We run the version installed from our dependencies using npm scripts, but if you prefer to install it globally you can do: 33 | ``` 34 | $ npm install -g truffle 35 | ``` 36 | 37 | Install project dependencies: 38 | ``` 39 | $ npm install 40 | ``` 41 | For setup your wallet configuration, addresses and blockchain node provider to deploy, an environment file is necessary. We provide a `sample.env` file. We recommend that you set up your own variables and rename the file to `.env`. 42 | 43 | sample.env 44 | ``` 45 | export NETWORK_ID=... 46 | export DEV_NETWORK_ID=... 47 | export TOKEN_ADDRESS=... 48 | export PROVIDER_URL="https://yourfavoriteprovider.../..." 49 | export WALLET_MNEMONIC="twelve words mnemonic ... potato bread coconut pencil" 50 | ``` 51 | Use your own provider. Some known networks below: 52 | #### NOTE: the current protocol version is not intended to be used on mainnet. 53 | 54 | | Network | Description | URL | 55 | |-----------|--------------------|-----------------------------| 56 | | Mainnet | main network | https://mainnet.infura.io | 57 | | Ropsten | test network | https://ropsten.infura.io | 58 | | INFURAnet | test network | https://infuranet.infura.io | 59 | | Kovan | test network | https://kovan.infura.io | 60 | | Rinkeby | test network | https://rinkeby.infura.io | 61 | | IPFS | gateway | https://ipfs.infura.io | 62 | | Local | Local provider | http://localhost:8545 | 63 | | Etc | ... | ... | 64 | 65 | Use a NETWORK_ID that matches with your network: 66 | * 0: Olympic, Ethereum public pre-release testnet 67 | * 1: Frontier, Homestead, Metropolis, the Ethereum public main network 68 | * 1: Classic, the (un)forked public Ethereum Classic main network, chain ID 61 69 | * 1: Expanse, an alternative Ethereum implementation, chain ID 2 70 | * 2: Morden, the public Ethereum testnet, now Ethereum Classic testnet 71 | * 3: Ropsten, the public cross-client Ethereum testnet 72 | * 4: Rinkeby, the public Geth Ethereum testnet 73 | * 42: Kovan, the public Parity Ethereum testnet 74 | * 7762959: Musicoin, the music blockchain 75 | * etc 76 | 77 | The value of TOKEN_ADDRESS depends on the network. We created the [Swapy Token Faucet](https://github.com/SwapyNetwork/swapy-test-faucet) to distribute an ERC20 Token and support our testnet releases. Follow the repos description to use a deployed Token, e.g., Ropsten and Infura, or make your 78 | own deploy to another network. 79 | 80 | After that, make available your environment file inside the bash context: 81 | ``` 82 | $ source .env 83 | ``` 84 | 85 | By using a local network, this lecture may be useful: [Connecting to the network](https://github.com/ethereum/go-ethereum/wiki/Connecting-to-the-network) 86 | 87 | Compile the contracts with truffle: 88 | ``` 89 | $ npm run compile 90 | ``` 91 | Run our migrations: 92 | ``` 93 | $ npm run migrate 94 | ``` 95 | We're running the contracts in a custom network defined in [truffle.js](https://github.com/swapynetwork/swapy-exchange-protocol/blob/master/truffle.js). 96 | 97 | After the transaction mining, the protocol is disponible for usage. 98 | 99 | We're using Truffle's test support. The script scripts/test.sh creates a local network and calls the unit tests. 100 | 101 | Type 102 | ``` 103 | $ npm test 104 | ``` 105 | and run our tests. 106 | 107 | [Truffle console](https://truffle.readthedocs.io/en/beta/getting_started/console/) can be used to interact with protocol. For example: 108 | ``` 109 | $ truffle console --network custom 110 | ``` 111 | ``` 112 | truffle(custom)> SwapyExchange.deployed().VERSION.call(); // "1.0.0" 113 | ``` 114 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) 9 | _; 10 | } 11 | 12 | constructor() 13 | public 14 | { 15 | owner = msg.sender; 16 | } 17 | 18 | function setCompleted(uint completed) restricted public { 19 | last_completed_migration = completed; 20 | } 21 | 22 | function upgrade(address new_address) restricted public { 23 | Migrations upgraded = Migrations(new_address); 24 | upgraded.setCompleted(last_completed_migration); 25 | } 26 | } -------------------------------------------------------------------------------- /contracts/SwapyExchange.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "./investment/InvestmentAsset.sol"; 4 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 5 | 6 | /** 7 | * @title Swapy Exchange Protocol 8 | * @dev Allows the creation of fundraising offers and many actions with them 9 | */ 10 | contract SwapyExchange { 11 | /** 12 | * Add safety checks for uint operations 13 | */ 14 | using SafeMath for uint256; 15 | 16 | /** 17 | * Constants 18 | */ 19 | bytes8 public latestVersion; 20 | 21 | /** 22 | * Storage 23 | */ 24 | mapping(bytes8 => address) libraries; 25 | address public owner; 26 | address public token; 27 | 28 | /** 29 | * Events 30 | */ 31 | event LogOffers(address indexed _from, bytes8 _protocolVersion, address[] _assets); 32 | event LogInvestments(address indexed _investor, address[] _assets, uint256 _value); 33 | event LogForSale(address indexed _investor, address _asset, uint256 _value); 34 | event LogBought(address indexed _buyer, address _asset, uint256 _value); 35 | event LogVersioning(bytes8 _version, address _library); 36 | event LogReturned(address indexed _from, address[] _assets, uint256[] _values, uint256 _value); 37 | 38 | /** 39 | * Modifiers 40 | */ 41 | modifier notEmpty(address[] _assets){ 42 | require(_assets.length > 0, "Empty list of assets"); 43 | _; 44 | } 45 | 46 | modifier onlyOwner(){ 47 | require(msg.sender == owner); 48 | _; 49 | } 50 | 51 | /** 52 | * @param _token Address of Swapy Token 53 | */ 54 | constructor(address _token, bytes8 _version, address _library) 55 | public 56 | { 57 | token = _token; 58 | owner = msg.sender; 59 | setLibrary(_version, _library); 60 | } 61 | 62 | /** 63 | * @dev add a new protocol library - internal 64 | * @param _version Version 65 | * @param _library Library's address. 66 | */ 67 | function setLibrary( 68 | bytes8 _version, 69 | address _library) 70 | private 71 | { 72 | require(_version != bytes8(0), "Invalid version"); 73 | require(_library != address(0), "Invalid library address"); 74 | require(libraries[_version] == address(0), "Library version already added"); 75 | latestVersion = _version; 76 | libraries[_version] = _library; 77 | emit LogVersioning(_version, _library); 78 | } 79 | 80 | /** 81 | * @dev retrieve library address of a version 82 | * @param _version Version 83 | * @return Address of library 84 | */ 85 | function getLibrary(bytes8 _version) view public returns(address) { 86 | require(libraries[_version] != address(0)); 87 | return libraries[_version]; 88 | } 89 | /** 90 | * @dev add a new protocol library 91 | * @param _version Version 92 | * @param _library Library's address. 93 | * @return Success 94 | */ 95 | function addLibrary( 96 | bytes8 _version, 97 | address _library) 98 | onlyOwner 99 | external 100 | returns(bool) 101 | { 102 | setLibrary(_version, _library); 103 | return true; 104 | } 105 | 106 | /** 107 | * @dev create a fundraising offer 108 | * @param _paybackDays Period in days until the return of investment 109 | * @param _grossReturn Gross return on investment 110 | * @param _currency Fundraising base currency, i.e, USD 111 | * @param _assets Asset's values. 112 | * @return Success 113 | */ 114 | function createOffer( 115 | bytes8 _version, 116 | uint256 _paybackDays, 117 | uint256 _grossReturn, 118 | bytes5 _currency, 119 | uint256[] _assets) 120 | external 121 | returns(address[] newAssets) 122 | { 123 | bytes8 version = _version == bytes8(0) ? latestVersion : _version; 124 | require(libraries[version] != address(0), "Library version doesn't exists"); 125 | newAssets = createOfferAssets(_assets, _currency, _paybackDays, _grossReturn, version); 126 | emit LogOffers(msg.sender, _version, newAssets); 127 | } 128 | 129 | /** 130 | * @dev Create fundraising assets 131 | * @param _assets Asset's values. The length will determine the number of assets composes the fundraising 132 | * @param _currency Fundraising base currency, i.e, USD 133 | * @param _paybackDays Period in days until the return of investment 134 | * @param _grossReturn Gross return on investment 135 | * @return Address of assets created 136 | */ 137 | function createOfferAssets( 138 | uint256[] _assets, 139 | bytes5 _currency, 140 | uint _paybackDays, 141 | uint _grossReturn, 142 | bytes8 _version) 143 | internal 144 | returns (address[]) 145 | { 146 | address[] memory newAssets = new address[](_assets.length); 147 | for (uint index = 0; index < _assets.length; index++) { 148 | newAssets[index] = new InvestmentAsset( 149 | libraries[_version], 150 | this, 151 | msg.sender, 152 | _version, 153 | _currency, 154 | _assets[index], 155 | _paybackDays, 156 | _grossReturn, 157 | token 158 | ); 159 | } 160 | return newAssets; 161 | } 162 | /** 163 | * @dev Invest in fundraising assets 164 | * @param _assets Asset addresses 165 | * @param value Asset unit value, i.e, _assets.length = 5 and msg.value = 5 ETH, then _value must be equal 1 ETH 166 | * @return Success 167 | */ 168 | function invest(address[] _assets, uint256 value) payable 169 | notEmpty(_assets) 170 | external 171 | returns(bool) 172 | { 173 | require( 174 | (value.mul(_assets.length) == msg.value) && value > 0, 175 | "The value transfered doesn't match with the unit value times the number of assets" 176 | ); 177 | for (uint index = 0; index < _assets.length; index++) { 178 | InvestmentAsset asset = InvestmentAsset(_assets[index]); 179 | require(address(asset).call.value(value)(abi.encodeWithSignature("invest(address)",address(msg.sender))), "An error ocurred when investing"); 180 | } 181 | emit LogInvestments(msg.sender, _assets, msg.value); 182 | return true; 183 | } 184 | 185 | /** 186 | * @dev Withdraw investments 187 | * @param _assets Asset addresses 188 | * @return Success 189 | */ 190 | function withdrawFunds(address[] _assets) 191 | notEmpty(_assets) 192 | external 193 | returns(bool) 194 | { 195 | for(uint index = 0; index < _assets.length; index++){ 196 | InvestmentAsset asset = InvestmentAsset(_assets[index]); 197 | require(msg.sender == asset.owner(), "The user isn't asset's owner"); 198 | require(address(asset).call(abi.encodeWithSignature("withdrawFunds()")), "An error ocurred when withdrawing asset's funds"); 199 | } 200 | return true; 201 | } 202 | 203 | /** 204 | * @dev Refuse investments 205 | * @param _assets Asset addresses 206 | * @return Success 207 | */ 208 | function refuseInvestment(address[] _assets) 209 | notEmpty(_assets) 210 | external 211 | returns(bool) 212 | { 213 | for(uint index = 0; index < _assets.length; index++){ 214 | InvestmentAsset asset = InvestmentAsset(_assets[index]); 215 | require(msg.sender == asset.owner(), "The user isn't asset's owner"); 216 | require(address(asset).call(abi.encodeWithSignature("refuseInvestment()")), "An error ocurred when refusing investment"); 217 | } 218 | return true; 219 | } 220 | 221 | /** 222 | * @dev Cancel investments made 223 | * @param _assets Asset addresses 224 | * @return Success 225 | */ 226 | function cancelInvestment(address[] _assets) 227 | notEmpty(_assets) 228 | external 229 | returns(bool) 230 | { 231 | for(uint index = 0; index < _assets.length; index++){ 232 | InvestmentAsset asset = InvestmentAsset(_assets[index]); 233 | require(msg.sender == asset.investor(), "The user isn't asset's investor"); 234 | require(address(asset).call(abi.encodeWithSignature("cancelInvestment()")), "An error ocurred when canceling investment"); 235 | } 236 | return true; 237 | } 238 | 239 | /** 240 | * @dev Put invested assets for sale. 241 | * @param _assets Asset addresses 242 | * @param _values Sale values. _assets[0] => _values[0], ..., _assets[n] => _values[n] 243 | * @return Success 244 | */ 245 | function sellAssets(address[] _assets, uint256[] _values) 246 | notEmpty(_assets) 247 | external 248 | returns(bool) 249 | { 250 | require(_assets.length == _values.length, "All the assets should have a value on sale"); 251 | for(uint index = 0; index < _assets.length; index++){ 252 | InvestmentAsset asset = InvestmentAsset(_assets[index]); 253 | require(msg.sender == asset.investor(), "The user isn't asset's investor"); 254 | require(address(asset).call(abi.encodeWithSignature("sell(uint256)",_values[index])), "An error ocurred when puting the asset on sale"); 255 | emit LogForSale(msg.sender, _assets[index], _values[index]); 256 | } 257 | return true; 258 | } 259 | 260 | /** 261 | * @dev Remove available assets from market place 262 | * @param _assets Asset addresses 263 | * @return Success 264 | */ 265 | function cancelSellOrder(address[] _assets) 266 | notEmpty(_assets) 267 | external 268 | returns(bool) 269 | { 270 | for(uint index = 0; index < _assets.length; index++){ 271 | InvestmentAsset asset = InvestmentAsset(_assets[index]); 272 | require(msg.sender == asset.investor(), "The user isn't asset's investor"); 273 | require(address(asset).call(abi.encodeWithSignature("cancelSellOrder()")), "An error ocurred when removing the asset from market place"); 274 | } 275 | return true; 276 | } 277 | 278 | /** 279 | * @dev Buy an available asset on market place 280 | * @param _asset Asset address 281 | * @return Success 282 | */ 283 | function buyAsset(address _asset) payable 284 | external 285 | returns(bool) 286 | { 287 | uint256 assetValue = msg.value; 288 | InvestmentAsset asset = InvestmentAsset(_asset); 289 | require(address(asset).call.value(assetValue)(abi.encodeWithSignature("buy(address)",msg.sender)), "An error ocurred when buying the asset"); 290 | emit LogBought(msg.sender, _asset, msg.value); 291 | return true; 292 | } 293 | 294 | /** 295 | * @dev Accept purchases on market place 296 | * @param _assets Asset addresses 297 | * @return Success 298 | */ 299 | function acceptSale(address[] _assets) 300 | notEmpty(_assets) 301 | external 302 | returns(bool) 303 | { 304 | for(uint index = 0; index < _assets.length; index++) { 305 | InvestmentAsset asset = InvestmentAsset(_assets[index]); 306 | require(msg.sender == asset.investor(), "The user isn't asset's investor"); 307 | require(address(asset).call(abi.encodeWithSignature("acceptSale()")), "An error ocurred when accepting sale"); 308 | } 309 | return true; 310 | } 311 | 312 | /** 313 | * @dev Refuse purchases on market place 314 | * @param _assets Asset addresses 315 | * @return Success 316 | */ 317 | function refuseSale(address[] _assets) 318 | notEmpty(_assets) 319 | external 320 | returns(bool) 321 | { 322 | for(uint index = 0; index < _assets.length; index++) { 323 | InvestmentAsset asset = InvestmentAsset(_assets[index]); 324 | require(msg.sender == asset.investor(), "The user isn't asset's investor"); 325 | require(address(asset).call(abi.encodeWithSignature("refuseSale()")), "An error ocurred when refusing sale"); 326 | } 327 | return true; 328 | } 329 | 330 | /** 331 | * @dev Cancel purchases made 332 | * @param _assets Asset addresses 333 | * @return Success 334 | */ 335 | function cancelSale(address[] _assets) 336 | notEmpty(_assets) 337 | external 338 | returns(bool) 339 | { 340 | for(uint index = 0; index < _assets.length; index++) { 341 | InvestmentAsset asset = InvestmentAsset(_assets[index]); 342 | address buyer; 343 | (,buyer) = asset.sellData(); 344 | require(msg.sender == buyer, "The user isn't asset's buyer"); 345 | require(address(asset).call(abi.encodeWithSignature("cancelSale()")), "An error ocurred when canceling sale"); 346 | } 347 | return true; 348 | } 349 | 350 | /** 351 | * @dev Require collateral of investments made 352 | * @param _assets Asset addresses 353 | * @return Success 354 | */ 355 | function requireTokenFuel(address[] _assets) 356 | notEmpty(_assets) 357 | external 358 | returns(bool) 359 | { 360 | for(uint index = 0; index < _assets.length; index++) { 361 | InvestmentAsset asset = InvestmentAsset(_assets[index]); 362 | require(msg.sender == asset.investor(), "The user isn't asset's investor"); 363 | require(address(asset).call(abi.encodeWithSignature("requireTokenFuel()")), "An error ocurred when requiring asset's collateral"); 364 | } 365 | return true; 366 | } 367 | 368 | /** 369 | * @dev Return of investments 370 | * @param _assets Asset addresses 371 | * @param _values Asset's return values 372 | * @return Success 373 | */ 374 | function returnInvestment(address[] _assets, uint256[] _values) payable 375 | notEmpty(_assets) 376 | external 377 | returns(bool) 378 | { 379 | uint256 total; 380 | for (uint index = 0; index < _values.length; index++) { 381 | total = total.add(_values[index]); 382 | } 383 | require((total == msg.value) && total > 0); 384 | for (index = 0; index < _assets.length; index++) { 385 | InvestmentAsset asset = InvestmentAsset(_assets[index]); 386 | require(msg.sender == asset.owner()); 387 | require( 388 | address(asset).call.value(_values[index])(abi.encodeWithSignature("returnInvestment()")), 389 | "An error ocurred when returning investment" 390 | ); 391 | } 392 | emit LogReturned(msg.sender, _assets, _values, msg.value); 393 | return true; 394 | } 395 | 396 | } 397 | -------------------------------------------------------------------------------- /contracts/investment/AssetEvents.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | /** 4 | * @title Asset Events 5 | * @dev Defines events fired by fundraising assets 6 | */ 7 | contract AssetEvents { 8 | 9 | /** 10 | * Events 11 | */ 12 | event LogInvested(address _owner, address _investor, uint256 _value); 13 | event LogCanceled(address _owner, address _investor, uint256 _value); 14 | event LogWithdrawal(address _owner, address _investor, uint256 _value); 15 | event LogRefused(address _owner, address _investor, uint256 _value); 16 | event LogReturned(address _owner, address _investor, uint256 _value, bool _delayed); 17 | event LogSupplied(address _owner, uint256 _amount, uint256 _assetFuel); 18 | event LogTokenWithdrawal(address _to, uint256 _amount); 19 | event LogForSale(address _investor, uint256 _value); 20 | event LogCanceledSell(address _investor, uint256 _value); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /contracts/investment/AssetLibrary.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "./AssetEvents.sol"; 4 | import "../token/Token.sol"; 5 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 6 | 7 | /** 8 | * @title Asset Library 9 | * @dev Defines the behavior of a fundraising asset. Designed to receive InvestmentAsset's calls and work on its storage 10 | */ 11 | contract AssetLibrary is AssetEvents { 12 | /** 13 | * Add safety checks for uint operations 14 | */ 15 | using SafeMath for uint256; 16 | 17 | /** 18 | * Storage 19 | */ 20 | // Asset owner 21 | address public owner; 22 | // Protocol 23 | address public protocol; 24 | // Asset currency 25 | bytes5 public currency; 26 | // Asset fixed value 27 | uint256 public value; 28 | //Value bought 29 | uint256 public boughtValue; 30 | // period to return the investment 31 | uint256 public paybackDays; 32 | // Gross return of investment 33 | uint256 public grossReturn; 34 | // Asset buyer 35 | address public investor; 36 | // Protocol version 37 | bytes8 public protocolVersion; 38 | // investment timestamp 39 | uint public investedAt; 40 | 41 | // asset fuel 42 | Token public token; 43 | uint256 public tokenFuel; 44 | 45 | // sale structure 46 | struct Sell { 47 | uint256 value; 48 | address buyer; 49 | } 50 | 51 | Sell public sellData; 52 | 53 | // possible stages of an asset 54 | enum Status { 55 | AVAILABLE, 56 | PENDING_OWNER_AGREEMENT, 57 | INVESTED, 58 | FOR_SALE, 59 | PENDING_INVESTOR_AGREEMENT, 60 | RETURNED, 61 | DELAYED_RETURN 62 | } 63 | Status public status; 64 | 65 | /** 66 | * Modifiers 67 | */ 68 | // Checks the current asset's status 69 | modifier hasStatus(Status _status) { 70 | assert(status == _status); 71 | _; 72 | } 73 | // Checks if the owner is the caller 74 | modifier onlyOwner() { 75 | require(msg.sender == owner, "The user isn't the owner"); 76 | _; 77 | } 78 | // Checks if the investor is the caller 79 | modifier onlyInvestor() { 80 | require(msg.sender == investor, "The user isn't the investor"); 81 | _; 82 | } 83 | modifier protocolOrInvestor() { 84 | require(msg.sender == protocol || msg.sender == investor, "The user isn't the protocol or investor"); 85 | _; 86 | } 87 | modifier protocolOrOwner() { 88 | require(msg.sender == protocol || msg.sender == owner, "The user isn't the protocol or owner"); 89 | _; 90 | } 91 | modifier isValidAddress(address _addr) { 92 | require(_addr != address(0), "Invalid address"); 93 | _; 94 | } 95 | 96 | modifier onlyDelayed(){ 97 | require(isDelayed(), "The return of investment isn't dalayed"); 98 | _; 99 | } 100 | 101 | /** 102 | * @dev Supply collateral tokens to the asset 103 | * @param _amount Token amount 104 | * @return Success 105 | */ 106 | function supplyFuel(uint256 _amount) 107 | onlyOwner 108 | hasStatus(Status.AVAILABLE) 109 | external 110 | returns(bool) 111 | { 112 | require(token.transferFrom(msg.sender, this, _amount), "An error ocurred when sending tokens"); 113 | tokenFuel = tokenFuel.add(_amount); 114 | emit LogSupplied(owner, _amount, tokenFuel); 115 | return true; 116 | } 117 | 118 | /** 119 | * @dev Add investment interest and retain pending funds within the asset 120 | * @param _investor Pending Investor 121 | * @return Success 122 | */ 123 | function invest(address _investor) payable 124 | isValidAddress(_investor) 125 | hasStatus(Status.AVAILABLE) 126 | external 127 | returns(bool) 128 | { 129 | status = Status.PENDING_OWNER_AGREEMENT; 130 | investor = _investor; 131 | investedAt = now; 132 | emit LogInvested(owner, investor, address(this).balance); 133 | return true; 134 | } 135 | 136 | /** 137 | * @dev Cancel a pending investment made 138 | * @return Success 139 | */ 140 | function cancelInvestment() 141 | protocolOrInvestor 142 | hasStatus(Status.PENDING_OWNER_AGREEMENT) 143 | external 144 | returns(bool) 145 | { 146 | address currentInvestor; 147 | uint256 investedValue; 148 | (currentInvestor, investedValue) = makeAvailable(); 149 | emit LogCanceled(owner, currentInvestor, investedValue); 150 | return true; 151 | } 152 | 153 | /** 154 | * @dev Refuse a pending investment 155 | * @return Success 156 | */ 157 | function refuseInvestment() 158 | protocolOrOwner 159 | hasStatus(Status.PENDING_OWNER_AGREEMENT) 160 | external 161 | returns(bool) 162 | { 163 | address currentInvestor; 164 | uint256 investedValue; 165 | (currentInvestor, investedValue) = makeAvailable(); 166 | emit LogRefused(owner, currentInvestor, investedValue); 167 | return true; 168 | } 169 | 170 | /** 171 | * @dev Accept the investor as the asset buyer and withdraw funds 172 | * @return Success 173 | */ 174 | function withdrawFunds() 175 | protocolOrOwner 176 | hasStatus(Status.PENDING_OWNER_AGREEMENT) 177 | external 178 | returns(bool) 179 | { 180 | status = Status.INVESTED; 181 | uint256 _value = address(this).balance; 182 | owner.transfer(_value); 183 | emit LogWithdrawal(owner, investor, _value); 184 | return true; 185 | } 186 | 187 | /** 188 | * @dev Put this asset for sale. 189 | * @param _sellValue Sale value 190 | * @return Success 191 | */ 192 | function sell(uint256 _sellValue) 193 | protocolOrInvestor 194 | hasStatus(Status.INVESTED) 195 | external 196 | returns(bool) 197 | { 198 | status = Status.FOR_SALE; 199 | sellData.value = _sellValue; 200 | emit LogForSale(msg.sender, _sellValue); 201 | return true; 202 | } 203 | 204 | /** 205 | * @dev Remove the asset from market place 206 | * @return Success 207 | */ 208 | function cancelSellOrder() 209 | protocolOrInvestor 210 | hasStatus(Status.FOR_SALE) 211 | external 212 | returns(bool) 213 | { 214 | status = Status.INVESTED; 215 | sellData.value = uint256(0); 216 | emit LogCanceledSell(investor, value); 217 | return true; 218 | } 219 | 220 | /** 221 | * @dev Buy the asset on market place 222 | * @param _buyer Address of pending buyer 223 | * @return Success 224 | */ 225 | function buy(address _buyer) payable 226 | isValidAddress(_buyer) 227 | hasStatus(Status.FOR_SALE) 228 | external 229 | returns(bool) 230 | { 231 | status = Status.PENDING_INVESTOR_AGREEMENT; 232 | sellData.buyer = _buyer; 233 | emit LogInvested(investor, _buyer, msg.value); 234 | return true; 235 | } 236 | 237 | /** 238 | * @dev Cancel a purchase made 239 | * @return Success 240 | */ 241 | function cancelSale() 242 | hasStatus(Status.PENDING_INVESTOR_AGREEMENT) 243 | external 244 | returns(bool) 245 | { 246 | require(msg.sender == protocol || msg.sender == sellData.buyer, "The user isn't the protocol or buyer"); 247 | status = Status.FOR_SALE; 248 | address buyer = sellData.buyer; 249 | uint256 _value = address(this).balance; 250 | sellData.buyer = address(0); 251 | buyer.transfer(_value); 252 | emit LogCanceled(investor, buyer, _value); 253 | return true; 254 | } 255 | 256 | /** 257 | * @dev Refuse purchase on market place and refunds the pending buyer 258 | * @return Success 259 | */ 260 | function refuseSale() 261 | protocolOrInvestor 262 | hasStatus(Status.PENDING_INVESTOR_AGREEMENT) 263 | external 264 | returns(bool) 265 | { 266 | status = Status.FOR_SALE; 267 | address buyer = sellData.buyer; 268 | uint256 _value = address(this).balance; 269 | sellData.buyer = address(0); 270 | buyer.transfer(_value); 271 | emit LogRefused(investor, buyer, _value); 272 | return true; 273 | } 274 | 275 | /** 276 | * @dev Accept purchase. Withdraw funds, clear the sell data and change investor 277 | * @return Success 278 | */ 279 | function acceptSale() 280 | protocolOrInvestor 281 | hasStatus(Status.PENDING_INVESTOR_AGREEMENT) 282 | external 283 | returns(bool) 284 | { 285 | status = Status.INVESTED; 286 | address currentInvestor = investor; 287 | uint256 _value = address(this).balance; 288 | investor = sellData.buyer; 289 | boughtValue = sellData.value; 290 | sellData.buyer = address(0); 291 | sellData.value = uint256(0); 292 | currentInvestor.transfer(_value); 293 | emit LogWithdrawal(currentInvestor, investor, _value); 294 | return true; 295 | } 296 | 297 | /** 298 | * @dev Require collateral tokens of the investment made 299 | * @return Success 300 | */ 301 | function requireTokenFuel() 302 | protocolOrInvestor 303 | hasStatus(Status.INVESTED) 304 | onlyDelayed 305 | external 306 | returns(bool) 307 | { 308 | return withdrawTokens(investor, tokenFuel); 309 | } 310 | 311 | /** 312 | * @dev Return investment. Refunds pending buyer if the asset is for sale and handle remaining collateral tokens according to 313 | * the period of return 314 | * @return Success 315 | */ 316 | function returnInvestment() payable 317 | protocolOrOwner 318 | external 319 | returns(bool) 320 | { 321 | assert(status == Status.INVESTED || status == Status.FOR_SALE || status == Status.PENDING_INVESTOR_AGREEMENT); 322 | Status currentStatus = status; 323 | bool _isDelayed = isDelayed(); 324 | status = _isDelayed ? Status.DELAYED_RETURN : Status.RETURNED; 325 | if(tokenFuel > 0){ 326 | address recipient = _isDelayed ? investor : owner; 327 | withdrawTokens(recipient, tokenFuel); 328 | } 329 | if (currentStatus == Status.PENDING_INVESTOR_AGREEMENT) { 330 | sellData.buyer.transfer(address(this).balance.sub(msg.value)); 331 | } 332 | investor.transfer(msg.value); 333 | emit LogReturned(owner, investor, msg.value, _isDelayed); 334 | return true; 335 | } 336 | 337 | /** 338 | * @dev Refund investor, clear investment values and become available 339 | * @return A tuple with the old investor and the refunded value 340 | */ 341 | function makeAvailable() 342 | private 343 | returns(address, uint256) 344 | { 345 | status = Status.AVAILABLE; 346 | uint256 investedValue = address(this).balance; 347 | address currentInvestor = investor; 348 | investor = address(0); 349 | investedAt = uint(0); 350 | currentInvestor.transfer(investedValue); 351 | return (currentInvestor, investedValue); 352 | } 353 | 354 | /** 355 | * @dev Withdraw collateral tokens 356 | * @param _recipient Address to send tokens 357 | * @param _amount Tokens amount 358 | * @return Success 359 | */ 360 | function withdrawTokens(address _recipient, uint256 _amount) 361 | private 362 | returns(bool) 363 | { 364 | assert(tokenFuel >= _amount); 365 | tokenFuel = tokenFuel.sub(_amount); 366 | require(token.transfer(_recipient, _amount), "An error ocurred in tokens transfer"); 367 | emit LogTokenWithdrawal(_recipient, _amount); 368 | return true; 369 | } 370 | 371 | /** 372 | * @dev Returns true if the return of investment is delayed according to the investment date and payback period 373 | * @return Delay verification 374 | */ 375 | function isDelayed() 376 | view 377 | internal 378 | returns(bool) 379 | { 380 | return now > investedAt + paybackDays * 1 days; 381 | } 382 | } 383 | 384 | -------------------------------------------------------------------------------- /contracts/investment/InvestmentAsset.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "../token/Token.sol"; 4 | 5 | /** 6 | * @title Investment Asset 7 | * @dev Defines a fundraising asset and its properties 8 | */ 9 | contract InvestmentAsset { 10 | 11 | /** 12 | * Storage 13 | */ 14 | // Asset owner 15 | address public owner; 16 | // Protocol 17 | address public protocol; 18 | // Asset currency 19 | bytes5 public currency; 20 | // Asset value 21 | uint256 public value; 22 | //Value bought 23 | uint256 public boughtValue; 24 | // period to return the investment 25 | uint256 public paybackDays; 26 | // Gross return of investment 27 | uint256 public grossReturn; 28 | // Asset buyer 29 | address public investor; 30 | // Protocol version 31 | bytes8 public protocolVersion; 32 | // investment timestamp 33 | uint public investedAt; 34 | 35 | // Fuel 36 | Token public token; 37 | uint256 public tokenFuel; 38 | 39 | // sale structure 40 | struct Sell { 41 | uint256 value; 42 | address buyer; 43 | } 44 | Sell public sellData; 45 | 46 | // possible stages of an asset 47 | enum Status { 48 | AVAILABLE, 49 | PENDING_OWNER_AGREEMENT, 50 | INVESTED, 51 | FOR_SALE, 52 | PENDING_INVESTOR_AGREEMENT, 53 | RETURNED, 54 | DELAYED_RETURN 55 | } 56 | Status public status; 57 | 58 | // Library to delegate calls 59 | address public assetLibrary; 60 | 61 | /** 62 | * @param _library Address of library that contains asset's logic 63 | * @param _protocol Swapy Exchange Protocol address 64 | * @param _owner Fundraising owner 65 | * @param _protocolVersion Version of Swapy Exchange protocol 66 | * @param _currency Fundraising base currency, i.e, USD 67 | * @param _value Asset value 68 | * @param _paybackDays Period in days until the return of investment 69 | * @param _grossReturn Gross return on investment 70 | * @param _token Collateral Token address 71 | */ 72 | constructor( 73 | address _library, 74 | address _protocol, 75 | address _owner, 76 | bytes8 _protocolVersion, 77 | bytes5 _currency, 78 | uint256 _value, 79 | uint _paybackDays, 80 | uint _grossReturn, 81 | address _token) 82 | public 83 | { 84 | assetLibrary = _library; 85 | protocol = _protocol; 86 | owner = _owner; 87 | protocolVersion = _protocolVersion; 88 | currency = _currency; 89 | value = _value; 90 | boughtValue = 0; 91 | paybackDays = _paybackDays; 92 | grossReturn = _grossReturn; 93 | status = Status.AVAILABLE; 94 | tokenFuel = 0; 95 | token = Token(_token); 96 | } 97 | 98 | /** 99 | * @dev Returns asset's properties as a tuple 100 | * @return A tuple with asset's properties 101 | */ 102 | function getAsset() 103 | external 104 | constant 105 | returns(address, bytes5, uint256, uint256, uint256, Status, address, bytes8, uint, uint256, address, uint256, uint256) 106 | { 107 | return (owner, currency, value, paybackDays, grossReturn, status, investor, protocolVersion, investedAt, tokenFuel, sellData.buyer, sellData.value, boughtValue); 108 | } 109 | 110 | /** 111 | * @dev Fallback function. Used to delegate calls to the library 112 | */ 113 | function () payable 114 | external 115 | { 116 | require(assetLibrary.delegatecall(msg.data), "An error ocurred when calling library"); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /contracts/token/Token.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol"; 4 | 5 | contract Token is MintableToken { 6 | 7 | string public constant name = "SWAPY"; 8 | string public constant symbol = "SWAPY"; 9 | uint8 public constant decimals = 18; 10 | 11 | } -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | // --- Contracts 2 | let SwapyExchange = artifacts.require("./SwapyExchange.sol") 3 | let AssetLibrary = artifacts.require("./investment/AssetLibrary.sol") 4 | let Token = artifacts.require("./token/Token.sol") 5 | 6 | module.exports = function(deployer, network, accounts) { 7 | if(network.indexOf('test') > -1) { 8 | deployer.deploy(Token).then(() => { 9 | return deployer.deploy(AssetLibrary).then(() => { 10 | return deployer.deploy(SwapyExchange, Token.address, "1.0.0", AssetLibrary.address) 11 | }) 12 | }) 13 | }else { 14 | deployer.deploy(AssetLibrary).then(() => { 15 | return deployer.deploy(SwapyExchange, process.env.TOKEN_ADDRESS, "1.0.0", AssetLibrary.address) 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swapy-exchange-protocol", 3 | "version": "1.0.0", 4 | "description": "Swapy Exchange smart contracts", 5 | "main": "truffle.js", 6 | "scripts": { 7 | "test": "scripts/test.sh", 8 | "test.win": "sh scripts/test.sh", 9 | "testrpc": "sh scripts/start_testrpc.sh", 10 | "compile": "truffle compile", 11 | "migrate": "truffle migrate --network custom", 12 | "migrate.hard": "truffle migrate --network custom --reset", 13 | "migrate.dev": "truffle migrate --network dev", 14 | "migrate.dev.hard": "truffle migrate --network dev --reset" 15 | }, 16 | "dependencies": { 17 | "bip39": "^2.4.0", 18 | "chai": "^4.1.2", 19 | "chai-as-promised": "^7.1.1", 20 | "chai-bignumber": "^2.0.2", 21 | "ethereumjs-wallet": "^0.6.0", 22 | "ganache-cli": "^6.1.0", 23 | "truffle": "4.1.7", 24 | "truffle-hdwallet-provider": "0.0.3", 25 | "web3": "^0.18.4", 26 | "web3-provider-engine": "^8.6.1", 27 | "openzeppelin-solidity": "^1.9.0" 28 | }, 29 | "devDependencies": {}, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/swapynetwork/swapy-exchange-protocol.git" 33 | }, 34 | "keywords": [ 35 | "solidity", 36 | "ethereum" 37 | ], 38 | "author": "", 39 | "license": "Apache-2.0", 40 | "bugs": { 41 | "url": "https://github.com/swapynetwork/swapy-exchange-protocol/issues" 42 | }, 43 | "homepage": "https://github.com/swapynetwork/swapy-exchange-protocol#readme" 44 | } 45 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | export NETWORK_ID=... 2 | export DEV_NETWORK_ID=... 3 | export PROVIDER_URL="https://yourfavoriteprovider.../..." 4 | export TOKEN_ADDRESS=... 5 | export WALLET_MNEMONIC="twelve words mnemonic ... potato bread coconut pencil" 6 | -------------------------------------------------------------------------------- /scripts/start_testrpc.sh: -------------------------------------------------------------------------------- 1 | testrpc_port=8545 2 | ganache-cli --network-id "${DEV_NETWORK_ID}" --gasLimit 0xfffffffffff --g 100000000000 -m "${WALLET_MNEMONIC}" --port "$testrpc_port" 3 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit script as soon as a command fails. 4 | set -o errexit 5 | 6 | # Executes cleanup function at script exit. 7 | trap cleanup EXIT 8 | 9 | cleanup() { 10 | # Kill the testrpc instance that we started (if we started one and if it's still running). 11 | if [ -n "$testrpc_pid" ] && ps -p $testrpc_pid > /dev/null; then 12 | kill -9 $testrpc_pid 13 | fi 14 | } 15 | 16 | testrpc_port=8545 17 | 18 | start_testrpc() { 19 | # We define 10 accounts with balance 1M ether, needed for high-value tests. 20 | local accounts=( 21 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200,1000000000000000000000000000000" 22 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201,1000000000000000000000000000000" 23 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202,1000000000000000000000000000000" 24 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203,1000000000000000000000000000000" 25 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204,1000000000000000000000000000000" 26 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205,1000000000000000000000000000000" 27 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206,1000000000000000000000000000000" 28 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000000000" 29 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000000000" 30 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000000000" 31 | ) 32 | 33 | node_modules/.bin/ganache-cli "${accounts[@]}" --gasLimit 79844520 --g 100000000000 > /dev/null & 34 | 35 | testrpc_pid=$! 36 | } 37 | 38 | start_testrpc 39 | 40 | node_modules/.bin/truffle test --network test "$@" 41 | -------------------------------------------------------------------------------- /test/AssetCall.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "../contracts/investment/InvestmentAsset.sol"; 4 | 5 | contract AssetCall { 6 | 7 | address self; 8 | 9 | constructor(address assetAddress){ 10 | self = assetAddress; 11 | } 12 | 13 | function () payable public { 14 | 15 | } 16 | 17 | function cancelInvestment() public returns(bool) { 18 | return self.call(abi.encodeWithSignature("cancelInvestment()")); 19 | } 20 | 21 | function invest(bool invalid) payable returns(bool) { 22 | if(invalid) { 23 | return self.call.value(msg.value)(abi.encodeWithSignature("invest(address)", address(0))); 24 | }else{ 25 | return self.call.value(msg.value)(abi.encodeWithSignature("invest(address)", address(this))); 26 | } 27 | } 28 | 29 | function sell(uint256 value) public returns(bool) { 30 | return self.call(abi.encodeWithSignature("sell(uint256)", value)); 31 | } 32 | 33 | function cancelSellOrder() returns(bool) { 34 | return self.call(abi.encodeWithSignature("cancelSellOrder()")); 35 | } 36 | 37 | function buy(bool invalid) payable returns(bool) { 38 | if(invalid) { 39 | return self.call.value(msg.value)(abi.encodeWithSignature("buy(address)", address(0))); 40 | }else{ 41 | return self.call.value(msg.value)(abi.encodeWithSignature("buy(address)", address(this))); 42 | } 43 | } 44 | 45 | function cancelSale() public returns(bool) { 46 | return self.call(abi.encodeWithSignature("cancelSale()")); 47 | } 48 | 49 | function refuseSale() public returns(bool) { 50 | return self.call(abi.encodeWithSignature("refuseSale()")); 51 | } 52 | 53 | function acceptSale() public returns(bool) { 54 | return self.call(abi.encodeWithSignature("acceptSale()")); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/InvestmentAsset.js: -------------------------------------------------------------------------------- 1 | 2 | // helpers 3 | const increaseTime = require('./helpers/increaseTime') 4 | const { getBalance, getGasPrice } = require('./helpers/web3') 5 | const ether = require('./helpers/ether') 6 | 7 | const BigNumber = web3.BigNumber 8 | const should = require('chai') 9 | .use(require('chai-as-promised')) 10 | .should() 11 | const expect = require('chai').expect 12 | 13 | 14 | // --- Handled contracts 15 | const SwapyExchange = artifacts.require("./SwapyExchange.sol") 16 | const AssetLibrary = artifacts.require("./investment/AssetLibrary.sol") 17 | const InvestmentAsset = artifacts.require("./investment/InvestmentAsset.sol") 18 | const Token = artifacts.require("./token/Token.sol") 19 | 20 | // --- Test constants 21 | const payback = new BigNumber(12) 22 | const grossReturn = new BigNumber(500) 23 | const assetValue = ether(5) 24 | // returned value = invested value + return on investment 25 | const returnValue = new BigNumber(1 + grossReturn.toNumber()/10000).times(assetValue) 26 | const assets = [500,500,500,500,500] 27 | const offerFuel = new BigNumber(5000) 28 | const assetFuel = offerFuel.dividedBy(new BigNumber(assets.length)) 29 | const currency = "USD" 30 | // asset status 31 | const AVAILABLE = new BigNumber(0) 32 | const PENDING_OWNER_AGREEMENT = new BigNumber(1) 33 | const INVESTED = new BigNumber(2) 34 | const FOR_SALE = new BigNumber(3) 35 | const PENDING_INVESTOR_AGREEMENT = new BigNumber(4) 36 | const RETURNED = new BigNumber(5) 37 | const DELAYED_RETURN = new BigNumber(6) 38 | 39 | // --- Test variables 40 | // Contracts 41 | let token = null 42 | let library = null 43 | let protocol = null 44 | // Assets 45 | let assetsAddress = [] 46 | let firstAsset = null 47 | let secondAsset = null 48 | let thirdAsset = null 49 | let fourthAsset = null 50 | // Agents 51 | let investor = null 52 | let creditCompany = null 53 | let Swapy = null 54 | let secondInvestor = null 55 | // Util 56 | let gasPrice = null 57 | let sellValue = null 58 | 59 | contract('InvestmentAsset', accounts => { 60 | 61 | before(async() => { 62 | 63 | // Get accounts and init contracts 64 | Swapy = accounts[0] 65 | creditCompany = accounts[1] 66 | investor = accounts[2] 67 | secondInvestor = accounts[3] 68 | gasPrice = await getGasPrice() 69 | gasPrice = new BigNumber(gasPrice) 70 | const library = await AssetLibrary.new({ from: Swapy }) 71 | token = await Token.new({from: Swapy}) 72 | protocol = await SwapyExchange.new( token.address, "1.0.0", library.address, { from: Swapy }) 73 | await token.mint(creditCompany, offerFuel, {from: Swapy}) 74 | 75 | // Creating assets by the protocol 76 | const {logs} = await protocol.createOffer( 77 | "1.0.0", 78 | payback, 79 | grossReturn, 80 | currency, 81 | assets, 82 | { from: creditCompany } 83 | ) 84 | const event = logs.find(e => e.event === 'LogOffers') 85 | const args = event.args 86 | assetsAddress = args._assets 87 | 88 | // payback and sell constants 89 | const periodAfterInvestment = new BigNumber(1/2) 90 | await increaseTime(86400 * payback * periodAfterInvestment.toNumber()) 91 | const returnOnPeriod = returnValue.minus(assetValue).times(periodAfterInvestment) 92 | sellValue = assetValue.plus(returnOnPeriod) 93 | }) 94 | 95 | 96 | it('should return the asset when calling getAsset', async () => { 97 | const asset = await InvestmentAsset.at(assetsAddress[0]) 98 | const assetValues = await asset.getAsset() 99 | assert.equal(assetValues.length, 13, "The asset must have 13 variables") 100 | assert.equal(assetValues[0], creditCompany, "The asset owner must be the creditCompany") 101 | }) 102 | 103 | context('Token Supply', () => { 104 | it("should supply tokens as fuel to the first asset", async () => { 105 | await token.approve(assetsAddress[1], assetFuel, {from: creditCompany}) 106 | firstAsset = await AssetLibrary.at(assetsAddress[1]) 107 | const {logs} = await firstAsset.supplyFuel( 108 | assetFuel, 109 | {from: creditCompany} 110 | ) 111 | const event = logs.find(e => e.event === 'LogSupplied') 112 | const args = event.args 113 | expect(args).to.include.all.keys([ 114 | '_owner', 115 | '_amount', 116 | '_assetFuel' 117 | ]) 118 | }) 119 | }) 120 | 121 | context('Invest', () => { 122 | it('should add an investment by using the asset', async () => { 123 | const previousAssetBalance = await getBalance(firstAsset.address) 124 | const previousInvestorBalance = await getBalance(investor) 125 | const {logs, receipt} = await firstAsset.invest( 126 | investor, 127 | {value: assetValue, from: investor} 128 | ) 129 | const currentAssetBalance = await getBalance(firstAsset.address) 130 | const currentInvestorBalance = await getBalance(investor) 131 | const gasUsed = new BigNumber(receipt.gasUsed) 132 | const event = logs.find(e => e.event === 'LogInvested') 133 | const args = event.args 134 | expect(args).to.include.all.keys([ 135 | '_owner', 136 | '_investor', 137 | '_value', 138 | ]) 139 | currentInvestorBalance.toNumber().should.equal( 140 | previousInvestorBalance 141 | .minus(assetValue) 142 | .minus(gasPrice.times(gasUsed)) 143 | .toNumber() 144 | ) 145 | currentAssetBalance.toNumber().should.equal(previousAssetBalance.plus(assetValue).toNumber()) 146 | }) 147 | 148 | it("should deny an investment if the asset isn't available", async () => { 149 | await firstAsset.invest(investor,{from: investor, value: assetValue}) 150 | .should.be.rejectedWith('VM Exception') 151 | }) 152 | }) 153 | 154 | context('Cancel Investment', () => { 155 | it("should deny a cancelment if the user isn't the investor", async () => { 156 | await firstAsset.cancelInvestment({from: creditCompany}) 157 | .should.be.rejectedWith('VM Exception') 158 | }) 159 | 160 | it('should cancel a pending investment', async () => { 161 | const previousAssetBalance = await getBalance(firstAsset.address) 162 | const previousInvestorBalance = await getBalance(investor) 163 | const { logs, receipt } = await firstAsset.cancelInvestment({from: investor}) 164 | const event = logs.find(e => e.event === 'LogCanceled') 165 | expect(event.args).to.include.all.keys([ 166 | '_owner', 167 | '_investor', 168 | '_value', 169 | ]) 170 | const currentAssetBalance = await getBalance(firstAsset.address) 171 | const currentInvestorBalance = await getBalance(investor) 172 | const gasUsed = new BigNumber(receipt.gasUsed) 173 | currentInvestorBalance.toNumber().should.equal( 174 | previousInvestorBalance 175 | .plus(assetValue) 176 | .minus(gasPrice.times(gasUsed)) 177 | .toNumber() 178 | ) 179 | currentAssetBalance.toNumber().should.equal(previousAssetBalance.minus(assetValue).toNumber()) 180 | }) 181 | }) 182 | 183 | context('Refuse Investment', () => { 184 | 185 | it('should add an investment', async () => { 186 | await firstAsset.invest(investor, {from: investor, value: assetValue}) 187 | }) 188 | 189 | it("should deny a refusement if the user isn't the asset owner", async () => { 190 | await firstAsset.refuseInvestment( { from: investor }) 191 | .should.be.rejectedWith('VM Exception') 192 | }) 193 | 194 | it('should refuse a pending investment', async () => { 195 | const previousAssetBalance = await getBalance(firstAsset.address) 196 | const previousInvestorBalance = await getBalance(investor) 197 | const {logs} = await firstAsset.refuseInvestment({ from: creditCompany }) 198 | let event = logs.find(e => e.event === 'LogRefused') 199 | expect(event.args).to.include.all.keys([ 200 | '_owner', 201 | '_investor', 202 | '_value', 203 | ]) 204 | const currentAssetBalance = await getBalance(firstAsset.address) 205 | const currentInvestorBalance = await getBalance(investor) 206 | currentInvestorBalance.toNumber().should.equal( 207 | previousInvestorBalance 208 | .plus(assetValue) 209 | .toNumber() 210 | ) 211 | currentAssetBalance.toNumber().should.equal(previousAssetBalance.minus(assetValue).toNumber()) 212 | }) 213 | 214 | }) 215 | 216 | context('Withdraw Funds', () => { 217 | 218 | it('should add an investment', async () => { 219 | await firstAsset.invest(investor,{from: investor, value: assetValue}) 220 | }) 221 | 222 | it("should deny a withdrawal if the user isn't the asset owner", async () => { 223 | await firstAsset.withdrawFunds({ from: investor }) 224 | .should.be.rejectedWith('VM Exception') 225 | }) 226 | 227 | it('should accept a pending investment and withdraw funds', async () => { 228 | const previousAssetBalance = await getBalance(firstAsset.address) 229 | const previousCreditCompanyBalance = await getBalance(creditCompany) 230 | const { logs, receipt } = await firstAsset.withdrawFunds( { from: creditCompany }) 231 | const currentAssetBalance = await getBalance(firstAsset.address) 232 | const currentCreditCompanyBalance = await getBalance(creditCompany) 233 | const gasUsed = new BigNumber(receipt.gasUsed) 234 | const event = logs.find(e => e.event === 'LogWithdrawal') 235 | 236 | expect(event.args).to.include.all.keys([ 237 | '_owner', 238 | '_investor', 239 | '_value', 240 | ]) 241 | currentCreditCompanyBalance.toNumber().should.equal( 242 | previousCreditCompanyBalance 243 | .plus(assetValue) 244 | .minus(gasPrice.times(gasUsed)) 245 | .toNumber() 246 | ) 247 | currentAssetBalance.toNumber().should.equal(previousAssetBalance.minus(assetValue).toNumber()) 248 | }) 249 | 250 | }) 251 | 252 | context('Sell', () => { 253 | 254 | it("should deny a sell order if the user isn't the investor", async() => { 255 | await firstAsset.sell(sellValue, {from: creditCompany}) 256 | .should.be.rejectedWith('VM Exception') 257 | }) 258 | 259 | it('should sell an asset', async() => { 260 | const {logs} = await firstAsset.sell(sellValue, { from: investor }) 261 | const event = logs.find(e => e.event === 'LogForSale') 262 | 263 | expect(event.args).to.include.all.keys([ 264 | '_investor', 265 | '_value' 266 | ]) 267 | }) 268 | 269 | }) 270 | 271 | context('Cancel sell order', () => { 272 | 273 | it("should deny a sell order cancelment if the user isn't the investor", async () => { 274 | await firstAsset.cancelSellOrder({from: creditCompany}) 275 | .should.be.rejectedWith('VM Exception') 276 | }) 277 | 278 | it("should cancel a sell", async () => { 279 | const {logs} = await firstAsset.cancelSellOrder({from: investor}) 280 | const event = logs.find(e => e.event === 'LogCanceledSell') 281 | 282 | expect(event.args).to.include.all.keys([ 283 | '_investor', 284 | '_value' 285 | ]) 286 | }) 287 | 288 | }) 289 | 290 | context('Buy', () => { 291 | 292 | it("should buy an asset", async () => { 293 | await firstAsset.sell(sellValue, {from: investor}) 294 | const previousAssetBalance = await getBalance(assetsAddress[1]) 295 | const previousBuyerBalance = await getBalance(secondInvestor) 296 | const { logs, receipt } = await firstAsset.buy(secondInvestor, { from: secondInvestor, value: sellValue }) 297 | const event = logs.find(e => e.event === 'LogInvested') 298 | expect(event.args).to.include.all.keys([ '_owner', '_investor', '_value' ]) 299 | const currentAssetBalance = await getBalance(assetsAddress[1]) 300 | const currentBuyerBalance = await getBalance(secondInvestor) 301 | const gasUsed = new BigNumber(receipt.gasUsed) 302 | currentBuyerBalance.toNumber().should.equal( 303 | previousBuyerBalance 304 | .minus(sellValue) 305 | .minus(gasPrice.times(gasUsed)) 306 | .toNumber() 307 | ) 308 | currentAssetBalance.toNumber().should.equal(previousAssetBalance.plus(sellValue).toNumber()) 309 | }) 310 | }) 311 | 312 | context('Cancel sale', () => { 313 | 314 | it("should deny a sale cancelment if the user isnt't the buyer", async () => { 315 | await firstAsset.cancelSale({from: investor}) 316 | .should.be.rejectedWith('VM Exception') 317 | }) 318 | 319 | it("should cancel a sale", async () => { 320 | const previousAssetBalance = await getBalance(firstAsset.address) 321 | const previousBuyerBalance = await getBalance(secondInvestor) 322 | const { logs, receipt } = await firstAsset.cancelSale({from: secondInvestor}) 323 | const event = logs.find(e => e.event === 'LogCanceled') 324 | expect(event.args).to.include.all.keys([ '_owner', '_investor', '_value' ]) 325 | const currentAssetBalance = await getBalance(firstAsset.address) 326 | const currentBuyerBalance = await getBalance(secondInvestor) 327 | const gasUsed = new BigNumber(receipt.gasUsed) 328 | currentBuyerBalance.toNumber().should.equal( 329 | previousBuyerBalance 330 | .plus(sellValue) 331 | .minus(gasPrice.times(gasUsed)) 332 | .toNumber() 333 | ) 334 | currentAssetBalance.toNumber().should.equal(previousAssetBalance.minus(sellValue).toNumber()) 335 | }) 336 | }) 337 | 338 | context('Refuse sale', () => { 339 | 340 | it("should buy an asset", async () => { 341 | const previousAssetBalance = await getBalance(firstAsset.address) 342 | const previousBuyerBalance = await getBalance(secondInvestor) 343 | const { logs, receipt } = await firstAsset.buy(secondInvestor, { from: secondInvestor, value: sellValue }) 344 | const event = logs.find(e => e.event === 'LogInvested') 345 | expect(event.args).to.include.all.keys([ '_owner', '_investor', '_value' ]) 346 | const currentAssetBalance = await getBalance(firstAsset.address) 347 | const currentBuyerBalance = await getBalance(secondInvestor) 348 | const gasUsed = new BigNumber(receipt.gasUsed) 349 | currentBuyerBalance.toNumber().should.equal( 350 | previousBuyerBalance 351 | .minus(sellValue) 352 | .minus(gasPrice.times(gasUsed)) 353 | .toNumber() 354 | ) 355 | currentAssetBalance.toNumber().should.equal(previousAssetBalance.plus(sellValue).toNumber()) 356 | }) 357 | 358 | it("should deny a sale refusement if the user isnt't the investor", async () => { 359 | await firstAsset.refuseSale({from: secondInvestor}) 360 | .should.be.rejectedWith('VM Exception') 361 | }) 362 | 363 | it("should refuse a sale", async () => { 364 | const previousAssetBalance = await getBalance(firstAsset.address) 365 | const previousBuyerBalance = await getBalance(secondInvestor) 366 | const { logs, receipt } = await firstAsset.refuseSale({from: investor}) 367 | const event = logs.find(e => e.event === 'LogRefused') 368 | expect(event.args).to.include.all.keys([ '_owner', '_investor', '_value' ]) 369 | const currentAssetBalance = await getBalance(firstAsset.address) 370 | const currentBuyerBalance = await getBalance(secondInvestor) 371 | currentBuyerBalance.toNumber().should.equal( 372 | previousBuyerBalance 373 | .plus(sellValue) 374 | .toNumber() 375 | ) 376 | currentAssetBalance.toNumber().should.equal(previousAssetBalance.minus(sellValue).toNumber()) 377 | 378 | }) 379 | }) 380 | 381 | context('Accept sale and withdraw funds', () => { 382 | 383 | it("should deny a sale acceptment if the user isnt't the investor", async () => { 384 | await firstAsset.buy(secondInvestor, { value: sellValue }) 385 | await firstAsset.acceptSale({from: secondInvestor}) 386 | .should.be.rejectedWith('VM Exception') 387 | }) 388 | 389 | it("should accept a sale", async () => { 390 | const previousAssetBalance = await getBalance(firstAsset.address) 391 | const previousSellerBalance = await getBalance(investor) 392 | const asset = await InvestmentAsset.at(firstAsset.address); 393 | const assetValues = await asset.getAsset(); 394 | const sellValue = assetValues[11]; 395 | const { logs, receipt } = await firstAsset.acceptSale({from: investor}) 396 | const event = logs.find(e => e.event === 'LogWithdrawal') 397 | expect(event.args).to.include.all.keys([ '_owner', '_investor', '_value' ]) 398 | const currentAssetBalance = await getBalance(firstAsset.address) 399 | const currentSellerBalance = await getBalance(investor) 400 | const gasUsed = new BigNumber(receipt.gasUsed) 401 | currentSellerBalance.toNumber().should.equal( 402 | previousSellerBalance 403 | .plus(sellValue) 404 | .minus(gasPrice.times(gasUsed)) 405 | .toNumber() 406 | ) 407 | currentAssetBalance.toNumber().should.equal(previousAssetBalance.minus(sellValue).toNumber()) 408 | const boughtValue = await firstAsset.boughtValue.call(); 409 | sellValue.toNumber().should.equal(boughtValue.toNumber()); 410 | }) 411 | }) 412 | 413 | 414 | context("Require Asset's Fuel", () => { 415 | 416 | it("should deny the token fuel request if the return of investment isn't delayed", async () => { 417 | await firstAsset.requireTokenFuel({ from: secondInvestor }) 418 | .should.be.rejectedWith('VM Exception') 419 | }) 420 | 421 | it("should deny the token fuel request if the user isn't the investor", async () => { 422 | // simulate a long period after the funds transfer 423 | await increaseTime(16416000) 424 | await firstAsset.requireTokenFuel({ from: creditCompany }) 425 | .should.be.rejectedWith('VM Exception') 426 | }) 427 | 428 | it("should send the token fuel to the asset's investor", async () => { 429 | const previousInvestorTokenBalance = await token.balanceOf(secondInvestor) 430 | const previousAssetTokenBalance = await token.balanceOf(firstAsset.address) 431 | const { logs, receipt } = await firstAsset.requireTokenFuel({ from: secondInvestor }) 432 | const event = logs.find(e => e.event === 'LogTokenWithdrawal') 433 | expect(event.args).to.include.all.keys([ 434 | '_to', 435 | '_amount' 436 | ]) 437 | const currentInvestorTokenBalance = await token.balanceOf(secondInvestor) 438 | const currentAssetTokenBalance = await token.balanceOf(firstAsset.address) 439 | currentInvestorTokenBalance.toNumber().should.equal( 440 | previousInvestorTokenBalance 441 | .plus(assetFuel) 442 | .toNumber() 443 | ) 444 | currentAssetTokenBalance.toNumber().should.equal(previousAssetTokenBalance.minus(assetFuel).toNumber()) 445 | }) 446 | }) 447 | 448 | context('Delayed return without remaining tokens', () => { 449 | 450 | it("should deny an investment return if the user isn't the asset owner", async () => { 451 | await firstAsset.returnInvestment({ from: secondInvestor, value: returnValue }) 452 | .should.be.rejectedWith('VM Exception') 453 | }) 454 | 455 | it('should return the investment with delay', async () => { 456 | const previousInvestorBalance = await getBalance(secondInvestor) 457 | const { logs, receipt } = await firstAsset.returnInvestment({ from: creditCompany, value: returnValue }) 458 | const event = logs.find(e => e.event === 'LogReturned') 459 | expect(event.args).to.include.all.keys([ 460 | '_owner', 461 | '_investor', 462 | '_value', 463 | '_delayed' 464 | ]) 465 | assert.equal(event.args._delayed,true,"The investment must be returned with delay") 466 | const currentInvestorBalance = await getBalance(secondInvestor) 467 | currentInvestorBalance.toNumber().should.equal( 468 | previousInvestorBalance 469 | .plus(returnValue) 470 | .toNumber() 471 | ) 472 | }) 473 | 474 | }) 475 | 476 | context('Delayed return with remaining tokens', () => { 477 | it("should supply tokens to the second asset", async () => { 478 | await token.approve(assetsAddress[2], assetFuel, {from: creditCompany}) 479 | secondAsset = await AssetLibrary.at(assetsAddress[2]) 480 | await secondAsset.supplyFuel(assetFuel, { from: creditCompany }) 481 | }) 482 | 483 | it('should add an investment', async () => { 484 | secondAsset = await AssetLibrary.at(assetsAddress[2]) 485 | await secondAsset.invest(investor,{from: investor, value: assetValue}) 486 | }) 487 | 488 | it('should accept a pending investment and withdraw funds', async () => { 489 | await secondAsset.withdrawFunds({from: creditCompany}) 490 | }) 491 | 492 | it("should return the investment with delay and send tokens to the asset's investor", async () => { 493 | // simulate a long period after the funds transfer 494 | await increaseTime(16416000) 495 | const previousInvestorTokenBalance = await token.balanceOf(investor) 496 | const previousAssetTokenBalance = await token.balanceOf(secondAsset.address) 497 | const { logs, receipt } = await secondAsset.returnInvestment({ from: creditCompany, value: returnValue }) 498 | const event = logs.find(e => e.event === 'LogReturned') 499 | expect(event.args).to.include.all.keys([ 500 | '_owner', 501 | '_investor', 502 | '_value', 503 | '_delayed' 504 | ]) 505 | assert.equal(event.args._delayed,true,"The investment must be returned with delay") 506 | const currentInvestorTokenBalance = await token.balanceOf(investor) 507 | const currentAssetTokenBalance = await token.balanceOf(secondAsset.address) 508 | currentInvestorTokenBalance.toNumber().should.equal( 509 | previousInvestorTokenBalance 510 | .plus(assetFuel) 511 | .toNumber() 512 | ) 513 | currentAssetTokenBalance.toNumber().should.equal(previousAssetTokenBalance.minus(assetFuel).toNumber()) 514 | }) 515 | }) 516 | 517 | context('Correct return with remaining tokens', () => { 518 | it("should supply tokens to the third asset", async () => { 519 | await token.approve(assetsAddress[3], assetFuel, {from: creditCompany}) 520 | thirdAsset = await AssetLibrary.at(assetsAddress[3]) 521 | await thirdAsset.supplyFuel(assetFuel, { from: creditCompany }) 522 | }) 523 | 524 | it('should add an investment', async () => { 525 | await thirdAsset.invest(investor, { from: investor, value: assetValue }) 526 | }) 527 | 528 | it('should accept a pending investment and withdraw funds', async () => { 529 | await thirdAsset.withdrawFunds({from: creditCompany}) 530 | }) 531 | 532 | it("should return the investment correctly and send tokens to the asset's owner", async () => { 533 | const previousCreditCompanyTokenBalance = await token.balanceOf(creditCompany) 534 | const previousAssetTokenBalance = await token.balanceOf(thirdAsset.address) 535 | const { logs } = await thirdAsset.returnInvestment({ from: creditCompany, value: returnValue }) 536 | const event = logs.find(e => e.event === 'LogReturned') 537 | expect(event.args).to.include.all.keys([ '_owner', '_investor', '_value', '_delayed' ]) 538 | assert.equal(event.args._delayed,false,"The investment must be returned without delay") 539 | const currentCreditCompanyTokenBalance = await token.balanceOf(creditCompany) 540 | const currentAssetTokenBalance = await token.balanceOf(thirdAsset.address) 541 | currentCreditCompanyTokenBalance.toNumber().should.equal( 542 | previousCreditCompanyTokenBalance 543 | .plus(assetFuel) 544 | .toNumber() 545 | ) 546 | currentAssetTokenBalance.toNumber().should.equal(previousAssetTokenBalance.minus(assetFuel).toNumber()) 547 | }) 548 | }) 549 | 550 | context('Return the investment when the asset is being sold and refund the buyer', () => { 551 | it('should add an investment', async () => { 552 | fourthAsset = await AssetLibrary.at(assetsAddress[4]) 553 | await fourthAsset.invest(investor,{ from: investor, value: assetValue }) 554 | }) 555 | 556 | it('should accept a pending investment and withdraw funds', async () => { 557 | await fourthAsset.withdrawFunds({from: creditCompany}) 558 | }) 559 | 560 | it('should sell the asset', async () => { 561 | await fourthAsset.sell(sellValue, {from: investor}) 562 | }) 563 | 564 | it('should buy the asset', async () => { 565 | await fourthAsset.buy(secondInvestor, { value: sellValue }) 566 | }) 567 | it("should return the investment to the investor and refund the buyer", async () => { 568 | const previousInvestorBalance = await getBalance(investor) 569 | const previousBuyerBalance = await getBalance(secondInvestor) 570 | const { logs } = await fourthAsset.returnInvestment({ from: creditCompany, value: returnValue }) 571 | const event = logs.find(e => e.event === 'LogReturned') 572 | assert.equal(event.args._investor, investor, "The investment must be returned to the asset's investor") 573 | const currentInvestorBalance = await getBalance(investor) 574 | const currentBuyerBalance = await getBalance(secondInvestor) 575 | currentBuyerBalance.toNumber().should.equal( 576 | previousBuyerBalance 577 | .plus(sellValue) 578 | .toNumber() 579 | ) 580 | currentInvestorBalance.toNumber().should.equal( 581 | previousInvestorBalance 582 | .plus(returnValue) 583 | .toNumber() 584 | ) 585 | }) 586 | }) 587 | 588 | }) 589 | -------------------------------------------------------------------------------- /test/SwapyExchange.js: -------------------------------------------------------------------------------- 1 | 2 | // helpers 3 | const increaseTime = require('./helpers/increaseTime') 4 | const { getBalance, getGasPrice } = require('./helpers/web3') 5 | const ether = require('./helpers/ether') 6 | 7 | const BigNumber = web3.BigNumber 8 | const should = require('chai') 9 | .use(require('chai-as-promised')) 10 | .should() 11 | const expect = require('chai').expect 12 | 13 | // --- Handled contracts 14 | const SwapyExchange = artifacts.require("./SwapyExchange.sol") 15 | const AssetLibrary = artifacts.require("./investment/AssetLibrary.sol") 16 | const Token = artifacts.require("./token/Token.sol") 17 | 18 | // --- Test constants 19 | const payback = new BigNumber(12) 20 | const grossReturn = new BigNumber(500) 21 | const assetValue = ether(5) 22 | 23 | // returned value = invested value + return on investment 24 | const returnValue = new BigNumber(1 + grossReturn.toNumber()/10000).times(assetValue) 25 | const assets = [500,500,500,500,500] 26 | const offerFuel = new BigNumber(5000) 27 | const assetFuel = offerFuel.dividedBy(new BigNumber(assets.length)) 28 | const currency = "USD" 29 | 30 | // --- Test variables 31 | // Contracts 32 | let token = null 33 | let library = null 34 | let protocol = null 35 | let firstAsset = null 36 | let secondAsset = null 37 | let thirdAsset = null 38 | let fourthAsset = null 39 | let fifthAsset = null 40 | // Assets 41 | let assetsAddress = [] 42 | // Agents 43 | let investor = null 44 | let creditCompany = null 45 | let Swapy = null 46 | let secondInvestor = null 47 | // Util 48 | let gasPrice = null 49 | let sellValue = null 50 | 51 | contract('SwapyExchange', async accounts => { 52 | 53 | before( async () => { 54 | 55 | Swapy = accounts[0] 56 | creditCompany = accounts[1] 57 | investor = accounts[2] 58 | secondInvestor = accounts[3] 59 | gasPrice = await getGasPrice() 60 | gasPrice = new BigNumber(gasPrice) 61 | const library = await AssetLibrary.new({ from: Swapy }) 62 | token = await Token.new({from: Swapy}) 63 | protocol = await SwapyExchange.new( token.address, "1.0.0", library.address, { from: Swapy }) 64 | await token.mint(creditCompany, offerFuel, {from: Swapy}) 65 | 66 | // payback and sell constants 67 | const periodAfterInvestment = new BigNumber(1/2) 68 | await increaseTime(86400 * payback * periodAfterInvestment.toNumber()) 69 | const returnOnPeriod = returnValue.minus(assetValue).times(periodAfterInvestment) 70 | sellValue = assetValue.plus(returnOnPeriod) 71 | 72 | }) 73 | 74 | it("should have a version", async () => { 75 | const version = await protocol.latestVersion.call() 76 | should.exist(version) 77 | }) 78 | 79 | context('Fundraising offers', () => { 80 | 81 | it("should create an investment offer with assets", async () => { 82 | const {logs} = await protocol.createOffer( 83 | "1.0.0", 84 | payback, 85 | grossReturn, 86 | currency, 87 | assets, 88 | {from: creditCompany} 89 | ) 90 | const event = logs.find(e => e.event === 'LogOffers') 91 | const args = event.args 92 | expect(args).to.include.all.keys([ '_from', '_protocolVersion', '_assets' ]) 93 | assetsAddress = args._assets 94 | assert.equal(args._from, creditCompany, 'The credit company must be the offer owner') 95 | assert.equal(args._assets.length, assets.length, 'The count of created assets must be equal the count of sent') 96 | firstAsset = await AssetLibrary.at(assetsAddress[0]) 97 | secondAsset = await AssetLibrary.at(assetsAddress[1]) 98 | thirdAsset = await AssetLibrary.at(assetsAddress[2]) 99 | fourthAsset = await AssetLibrary.at(assetsAddress[3]) 100 | fifthAsset = await AssetLibrary.at(assetsAddress[4]) 101 | await token.approve(assetsAddress[0], assetFuel, {from: creditCompany}) 102 | await token.approve(assetsAddress[1], assetFuel, {from: creditCompany}) 103 | await token.approve(assetsAddress[2], assetFuel, {from: creditCompany}) 104 | await token.approve(assetsAddress[3], assetFuel, {from: creditCompany}) 105 | await token.approve(assetsAddress[4], assetFuel, {from: creditCompany}) 106 | await firstAsset.supplyFuel(assetFuel, { from: creditCompany }) 107 | await secondAsset.supplyFuel(assetFuel, { from: creditCompany }) 108 | await thirdAsset.supplyFuel(assetFuel, { from: creditCompany }) 109 | await fourthAsset.supplyFuel(assetFuel, { from: creditCompany }) 110 | await fifthAsset.supplyFuel(assetFuel, { from: creditCompany }) 111 | }) 112 | }) 113 | 114 | context('Investment', () => { 115 | 116 | it("should add an investment of many assets", async () => { 117 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 118 | // balances before invest 119 | let previousAssetsBalance = new BigNumber(0) 120 | for(let assetAddress of assets){ 121 | let assetBalance = await getBalance(assetAddress) 122 | previousAssetsBalance = previousAssetsBalance.plus(assetBalance) 123 | } 124 | const previousInvestorBalance = await getBalance(investor) 125 | const { logs, receipt } = await protocol.invest( assets, assetValue, { value: assetValue * assets.length, from: investor }) 126 | // balances after invest 127 | let currentAssetsBalance = new BigNumber(0) 128 | for(let assetAddress of assets){ 129 | let assetBalance = await getBalance(assetAddress) 130 | currentAssetsBalance = currentAssetsBalance.plus(assetBalance) 131 | } 132 | const currentInvestorBalance = await getBalance(investor) 133 | const gasUsed = new BigNumber(receipt.gasUsed) 134 | const event = logs.find(e => e.event === 'LogInvestments') 135 | const args = event.args 136 | expect(args).to.include.all.keys([ '_investor', '_assets', '_value' ]) 137 | currentInvestorBalance.toNumber().should.equal( 138 | previousInvestorBalance 139 | .minus(assetValue * assets.length) 140 | .minus(gasPrice.times(gasUsed)) 141 | .toNumber() 142 | ) 143 | currentAssetsBalance.toNumber().should.equal(previousAssetsBalance.plus(assetValue * assets.length).toNumber()) 144 | }) 145 | 146 | it("should deny a cancelment if the user isn't the investor", async () => { 147 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 148 | await protocol.cancelInvestment(assets, { from: creditCompany }) 149 | .should.be.rejectedWith('VM Exception') 150 | }) 151 | 152 | it("should cancel an investment on many assets", async () => { 153 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 154 | // balances before invest 155 | let previousAssetsBalance = new BigNumber(0) 156 | for(let assetAddress of assets){ 157 | let assetBalance = await getBalance(assetAddress) 158 | previousAssetsBalance = previousAssetsBalance.plus(assetBalance) 159 | } 160 | const previousInvestorBalance = await getBalance(investor) 161 | const { receipt } = await protocol.cancelInvestment( assets, { from: investor }) 162 | // balances after invest 163 | let currentAssetsBalance = new BigNumber(0) 164 | for(let assetAddress of assets){ 165 | let assetBalance = await getBalance(assetAddress) 166 | currentAssetsBalance = currentAssetsBalance.plus(assetBalance) 167 | } 168 | const currentInvestorBalance = await getBalance(investor) 169 | const gasUsed = new BigNumber(receipt.gasUsed) 170 | currentInvestorBalance.toNumber().should.equal( 171 | previousInvestorBalance 172 | .plus(assetValue * assets.length) 173 | .minus(gasPrice.times(gasUsed)) 174 | .toNumber() 175 | ) 176 | currentAssetsBalance.toNumber().should.equal(previousAssetsBalance.minus(assetValue * assets.length).toNumber()) 177 | 178 | }) 179 | 180 | it("should deny an investment refusement if the user isn't the owner", async () => { 181 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 182 | await protocol.invest( assets, assetValue, { value: assetValue * assets.length, from: investor }) 183 | await protocol.refuseInvestment(assets, { from: investor }) 184 | .should.be.rejectedWith('VM Exception') 185 | }) 186 | 187 | it("should refuse an investment on many assets", async () => { 188 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 189 | // balances before invest 190 | let previousAssetsBalance = new BigNumber(0) 191 | for(let assetAddress of assets){ 192 | let assetBalance = await getBalance(assetAddress) 193 | previousAssetsBalance = previousAssetsBalance.plus(assetBalance) 194 | } 195 | const previousInvestorBalance = await getBalance(investor) 196 | await protocol.refuseInvestment(assets, { from: creditCompany }) 197 | // balances after invest 198 | let currentAssetsBalance = new BigNumber(0) 199 | for(let assetAddress of assets){ 200 | let assetBalance = await getBalance(assetAddress) 201 | currentAssetsBalance = currentAssetsBalance.plus(assetBalance) 202 | } 203 | const currentInvestorBalance = await getBalance(investor) 204 | currentInvestorBalance.toNumber().should.equal( 205 | previousInvestorBalance 206 | .plus(assetValue * assets.length) 207 | .toNumber() 208 | ) 209 | currentAssetsBalance.toNumber().should.equal(previousAssetsBalance.minus(assetValue * assets.length).toNumber()) 210 | }) 211 | 212 | it("should deny an investment withdrawal if the user isn't the owner", async () => { 213 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 214 | await protocol.invest( assets, assetValue, { value: assetValue * assets.length, from: investor }) 215 | await protocol.withdrawFunds(assets, { from: investor }) 216 | .should.be.rejectedWith('VM Exception') 217 | }) 218 | 219 | it("should withdraw funds on many assets", async () => { 220 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 221 | // balances before invest 222 | let previousAssetsBalance = new BigNumber(0) 223 | for(let assetAddress of assets){ 224 | let assetBalance = await getBalance(assetAddress) 225 | previousAssetsBalance = previousAssetsBalance.plus(assetBalance) 226 | } 227 | const previousCreditCompanyBalance = await getBalance(creditCompany) 228 | const {receipt} = await protocol.withdrawFunds( assets, { from: creditCompany }) 229 | // balances after invest 230 | let currentAssetsBalance = new BigNumber(0) 231 | for(let assetAddress of assets){ 232 | let assetBalance = await getBalance(assetAddress) 233 | currentAssetsBalance = currentAssetsBalance.plus(assetBalance) 234 | } 235 | const currentCreditCompanyBalance = await getBalance(creditCompany) 236 | const gasUsed = new BigNumber(receipt.gasUsed) 237 | currentCreditCompanyBalance.toNumber().should.equal( 238 | previousCreditCompanyBalance 239 | .plus(assetValue * assets.length) 240 | .minus(gasPrice.times(gasUsed)) 241 | .toNumber() 242 | ) 243 | currentAssetsBalance.toNumber().should.equal(previousAssetsBalance.minus(assetValue * assets.length).toNumber()) 244 | }) 245 | 246 | it("should deny collateral tokens request if the user isn't the investor", async() => { 247 | // simulate a long period after the funds transfer 248 | await increaseTime(16416000) 249 | const assets = [assetsAddress[0], assetsAddress[1]] 250 | await protocol.requireTokenFuel(assets, { from: creditCompany }) 251 | .should.be.rejectedWith('VM Exception') 252 | }) 253 | 254 | it("should request collateral tokens of many assets", async() => { 255 | const assets = [assetsAddress[0], assetsAddress[1]] 256 | const previousInvestorTokenBalance = await token.balanceOf(investor) 257 | let previousAssetsTokenBalance = new BigNumber(0) 258 | for(let assetAddress of assets){ 259 | let assetBalance = await token.balanceOf(assetAddress) 260 | previousAssetsTokenBalance = previousAssetsTokenBalance.plus(assetBalance) 261 | } 262 | await protocol.requireTokenFuel(assets, { from: investor }) 263 | // balances after invest 264 | let currentInvestorTokenBalance = await token.balanceOf(investor) 265 | let currentAssetsTokenBalance = new BigNumber(0) 266 | for(let assetAddress of assets){ 267 | let assetBalance = await token.balanceOf(assetAddress) 268 | currentAssetsTokenBalance = currentAssetsTokenBalance.plus(assetBalance) 269 | } 270 | currentInvestorTokenBalance.toNumber().should.equal( 271 | previousInvestorTokenBalance 272 | .plus(assetFuel.times(new BigNumber(assets.length))) 273 | .toNumber() 274 | ) 275 | currentAssetsTokenBalance.toNumber().should.equal(previousAssetsTokenBalance.minus(assetFuel.times(new BigNumber(assets.length))).toNumber()) 276 | }) 277 | 278 | }) 279 | 280 | context('Market Place', () => { 281 | 282 | it("should deny a sell order if the user isn't the investor", async() => { 283 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 284 | const values = [sellValue, sellValue, sellValue, sellValue, sellValue] 285 | await protocol.sellAssets(assets, values, { from: creditCompany }) 286 | .should.be.rejectedWith('VM Exception') 287 | }) 288 | 289 | it('should create sell orders of many assets', async() => { 290 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 291 | const values = [sellValue, sellValue, sellValue, sellValue, sellValue] 292 | const { logs } = await protocol.sellAssets(assets, values, { from: investor }) 293 | const event = logs.find(e => e.event === 'LogForSale') 294 | expect(event.args).to.include.all.keys([ '_investor', '_asset', '_value' ]) 295 | }) 296 | 297 | it("should deny a sell order cancelment if the user isn't the investor", async () => { 298 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 299 | await protocol.cancelSellOrder(assets, { from: creditCompany }) 300 | .should.be.rejectedWith('VM Exception') 301 | }) 302 | 303 | it("should cancel a sell", async () => { 304 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 305 | const { receipt } = await protocol.cancelSellOrder(assets, { from: investor }) 306 | receipt.status.should.equal('0x01') 307 | }) 308 | 309 | it("should buy an asset" , async () => { 310 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 311 | const values = [sellValue, sellValue, sellValue, sellValue, sellValue] 312 | await protocol.sellAssets(assets, values, { from: investor }) 313 | const asset = assets[0] 314 | const previousAssetBalance = await getBalance(asset) 315 | const previousBuyerBalance = await getBalance(secondInvestor) 316 | const { logs, receipt } = await protocol.buyAsset(asset, { from: secondInvestor, value: sellValue }) 317 | const event = logs.find(e => e.event === 'LogBought') 318 | expect(event.args).to.include.all.keys([ '_buyer', '_asset', '_value' ]) 319 | const currentAssetBalance = await getBalance(asset) 320 | const currentBuyerBalance = await getBalance(secondInvestor) 321 | const gasUsed = new BigNumber(receipt.gasUsed) 322 | currentBuyerBalance.toNumber().should.equal( 323 | previousBuyerBalance 324 | .minus(sellValue) 325 | .minus(gasPrice.times(gasUsed)) 326 | .toNumber() 327 | ) 328 | currentAssetBalance.toNumber().should.equal(previousAssetBalance.plus(sellValue).toNumber()) 329 | }) 330 | 331 | it("should deny a sale cancelment if the user isn't the buyer", async () => { 332 | const assets = [assetsAddress[0]] 333 | await protocol.cancelSale(assets, { from: investor }) 334 | .should.be.rejectedWith('VM Exception') 335 | }) 336 | 337 | it("should cancel many sales", async () => { 338 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 339 | await protocol.buyAsset(assets[1], { from: secondInvestor, value: sellValue }) 340 | await protocol.buyAsset(assets[2], { from: secondInvestor, value: sellValue }) 341 | await protocol.buyAsset(assets[3], { from: secondInvestor, value: sellValue }) 342 | await protocol.buyAsset(assets[4], { from: secondInvestor, value: sellValue }) 343 | let previousAssetsBalance = new BigNumber(0) 344 | for(let assetAddress of assets){ 345 | let assetBalance = await getBalance(assetAddress) 346 | previousAssetsBalance = previousAssetsBalance.plus(assetBalance) 347 | } 348 | const previousBuyerBalance = await getBalance(secondInvestor) 349 | const { receipt } = await protocol.cancelSale(assets, { from: secondInvestor }) 350 | let currentAssetsBalance = new BigNumber(0) 351 | for(let assetAddress of assets){ 352 | let assetBalance = await getBalance(assetAddress) 353 | currentAssetsBalance = currentAssetsBalance.plus(assetBalance) 354 | } 355 | const currentBuyerBalance = await getBalance(secondInvestor) 356 | const gasUsed = new BigNumber(receipt.gasUsed) 357 | currentBuyerBalance.toNumber().should.equal( 358 | previousBuyerBalance 359 | .plus(sellValue * assets.length) 360 | .minus(gasPrice.times(gasUsed)) 361 | .toNumber() 362 | ) 363 | currentAssetsBalance.toNumber().should.equal(previousAssetsBalance.minus(sellValue * assets.length).toNumber()) 364 | }) 365 | 366 | it("should deny a sale refusement if the user isn't the investor", async () => { 367 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 368 | const values = [sellValue, sellValue, sellValue, sellValue, sellValue] 369 | await protocol.buyAsset(assets[0], { from: secondInvestor, value: sellValue }) 370 | await protocol.buyAsset(assets[1], { from: secondInvestor, value: sellValue }) 371 | await protocol.buyAsset(assets[2], { from: secondInvestor, value: sellValue }) 372 | await protocol.buyAsset(assets[3], { from: secondInvestor, value: sellValue }) 373 | await protocol.buyAsset(assets[4], { from: secondInvestor, value: sellValue }) 374 | await protocol.refuseSale(assets, { from: secondInvestor }) 375 | .should.be.rejectedWith('VM Exception') 376 | }) 377 | 378 | it("should refuse many sales", async () => { 379 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 380 | let previousAssetsBalance = new BigNumber(0) 381 | for(let assetAddress of assets){ 382 | let assetBalance = await getBalance(assetAddress) 383 | previousAssetsBalance = previousAssetsBalance.plus(assetBalance) 384 | } 385 | const previousBuyerBalance = await getBalance(secondInvestor) 386 | await protocol.refuseSale(assets, { from: investor }) 387 | let currentAssetsBalance = new BigNumber(0) 388 | for(let assetAddress of assets){ 389 | let assetBalance = await getBalance(assetAddress) 390 | currentAssetsBalance = currentAssetsBalance.plus(assetBalance) 391 | } 392 | const currentBuyerBalance = await getBalance(secondInvestor) 393 | currentBuyerBalance.toNumber().should.equal( 394 | previousBuyerBalance 395 | .plus(sellValue * assets.length) 396 | .toNumber() 397 | ) 398 | currentAssetsBalance.toNumber().should.equal(previousAssetsBalance.minus(sellValue * assets.length).toNumber()) 399 | }) 400 | 401 | it("should deny a sale acceptment if the user isnt't the investor", async () => { 402 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 403 | const values = [sellValue, sellValue, sellValue, sellValue, sellValue] 404 | await protocol.buyAsset(assets[0], { from: secondInvestor, value: sellValue }) 405 | await protocol.buyAsset(assets[1], { from: secondInvestor, value: sellValue }) 406 | await protocol.buyAsset(assets[2], { from: secondInvestor, value: sellValue }) 407 | await protocol.buyAsset(assets[3], { from: secondInvestor, value: sellValue }) 408 | await protocol.buyAsset(assets[4], { from: secondInvestor, value: sellValue }) 409 | await protocol.acceptSale(assets, { from: secondInvestor }) 410 | .should.be.rejectedWith('VM Exception') 411 | }) 412 | 413 | it("should accept many sales", async () => { 414 | const assets = [assetsAddress[0], assetsAddress[1], assetsAddress[2], assetsAddress[3], assetsAddress[4]] 415 | let previousAssetsBalance = new BigNumber(0) 416 | for(let assetAddress of assets){ 417 | let assetBalance = await getBalance(assetAddress) 418 | previousAssetsBalance = previousAssetsBalance.plus(assetBalance) 419 | } 420 | const previousSellerBalance = await getBalance(investor) 421 | const { receipt } = await protocol.acceptSale(assets, { from: investor }) 422 | let currentAssetsBalance = new BigNumber(0) 423 | for(let assetAddress of assets) { 424 | let assetBalance = await getBalance(assetAddress) 425 | currentAssetsBalance = currentAssetsBalance.plus(assetBalance) 426 | } 427 | const currentSellerBalance = await getBalance(investor) 428 | const gasUsed = new BigNumber(receipt.gasUsed) 429 | currentSellerBalance.toNumber().should.equal( 430 | previousSellerBalance 431 | .plus(sellValue * assets.length) 432 | .minus(gasPrice.times(gasUsed)) 433 | .toNumber() 434 | ) 435 | currentAssetsBalance.toNumber().should.equal(previousAssetsBalance.minus(sellValue * assets.length).toNumber()) 436 | }) 437 | 438 | }) 439 | 440 | }) 441 | -------------------------------------------------------------------------------- /test/TestInvestmentAsset_investment.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "truffle/Assert.sol"; 4 | import "truffle/DeployedAddresses.sol"; 5 | import "../contracts/investment/InvestmentAsset.sol"; 6 | import "../contracts/SwapyExchange.sol"; 7 | import "./helpers/ThrowProxy.sol"; 8 | import "./AssetCall.sol"; 9 | 10 | contract TestInvestmentAsset_investment { 11 | 12 | SwapyExchange protocol = SwapyExchange(DeployedAddresses.SwapyExchange()); 13 | 14 | address token = protocol.token(); 15 | bytes8 version = protocol.latestVersion(); 16 | address _library = protocol.getLibrary(version); 17 | 18 | InvestmentAsset assetInstance = new InvestmentAsset( 19 | _library, 20 | address(protocol), 21 | address(this), 22 | version, 23 | bytes5("USD"), 24 | uint256(500), 25 | uint256(360), 26 | uint256(10), 27 | token 28 | ); 29 | 30 | ThrowProxy throwProxy = new ThrowProxy(address(assetInstance)); 31 | AssetCall throwableAsset = new AssetCall(address(throwProxy)); 32 | AssetCall asset = new AssetCall(address(assetInstance)); 33 | // Truffle looks for `initialBalance` when it compiles the test suite 34 | // and funds this test contract with the specified amount on deployment. 35 | uint public initialBalance = 10 ether; 36 | 37 | function() payable public { 38 | 39 | } 40 | 41 | function shouldThrow(bool result) public { 42 | Assert.isFalse(result, "Should throw an exception"); 43 | } 44 | 45 | // Testing invest() function 46 | function testInvestorAddressMustBeValid() { 47 | bool result = throwableAsset.invest(true); 48 | throwProxy.shouldThrow(); 49 | } 50 | 51 | function testUnavailableActionsWhenAvaiable() public { 52 | bool available = refuseInvestment(address(assetInstance)) && 53 | withdrawFunds(address(assetInstance)) && 54 | returnInvestment(address(assetInstance), 1100 finney) && 55 | asset.cancelInvestment(); 56 | Assert.isFalse(available, "Should not be executed"); 57 | } 58 | 59 | function testUserCanInvest() public { 60 | uint256 previousBalance = address(this).balance; 61 | uint256 previousAssetBalance = address(assetInstance).balance; 62 | bool result = asset.invest.value(1 ether)(false); 63 | InvestmentAsset.Status currentStatus = assetInstance.status(); 64 | bool isPending = currentStatus == InvestmentAsset.Status.PENDING_OWNER_AGREEMENT; 65 | Assert.equal(result, true, "Asset must be invested"); 66 | Assert.equal(isPending, true, "The asset must be locked for investments"); 67 | Assert.equal( 68 | previousBalance - address(this).balance, 69 | address(assetInstance).balance - previousAssetBalance, 70 | "balance changes must be equal" 71 | ); 72 | } 73 | 74 | function testUnavailableActionsWhenPending() public { 75 | bool available = asset.invest.value(1 ether)(false) && 76 | returnInvestment(address(assetInstance), 1100 finney); 77 | Assert.isFalse(available, "Should not be executed"); 78 | } 79 | 80 | // Testing cancelInvestment() function 81 | function testOnlyInvestorCanCancelInvestment() public { 82 | bool result = throwableAsset.cancelInvestment(); 83 | throwProxy.shouldThrow(); 84 | } 85 | 86 | function testInvestorCanCancelInvestment() public { 87 | uint256 previousBalance = address(asset).balance; 88 | uint256 previousAssetBalance = address(assetInstance).balance; 89 | bool result = asset.cancelInvestment(); 90 | InvestmentAsset.Status currentStatus = assetInstance.status(); 91 | bool isAvailable = currentStatus == InvestmentAsset.Status.AVAILABLE; 92 | Assert.equal(result, true, "Investment must be canceled"); 93 | Assert.equal(isAvailable, true, "The asset must be available for investments"); 94 | Assert.equal( 95 | address(asset).balance - previousBalance, 96 | previousAssetBalance - address(assetInstance).balance, 97 | "balance changes must be equal" 98 | ); 99 | } 100 | 101 | // Testing refuseInvestment() function 102 | function testOnlyOwnerCanRefuseInvestment() public { 103 | asset.invest(false); 104 | bool result = refuseInvestment(address(throwProxy)); 105 | throwProxy.shouldThrow(); 106 | } 107 | 108 | function testOwnerCanRefuseInvestment() public { 109 | uint256 previousBalance = address(asset).balance; 110 | uint256 previousAssetBalance = address(assetInstance).balance; 111 | bool result = refuseInvestment(address(assetInstance)); 112 | InvestmentAsset.Status currentStatus = assetInstance.status(); 113 | bool isAvailable = currentStatus == InvestmentAsset.Status.AVAILABLE; 114 | Assert.equal(result, true, "Investment must be refused"); 115 | Assert.equal(isAvailable, true, "The asset must be available for investments"); 116 | Assert.equal( 117 | address(asset).balance - previousBalance, 118 | previousAssetBalance - address(assetInstance).balance, 119 | "balance changes must be equal" 120 | ); 121 | } 122 | 123 | // Testing withdrawFunds() function 124 | function testOnlyOwnerCanWithdrawFunds() public { 125 | asset.invest(false); 126 | bool result = withdrawFunds(address(throwProxy)); 127 | throwProxy.shouldThrow(); 128 | } 129 | 130 | function testOwnerCanWithdrawFunds() public { 131 | uint256 previousBalance = address(this).balance; 132 | uint256 previousAssetBalance = address(assetInstance).balance; 133 | bool result = withdrawFunds(address(assetInstance)); 134 | InvestmentAsset.Status currentStatus = assetInstance.status(); 135 | bool isInvested = currentStatus == InvestmentAsset.Status.INVESTED; 136 | Assert.equal(result, true, "Investment must be accepted"); 137 | Assert.equal(isInvested, true, "The asset must be invested"); 138 | Assert.equal( 139 | address(this).balance - previousBalance, 140 | previousAssetBalance - address(assetInstance).balance, 141 | "balance changes must be equal" 142 | ); 143 | } 144 | 145 | function testUnavailableActionsWhenInvested() public { 146 | bool available = asset.invest.value(1 ether)(false) && 147 | refuseInvestment(address(assetInstance)) && 148 | withdrawFunds(address(assetInstance)) && 149 | asset.cancelInvestment(); 150 | Assert.isFalse(available, "Should not be executed"); 151 | } 152 | 153 | // Testing returnInvestment() function 154 | function testOnlyOwnerCanReturnInvestment() public { 155 | returnInvestment(address(throwProxy), 1100 finney); 156 | throwProxy.shouldThrow(); 157 | } 158 | 159 | function testOwnerCanReturnInvestment() public { 160 | uint256 previousBalance = address(this).balance; 161 | uint256 previousAssetBalance = address(asset).balance; 162 | bool result = returnInvestment(address(assetInstance), 1100 finney); 163 | Assert.equal(result, true, "Investment must be returned"); 164 | InvestmentAsset.Status currentStatus = assetInstance.status(); 165 | bool isReturned = currentStatus == InvestmentAsset.Status.RETURNED; 166 | Assert.equal(isReturned, true, "The asset must be returned"); 167 | Assert.equal( 168 | previousBalance - address(this).balance, 169 | address(asset).balance - previousAssetBalance, 170 | "balance changes must be equal" 171 | ); 172 | } 173 | 174 | function testUnavailableActionsWhenReturned() public { 175 | bool available = asset.invest.value(1 ether)(false) && 176 | refuseInvestment(address(assetInstance)) && 177 | withdrawFunds(address(assetInstance)) && 178 | returnInvestment(address(assetInstance), 1100 finney) && 179 | asset.cancelInvestment(); 180 | Assert.isFalse(available, "Should not be executed"); 181 | } 182 | 183 | function refuseInvestment(address _asset) returns(bool) { 184 | return _asset.call(abi.encodeWithSignature("refuseInvestment()")); 185 | } 186 | 187 | function withdrawFunds(address _asset) returns(bool) { 188 | return _asset.call(abi.encodeWithSignature("withdrawFunds()")); 189 | } 190 | 191 | function returnInvestment(address _asset, uint256 _value) payable returns(bool) { 192 | return _asset.call.value(_value)(abi.encodeWithSignature("returnInvestment()")); 193 | } 194 | 195 | } 196 | 197 | -------------------------------------------------------------------------------- /test/TestInvestmentAsset_marketplace.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "truffle/Assert.sol"; 4 | import "truffle/DeployedAddresses.sol"; 5 | import "../contracts/investment/InvestmentAsset.sol"; 6 | import "../contracts/SwapyExchange.sol"; 7 | import "./helpers/ThrowProxy.sol"; 8 | import "./AssetCall.sol"; 9 | 10 | contract TestInvestmentAsset_marketplace { 11 | 12 | SwapyExchange protocol = SwapyExchange(DeployedAddresses.SwapyExchange()); 13 | 14 | address token = protocol.token(); 15 | bytes8 version = protocol.latestVersion(); 16 | address _library = protocol.getLibrary(version); 17 | 18 | InvestmentAsset assetInstance = new InvestmentAsset( 19 | _library, 20 | address(protocol), 21 | address(this), 22 | version, 23 | bytes5("USD"), 24 | uint256(500), 25 | uint256(360), 26 | uint256(10), 27 | token 28 | ); 29 | 30 | ThrowProxy throwProxy = new ThrowProxy(address(assetInstance)); 31 | 32 | AssetCall asset = new AssetCall(address(assetInstance)); 33 | AssetCall throwableAsset = new AssetCall(address(throwProxy)); 34 | 35 | // Truffle looks for `initialBalance` when it compiles the test suite 36 | // and funds this test contract with the specified amount on deployment. 37 | uint public initialBalance = 10 ether; 38 | 39 | function() payable public { 40 | 41 | } 42 | 43 | function shouldThrow(bool result) public { 44 | Assert.isFalse(result, "Should throw an exception"); 45 | } 46 | 47 | function testUnavailableActionsWhenInvested() public { 48 | asset.invest.value(1 ether)(false); 49 | withdrawFunds(address(assetInstance)); 50 | bool available = asset.sell(uint256(525)) && 51 | asset.cancelSellOrder() && 52 | asset.buy.value(1050 finney)(true) && 53 | asset.cancelSale() && 54 | asset.refuseSale() && 55 | asset.acceptSale(); 56 | Assert.isFalse(available, "Should not be executed"); 57 | } 58 | 59 | // Testing sell() function 60 | function testOnlyInvestorCanPutOnSale() public { 61 | bool result = throwableAsset.sell(uint256(525)); 62 | throwProxy.shouldThrow(); 63 | } 64 | 65 | function testInvestorCanPutOnSale() public { 66 | bool result = asset.sell(uint256(525)); 67 | Assert.equal(result, true, "Asset must be put up on sale"); 68 | InvestmentAsset.Status currentStatus = assetInstance.status(); 69 | bool isForSale = currentStatus == InvestmentAsset.Status.FOR_SALE; 70 | Assert.equal(isForSale, true, "The asset must be available on market place"); 71 | } 72 | 73 | function testUnavailableActionsWhenOnSale() public { 74 | bool available = asset.sell(uint256(525)) && 75 | asset.cancelSale() && 76 | asset.refuseSale() && 77 | asset.acceptSale(); 78 | Assert.isFalse(available, "Should not be executed"); 79 | } 80 | 81 | // Testing cancelSellOrder() function 82 | function testOnlyInvestorCanRemoveOnSale() public { 83 | bool result = throwableAsset.cancelSellOrder(); 84 | throwProxy.shouldThrow(); 85 | } 86 | 87 | function testInvestorCanRemoveOnSale() public { 88 | bool result = asset.cancelSellOrder(); 89 | Assert.equal(result, true, "Asset must be removed for sale"); 90 | InvestmentAsset.Status currentStatus = assetInstance.status(); 91 | bool isInvested = currentStatus == InvestmentAsset.Status.INVESTED; 92 | Assert.equal(isInvested, true, "The asset must be invested"); 93 | } 94 | 95 | // Testing buy() function 96 | function testBuyerAddressMustBeValid() { 97 | asset.sell(uint256(525)); 98 | bool result = throwableAsset.buy.value(1050 finney)(true); 99 | throwProxy.shouldThrow(); 100 | } 101 | 102 | function testUserCanBuyAsset() public { 103 | uint256 previousBalance = address(this).balance; 104 | uint256 previousAssetBalance = address(assetInstance).balance; 105 | bool result = asset.buy.value(1050 finney)(false); 106 | Assert.equal(result, true, "Asset must be bought"); 107 | InvestmentAsset.Status currentStatus = assetInstance.status(); 108 | bool isPendingSale = currentStatus == InvestmentAsset.Status.PENDING_INVESTOR_AGREEMENT; 109 | Assert.equal(isPendingSale, true, "The asset must be locked on market place"); 110 | Assert.equal( 111 | previousBalance - address(this).balance, 112 | address(assetInstance).balance - previousAssetBalance, 113 | "balance changes must be equal" 114 | ); 115 | } 116 | 117 | function testUnavailableActionsWhenPendingSale() public { 118 | bool available = withdrawFunds(address(assetInstance)) && 119 | asset.cancelInvestment() && 120 | asset.sell(uint256(525)) && 121 | asset.buy.value(1050 finney)(false); 122 | Assert.isFalse(available, "Should not be executed"); 123 | } 124 | 125 | // Testing cancelSale() function 126 | function testOnlyBuyerCanCancelPurchase() public { 127 | bool result = throwableAsset.cancelSale(); 128 | throwProxy.shouldThrow(); 129 | } 130 | 131 | function testBuyerCanCancelPurchase() public { 132 | bool result = asset.cancelSale(); 133 | Assert.equal(result, true, "Purchase must be canceled"); 134 | InvestmentAsset.Status currentStatus = assetInstance.status(); 135 | bool isForSale = currentStatus == InvestmentAsset.Status.FOR_SALE; 136 | Assert.equal(isForSale, true, "The asset must be available on market place"); 137 | } 138 | 139 | // Testing refuseSale() function 140 | function testOnlyInvestorCanRefusePurchase() public { 141 | asset.buy.value(1050 finney)(false); 142 | bool result = throwableAsset.refuseSale(); 143 | throwProxy.shouldThrow(); 144 | } 145 | 146 | function testInvestorCanRefusePurchase() public { 147 | bool result = asset.refuseSale(); 148 | Assert.equal(result, true, "Purchase must be refused"); 149 | InvestmentAsset.Status currentStatus = assetInstance.status(); 150 | bool isForSale = currentStatus == InvestmentAsset.Status.FOR_SALE; 151 | Assert.equal(isForSale, true, "The asset must be available on market place"); 152 | } 153 | 154 | // Testing acceptSale() function 155 | function testOnlyInvestorCanAcceptSale() public { 156 | asset.buy.value(1050 finney)(false); 157 | bool result = throwableAsset.acceptSale(); 158 | throwProxy.shouldThrow(); 159 | } 160 | 161 | function testInvestorCanAcceptSale() public { 162 | uint256 previousBalance = address(asset).balance; 163 | uint256 previousAssetBalance = address(assetInstance).balance; 164 | bool result = asset.acceptSale(); 165 | Assert.equal(result, true, "Sale must be accepted"); 166 | InvestmentAsset.Status currentStatus = assetInstance.status(); 167 | bool isInvested = currentStatus == InvestmentAsset.Status.INVESTED; 168 | Assert.equal(isInvested, true, "The asset must be invested"); 169 | Assert.equal( 170 | address(asset).balance - previousBalance, 171 | previousAssetBalance - address(assetInstance).balance, 172 | "balance changes must be equal" 173 | ); 174 | } 175 | 176 | function withdrawFunds(address _asset) returns(bool) { 177 | return _asset.call(abi.encodeWithSignature("withdrawFunds()")); 178 | } 179 | 180 | } 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /test/TestSwapyExchange_actions.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "truffle/Assert.sol"; 4 | import "truffle/DeployedAddresses.sol"; 5 | import "../contracts/SwapyExchange.sol"; 6 | import "./helpers/ThrowProxy.sol"; 7 | 8 | contract TestSwapyExchange_actions { 9 | SwapyExchange protocol = SwapyExchange(DeployedAddresses.SwapyExchange()); 10 | ThrowProxy throwProxy = new ThrowProxy(address(protocol)); 11 | SwapyExchange throwableProtocol = SwapyExchange(address(throwProxy)); 12 | uint256[] _assetValues; 13 | uint256[] _returnValues; 14 | address[] assets; 15 | 16 | // Truffle looks for `initialBalance` when it compiles the test suite 17 | // and funds this test contract with the specified amount on deployment. 18 | uint public initialBalance = 10 ether; 19 | 20 | bytes msgData; 21 | function() payable public { 22 | 23 | } 24 | 25 | function shouldThrow(bool result) public { 26 | Assert.isFalse(result, "Should throw an exception"); 27 | } 28 | 29 | // Testing the createOffer() function 30 | function testUserCanCreateOfferWithoutVersion() public { 31 | _assetValues.push(uint256(500)); 32 | _assetValues.push(uint256(500)); 33 | _assetValues.push(uint256(500)); 34 | assets = protocol.createOffer( 35 | bytes8(0), 36 | uint256(360), 37 | uint256(10), 38 | bytes5("USD"), 39 | _assetValues 40 | ); 41 | Assert.equal(assets.length, 3, "3 Assets must be created"); 42 | bool isOwner = (InvestmentAsset(assets[0]).owner() == address(this)) 43 | && (InvestmentAsset(assets[1]).owner() == address(this)) 44 | && (InvestmentAsset(assets[2]).owner() == address(this)); 45 | Assert.equal(isOwner, true, "The test contract must be owner of the fundraising offer"); 46 | } 47 | 48 | function testUserCanCreateOfferWithVersion() public { 49 | assets = protocol.createOffer( 50 | bytes8("1.0.0"), 51 | uint256(360), 52 | uint256(10), 53 | bytes5("USD"), 54 | _assetValues 55 | ); 56 | Assert.equal(assets.length, 3, "3 Assets must be created"); 57 | bool isOwner = (InvestmentAsset(assets[0]).owner() == address(this)) 58 | && (InvestmentAsset(assets[1]).owner() == address(this)) 59 | && (InvestmentAsset(assets[2]).owner() == address(this)); 60 | Assert.equal(isOwner, true, "The test contract must be owner of the fundraising offer"); 61 | } 62 | 63 | function testUserCannotCreateOfferWithAnInvalidVersion() public { 64 | address(throwableProtocol).call(abi.encodeWithSignature("createOffer(bytes8,uint256,uint256,bytes5,uint256[])", 65 | bytes8("3.0.0"), 66 | uint256(360), 67 | uint256(10), 68 | bytes5("USD"), 69 | _assetValues 70 | )); 71 | throwProxy.shouldThrow(); 72 | } 73 | 74 | // testing invest() function 75 | function testUnitValueAndFundsMustMatch() public { 76 | address(throwableProtocol).call.value(2 ether)(abi.encodeWithSignature("invest(address[], uint256)", assets, 1 ether)); 77 | throwProxy.shouldThrow(); 78 | } 79 | 80 | function testUserCanInvest() public { 81 | bool result = protocol.invest.value(3 ether)(assets, 1 ether); 82 | Assert.equal(result, true, "Assets must be invested"); 83 | bool isInvestor = (InvestmentAsset(assets[0]).investor() == address(this)) 84 | && (InvestmentAsset(assets[1]).investor() == address(this)) 85 | && (InvestmentAsset(assets[2]).investor() == address(this)); 86 | Assert.equal(isInvestor, true, "The test contract must be the investor of these assets"); 87 | } 88 | 89 | // Testing cancelInvestment() function 90 | function testOnlyInvestorCanCancelInvestment() public { 91 | address(throwableProtocol).call(abi.encodeWithSignature("cancelInvestment(address[])", assets)); 92 | throwProxy.shouldThrow(); 93 | } 94 | 95 | function testInvestorCanCancelInvestment() public { 96 | bool result = protocol.cancelInvestment(assets); 97 | Assert.equal(result, true, "Investments must be canceled"); 98 | } 99 | 100 | // Testing refuseInvestment() function 101 | function testOnlyOwnerCanRefuseInvestment() public { 102 | protocol.invest.value(3 ether)(assets, 1 ether); 103 | address(throwableProtocol).call(abi.encodeWithSignature("refuseInvestment(address[])", assets)); 104 | throwProxy.shouldThrow(); 105 | } 106 | 107 | function testOwnerCanRefuseInvestment() public { 108 | bool result = protocol.refuseInvestment(assets); 109 | Assert.equal(result, true, "Investments must be refused"); 110 | } 111 | 112 | // Testing withdrawFunds() function 113 | function testOnlyOwnerCanWithdrawFunds() public { 114 | protocol.invest.value(3 ether)(assets, 1 ether); 115 | address(throwableProtocol).call(abi.encodeWithSignature("withdrawFunds(address[])", assets)); 116 | throwProxy.shouldThrow(); 117 | } 118 | 119 | function testOwnerCanWithdrawFunds() public { 120 | bool result = protocol.withdrawFunds(assets); 121 | Assert.equal(result, true, "Investments must be accepted"); 122 | } 123 | 124 | // Testing sell() function 125 | function testOnlyInvestorCanPutOnSale() public { 126 | _assetValues[0] += 25; 127 | _assetValues[1] += 25; 128 | _assetValues[2] += 25; 129 | address(throwableProtocol).call(abi.encodeWithSignature("sellAssets(address[],uint256[])",assets, _assetValues)); 130 | throwProxy.shouldThrow(); 131 | } 132 | 133 | function testInvestorCanPutOnSale() public { 134 | bool result = protocol.sellAssets(assets, _assetValues); 135 | Assert.equal(result, true, "Assets must be put up on sale"); 136 | } 137 | 138 | // Testing cancelSellOrder() function 139 | function testOnlyInvestorCanRemoveOnSale() public { 140 | address(throwableProtocol).call(abi.encodeWithSignature("cancelSellOrder(address[])", assets)); 141 | throwProxy.shouldThrow(); 142 | } 143 | 144 | function testInvestorCanRemoveOnSale() public { 145 | bool result = protocol.cancelSellOrder(assets); 146 | Assert.equal(result, true, "Asset must be removed for sale"); 147 | } 148 | 149 | // Testing buy() function 150 | 151 | function testUserCanBuyAsset() public { 152 | protocol.sellAssets(assets, _assetValues); 153 | bool result = protocol.buyAsset.value(1050 finney)(assets[0]); 154 | Assert.equal(result, true, "Asset must be bought"); 155 | } 156 | 157 | // Testing cancelSale() function 158 | function testOnlyBuyerCanCancelPurchase() public { 159 | protocol.buyAsset.value(1050 finney)(assets[1]); 160 | protocol.buyAsset.value(1050 finney)(assets[2]); 161 | address(throwableProtocol).call(abi.encodeWithSignature("cancelSale(address[])", assets)); 162 | throwProxy.shouldThrow(); 163 | } 164 | 165 | function testBuyerCanCancelPurchase() public { 166 | bool result = protocol.cancelSale(assets); 167 | Assert.equal(result, true, "Purchase must be canceled"); 168 | } 169 | 170 | // Testing refuseSale() function 171 | function testOnlyInvestorCanRefusePurchase() public { 172 | protocol.buyAsset.value(1050 finney)(assets[0]); 173 | protocol.buyAsset.value(1050 finney)(assets[1]); 174 | protocol.buyAsset.value(1050 finney)(assets[2]); 175 | address(throwableProtocol).call(abi.encodeWithSignature("refuseSale(address[])", assets)); 176 | throwProxy.shouldThrow(); 177 | } 178 | 179 | function testInvestorCanRefusePurchase() public { 180 | bool result = protocol.refuseSale(assets); 181 | Assert.equal(result, true, "Purchases must be refused"); 182 | } 183 | 184 | // Testing acceptSale() function 185 | function testOnlyInvestorCanAcceptSale() public { 186 | protocol.buyAsset.value(1050 finney)(assets[0]); 187 | protocol.buyAsset.value(1050 finney)(assets[1]); 188 | protocol.buyAsset.value(1050 finney)(assets[2]); 189 | address(throwableProtocol).call(abi.encodeWithSignature("acceptSale(address[])", assets)); 190 | throwProxy.shouldThrow(); 191 | } 192 | 193 | function testInvestorCanAcceptSale() public { 194 | bool result = protocol.acceptSale(assets); 195 | Assert.equal(result, true, "Sales must be accepted"); 196 | } 197 | 198 | //testing returnInvestment() function 199 | function testOnlyOwnerCanReturnInvestment() public { 200 | _returnValues.push(1100 finney); 201 | _returnValues.push(1100 finney); 202 | _returnValues.push(1100 finney); 203 | address(throwableProtocol) 204 | .call 205 | .value(3300 finney) 206 | (abi.encodeWithSignature("returnInvestment(address[], uint256[])", assets, _returnValues)); 207 | throwProxy.shouldThrow(); 208 | } 209 | 210 | function testReturnValuesAndFundsMustMatch() public { 211 | bool result = address(protocol) 212 | .call 213 | .value(3299 finney) 214 | (abi.encodeWithSignature("returnInvestment(address[], uint256[])", assets, _returnValues)); 215 | shouldThrow(result); 216 | } 217 | 218 | function testOwnerCanReturnInvestment() public { 219 | bool result = address(throwableProtocol) 220 | .call 221 | .value(3300 finney) 222 | (abi.encodeWithSignature("returnInvestment(address[], uint256[])", assets, _returnValues)); 223 | Assert.equal(result, true, "Investments must be returned"); 224 | } 225 | 226 | } -------------------------------------------------------------------------------- /test/TestSwapyExchange_versioning.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.23; 2 | 3 | import "truffle/Assert.sol"; 4 | import "../contracts/SwapyExchange.sol"; 5 | import "./helpers/ThrowProxy.sol"; 6 | 7 | contract TestSwapyExchange_versioning { 8 | SwapyExchange protocol = new SwapyExchange(address(0x281055afc982d96fab65b3a49cac8b878184cb16),"1.0.0",address(0x6f46cf5569aefa1acc1009290c8e043747172d89)); 9 | ThrowProxy throwProxy = new ThrowProxy(address(protocol)); 10 | SwapyExchange throwableProtocol = SwapyExchange(address(throwProxy)); 11 | 12 | // Truffle looks for `initialBalance` when it compiles the test suite 13 | // and funds this test contract with the specified amount on deployment. 14 | uint public initialBalance = 10 ether; 15 | 16 | function shouldThrow(bool execution) public { 17 | Assert.isFalse(execution, "Should throw an exception"); 18 | } 19 | 20 | // Testing the addLibrary() function 21 | function testOnlyProtocolOwnerCanAddLibrary() public { 22 | address(throwableProtocol).call(abi.encodeWithSignature("addLibrary(bytes8,address)", bytes8("3.0.0"), address(0x8f46cf5569aefa1acc1009290c8e043747172d45))); 23 | throwProxy.shouldThrow(); 24 | } 25 | 26 | function testProtocolOwnerCanAddLibraryVersion() public { 27 | address lib3 = address(0x9f46cf5569aefa1acc1009290c8e043747172d45); 28 | bool result = protocol.addLibrary(bytes8("3.0.0"), lib3); 29 | bytes8 latestVersion = protocol.latestVersion(); 30 | address latestLib = protocol.getLibrary(latestVersion); 31 | bool check = (latestVersion == bytes8("3.0.0")) && (latestLib == address(lib3)); 32 | Assert.equal(check, true, "The latest version must be 3.0.0 and the library address of this version must be equal the lastest deployed library"); 33 | } 34 | 35 | function testOwnerCannotAddDuplicatedVersion() public { 36 | bool result = address(protocol).call(abi.encodeWithSignature("addLibrary(bytes8,address)", bytes8("3.0.0"), address(0x9f46cf5569aefa1acc1009290c8e043747172d45))); 37 | shouldThrow(result); 38 | } 39 | 40 | function testOwnerCannotAddInvalidVersion() public { 41 | bool result = address(protocol).call(abi.encodeWithSignature("addLibrary(bytes8,address)", bytes8(0), address(0x9f46cf5569aefa1acc1009290c8e043747172d45))); 42 | shouldThrow(result); 43 | } 44 | 45 | function testOwnerCannotAddInvalidLibrary() public { 46 | bool result = address(protocol).call(abi.encodeWithSignature("addLibrary(bytes8,address)", bytes8("4.0.0"), address(0))); 47 | shouldThrow(result); 48 | } 49 | } -------------------------------------------------------------------------------- /test/helpers/ThrowProxy.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.17; 2 | 3 | import "truffle/Assert.sol"; 4 | 5 | // Proxy contract for testing throws 6 | contract ThrowProxy { 7 | address public target; 8 | bytes data; 9 | uint256 value; 10 | 11 | constructor(address _target) public { 12 | target = _target; 13 | } 14 | 15 | //prime the data using the fallback function. 16 | function() public payable { 17 | data = msg.data; 18 | if(msg.value > 0){ 19 | value = msg.value; 20 | } 21 | } 22 | 23 | function execute() public returns (bool) { 24 | if(value > 0){ 25 | return target.call.value(value)(data); 26 | }else { 27 | return target.call(data); 28 | } 29 | } 30 | 31 | function shouldThrow() public { 32 | bool r = this.execute.gas(200000)(); 33 | Assert.isFalse(r, "Should throw an exception"); 34 | } 35 | } -------------------------------------------------------------------------------- /test/helpers/ether.js: -------------------------------------------------------------------------------- 1 | module.exports = (n) => { 2 | return new web3.BigNumber(web3.toWei(n, 'ether')) 3 | } 4 | -------------------------------------------------------------------------------- /test/helpers/increaseTime.js: -------------------------------------------------------------------------------- 1 | // Increases testrpc time by the passed duration in seconds 2 | module.exports = function increaseTime(duration) { 3 | const id = Date.now() 4 | 5 | return new Promise((resolve, reject) => { 6 | web3.currentProvider.sendAsync({ 7 | jsonrpc: '2.0', 8 | method: 'evm_increaseTime', 9 | params: [duration], 10 | id: id, 11 | }, err1 => { 12 | if (err1) return reject(err1) 13 | 14 | web3.currentProvider.sendAsync({ 15 | jsonrpc: '2.0', 16 | method: 'evm_mine', 17 | id: id+1, 18 | }, (err2, res) => { 19 | return err2 ? reject(err2) : resolve(res) 20 | }) 21 | }) 22 | }) 23 | } -------------------------------------------------------------------------------- /test/helpers/web3.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | 4 | getBalance(addr) { 5 | return new Promise((resolve, reject) => { 6 | web3.eth.getBalance(addr, async (err, res) => { 7 | if (err || !res) return reject(err) 8 | resolve(res) 9 | }) 10 | }) 11 | }, 12 | 13 | getBlockNumber() { 14 | return new Promise((resolve, reject) => { 15 | web3.eth.getBlockNumber(async (err, res) => { 16 | if (err || !res) return reject(err) 17 | resolve(res) 18 | }) 19 | }) 20 | }, 21 | 22 | getBlock(n) { 23 | return new Promise(async (resolve, reject) => { 24 | web3.eth.getBlock(n, (err, res) => { 25 | if (err || !res) return reject(err) 26 | resolve(res) 27 | }) 28 | }) 29 | }, 30 | 31 | sendTransaction(payload) { 32 | return new Promise((resolve, reject) => { 33 | web3.eth.sendTransaction(payload, async (err, res) => { 34 | if (err || !res) return reject(err) 35 | resolve(res) 36 | }) 37 | }) 38 | }, 39 | 40 | getNonce(web3) { 41 | return new Promise((resolve, reject) => { 42 | web3.eth.getAccounts((err, acc) => { 43 | if (err) return reject(err) 44 | web3.eth.getTransactionCount(acc[0], (err, n) => { 45 | if (err) return reject(err) 46 | resolve(n) 47 | }) 48 | }) 49 | }) 50 | }, 51 | 52 | sign(payload, address) { 53 | return new Promise((resolve, reject) => { 54 | web3.eth.sign(address, payload, async (err, signedPayload) => { 55 | if (err || !signedPayload) return reject(err) 56 | const adding0x = x => '0x'.concat(x) 57 | resolve({ 58 | r: adding0x(signedPayload.substr(2, 64)), 59 | s: adding0x(signedPayload.substr(66, 64)), 60 | v: signedPayload.substr(130, 2) == '00' ? 27 : 28, 61 | }) 62 | }) 63 | }) 64 | }, 65 | 66 | getGasPrice() { 67 | return new Promise((resolve, reject) => { 68 | web3.eth.getGasPrice(async (err, res) => { 69 | if (err || !res) return reject(err) 70 | resolve(res) 71 | }) 72 | }) 73 | }, 74 | 75 | } -------------------------------------------------------------------------------- /test/helpers/wei.js: -------------------------------------------------------------------------------- 1 | module.exports = (n) => { 2 | return new web3.BigNumber(n) 3 | } 4 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | var bip39 = require("bip39"); 2 | var hdkey = require('ethereumjs-wallet/hdkey'); 3 | var ProviderEngine = require("web3-provider-engine"); 4 | var WalletSubprovider = require('web3-provider-engine/subproviders/wallet.js'); 5 | var Web3Subprovider = require("web3-provider-engine/subproviders/web3.js"); 6 | var Web3 = require("web3"); 7 | 8 | // Get our mnemonic and create an hdwallet 9 | var mnemonic = process.env.WALLET_MNEMONIC; 10 | var hdwallet = hdkey.fromMasterSeed(bip39.mnemonicToSeed(mnemonic)); 11 | // Get the first account using the standard hd path. 12 | var wallet_hdpath = "m/44'/60'/0'/0/"; 13 | var wallet = hdwallet.derivePath(wallet_hdpath + "0").getWallet(); 14 | var address = "0x" + wallet.getAddress().toString("hex"); 15 | 16 | // Configure the custom provider 17 | var engine = new ProviderEngine(); 18 | const FilterSubprovider = require('web3-provider-engine/subproviders/filters.js') 19 | engine.addProvider(new FilterSubprovider()) 20 | engine.addProvider(new WalletSubprovider(wallet, {})); 21 | var providerUrl = process.env.PROVIDER_URL; 22 | engine.addProvider(new Web3Subprovider(new Web3.providers.HttpProvider(providerUrl))); 23 | engine.start(); // Required by the provider engine. 24 | 25 | 26 | const network_id = process.env.NETWORK_ID; 27 | const dev_network_id = process.env.DEV_NETWORK_ID; 28 | 29 | module.exports = { 30 | networks: { 31 | custom : { 32 | network_id: network_id, // custom network id 33 | provider: engine, // Use our custom provider 34 | from: address, // Use the address we derived 35 | gas: 4670000 36 | }, 37 | dev : { 38 | host: "localhost", 39 | network_id: '*', 40 | port: 8545, 41 | gas: 7984452 42 | }, 43 | test : { 44 | host: "localhost", 45 | network_id: '*', 46 | port: 8545, 47 | gas: 7984452 48 | } 49 | }, 50 | rpc: { 51 | // Use the default host and port when not using ropsten 52 | host: "localhost", 53 | port: 8545 54 | } 55 | }; 56 | --------------------------------------------------------------------------------