├── .babelrc ├── .gitattributes ├── .gitignore ├── .solcover.js ├── .soliumignore ├── .soliumrc.json ├── .travis.yml ├── FAQ.md ├── LICENSE ├── README.md ├── contracts ├── Fund.sol ├── FundOperator.sol ├── FundToken.sol ├── FundWallet.sol ├── Migrations.sol ├── mocks │ └── FundOperatorMock.sol └── open-zeppelin │ ├── examples │ └── SimpleToken.sol │ ├── lifecycle │ └── Pausable.sol │ ├── math │ └── SafeMath.sol │ ├── ownership │ └── Ownable.sol │ └── token │ └── ERC20 │ ├── BasicToken.sol │ ├── ERC20.sol │ ├── ERC20Basic.sol │ ├── MintableToken.sol │ └── StandardToken.sol ├── imgs ├── ercfund.png └── secprivlogo.png ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package.json ├── scripts ├── coverage.sh └── test.sh ├── test ├── Fund.test.js ├── FundOperator.test.js ├── FundToken.test.js ├── FundWallet.test.js └── open-zeppelin │ └── helpers │ ├── EVMRevert.js │ └── ether.js ├── truffle-config.js └── truffle.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "stage-3"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.idea 3 | *.log 4 | 5 | *.node* 6 | node_modules 7 | build 8 | coverage 9 | 10 | coverage.json 11 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | norpc: true, 3 | testCommand: 'node --max-old-space-size=4096 ../node_modules/.bin/truffle test --network coverage', 4 | compileCommand: 'node --max-old-space-size=4096 ../node_modules/.bin/truffle compile --network coverage', 5 | skipFiles: [ 6 | 'open-zeppelin', 7 | 'mocks' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | contracts/open-zeppelin 3 | contracts/Migrations.sol 4 | -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:recommended", 3 | "plugins": [ 4 | "security" 5 | ], 6 | "rules": { 7 | "quotes": [ 8 | "error", 9 | "double" 10 | ], 11 | "indentation": [ 12 | "error", 13 | 4 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | group: beta 4 | language: node_js 5 | node_js: 6 | - "8" 7 | cache: 8 | directories: 9 | - node_modules 10 | env: 11 | - 12 | - SOLIDITY_COVERAGE=true 13 | matrix: 14 | fast_finish: true 15 | allow_failures: 16 | - env: SOLIDITY_COVERAGE=true 17 | before_script: 18 | - truffle version 19 | script: 20 | - npm run lint 21 | - npm run test 22 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # ERCFund-FAQ 2 | 3 | ## I want to create a fund running on your smart contracts! How would I go about doing this? 4 | Firstly I'm happy to hear that you consider this software useful! Even though the code is rigorously tested I would advise you to thoroughly go over the code or pay for audit, I take no responsibility for any bugs in the code. 5 | 6 | For actively managing funds with this set of smart contracts I would advise to program a client-side software which takes care of executing and signing any transactions you send to the fund. 7 | Another option is to simply deploy the smart contracts without the FundOperator.class and manage them manually, however this removes some trust functionalities and the option for cold wallets. 8 | 9 | In order to kickstart your fund in the beginning you could additionally add a Crowdfunding contract to the current set of contracts. This is currently not implemented, however it is such a common topic that there 10 | are many good resources explaining how to start an ICO. I can especially recommend the community-audited code of OpenZeppelin. 11 | 12 | In any case, it is crucial to work together with somebody who has experience deploying and managing smart contracts. 13 | 14 | ## Why does the fund not implement the withdraw pattern? 15 | The commonly used [withdraw pattern](http://solidity.readthedocs.io/en/v0.4.21/common-patterns.html#withdrawal-from-contracts) is an immensly useful pattern and agree it is best practice. Usually the main reason for using a withdraw pattern is to avoid having state changes before the withdrawal which can then be reverted if the receiver's fallback function throws. 16 | 17 | In the case of the withdraw function of the fund, no complicated logic or important state changes happen before the withdrawal. If the withdrawer should purposely throw in his fallback function, nothing bad would happen besides burning some gas. 18 | 19 | If however you would e.g., change the updatePrice function to directly update the price before a withdrawal, this pattern might be useful. 20 | 21 | ## I do not understand the signature verification in the FundOperator class 22 | This is completely understandable: Manual signatures in Solidity are tricky to implement and from my experience not especially user-friendly. 23 | Basically the idea is to sign a message off-chain with the required number of private keys from the fund managers. This way you obtain a number of signatures which you then pass on split into their respective V, R and S parts which are the output of the EXDSA signing method. 24 | Within each Solidity function the purpose of the transaction is checked if it matches the signatures via the ecrecover method. 25 | 26 | The signatures used follow the [ERC191](https://github.com/ethereum/EIPs/issues/191) specification. 27 | 28 | For examples on how to sign transaction it is surely helpful to look at the tests in the test/FundOperator.test.js 29 | 30 | ## Why are you signing transactions off-chain? 31 | It is commonly seen and widespread to sign transaction on-chain for multi-signature wallets. The reasons for this is simple and justified: It is difficult to manually sign transactions off-chain without a client-software 32 | designed for signing these transaction. Gnosis multi-signature wallet can be used with standard wallet software. 33 | 34 | However signing on-chain has some restrictions that make it unsuitable for an actively managed fund: **Every transaction is very expensive.** 35 | An investment fund potentially has to send hundreds of transactions daily, which makes gas costs a big cost factor. 36 | What is so expensive about these multi-sig wallets? Let's say you have a wallet with 10 owners which requires 7 keys to send a transaction. 37 | I will make a very rough gas calculation comparing on-chain vs. off-chain for this scenario. 38 | 39 | Signing on-chain requires at least 7 transactions from the different accounts, every transaction costs at least 20.000 gas just to send alone. Additionally every signature has to be saved within the storage of the contract. 40 | Saving values on the blockchain is one of the most expensive operations: Saving a new value costs 20.000 gas. That means we have at least a gas cost of 7 * (20.000 + 20.000) = 280.000 gas. 41 | By looking at the last transactions of the Gnosis multi-sig we can expect the gas cost to be at least 50% higher in reality because of other operations called. 42 | 43 | Signing off-chain only requires a single transaction to execute an action independent of the amount of signatures needed. Furthermore there is no need to save signatures in the storage of the contract. 44 | Instead of saving a confirmation in storage we have to pay the cost for the operations of hashing and recovering the signatures of seven keys. 45 | From observing the contracts on a local-chain the gas cost for signing with 7 keys off-chain is ~120.000 gas. 46 | 47 | You shouldn't take away from this that signing off-chain is superior, quite the contrary, I believe signing on-chain is superior for most use cases. 48 | However for a high-frequency of transactions off-chain signing is crucial for reducing the upkeep. 49 | 50 | ## What are the pros of an open-ended fund compared to a closed-end fund? 51 | Closed-end funds only allow investment at one point in time, usually in the form of an ICO. Afterwards no more money can be invested in the fund. This can lead to two common scenarios: 52 | If the fund is well-managed and produces profit, shares of the fund are sold at a premium price, meaning you e.g., pay 20% more for a share then the underlying assets are actually worth. 53 | If the fund actively loses money, many investors will try to sell their shares to other people. This can lead to shares being sold for less than they are worth because investors are afraid of losing more. 54 | 55 | These problems are solved in an open-ended fund: If a new investor wants to buy shares they can simply send Ether to the Fund's address which then dynamically mints new tokens (shares) based on the amount send. 56 | If they fund is losing money, people can sell their shares/tokens directly to the fund and receive Ether based on the underlying assets of a share. 57 | 58 | This way you can make sure shares are never sold for significantly more or less than their underlying assets are worth. 59 | 60 | ## How am I supposed to change owners of the Fund Operator? 61 | Changing owners is not a feature planned for implementation because it increases the attack surface of the fund operator. 62 | In order to change owners you would re-deploy the set of smart contracts with new permissions. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Everything in this repository except for files in the "imgs/"-folder is licensed under the MIT License. 4 | 5 | Copyright (c) 2018 Schneider Jakob 6 | Parts of the repository are copyrighted (c) by 2016 Smart Contract Solutions, Inc. 7 | 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ERCfund 2 | [![Builds status](https://travis-ci.org/ScJa/ercfund.svg?branch=master)](https://travis-ci.org/ScJa/ercfund) 3 | [![Coverage Status](https://coveralls.io/repos/github/ScJa/ercfund/badge.svg?branch=master)](https://coveralls.io/github/ScJa/ercfund?branch=master) 4 | [![Twitter](https://img.shields.io/twitter/url/https/github.com/ScJa/ercfund.svg?style=social)](https://twitter.com/intent/tweet?text=Check%20out%20ERCFund:%20An%20open-ended%20investment%20fund%20directly%20on%20the%20blockchain:&url=https%3A%2F%2Fgithub.com%2FScJa%2Fercfund) 5 | ### An open-ended investment fund implementation on the Ethereum blockchain for managing ERC20 tokens. 6 | 7 | ![Fund architecture](./imgs/ercfund.png) 8 | 9 | 10 | ## What is ERCFund? 11 | ERCFund makes it possible to invest into an actively managed portfolio of ERC20-Tokens and Ether by introducing a **on-demand minted and burned token as the medium for shares** in the fund. 12 | Compared to some other closed-end funds (e.g., TaaS.fund, The Token Fund) you can **buy shares/tokens at any time** by simply sending Ether to the fund. 13 | These shares/tokens can also be sold at any time by calling the withdraw function of the fund which sends the corresponding value of Ether back to a specified wallet. 14 | 15 | **Fund managers can freely trade** with these tokens and make profits. Based on the assets under management the price for one token should be continuously updated. 16 | ERCFund comes with a multi-signature implementation for a fund operator contract. This implementation is especially designed for the purpose of managing a fund. 17 | It offers **cold-wallet support** and a defined set of trusted wallets for different trust levels. 18 | Additionally the multi-signature operator has a **significantly lower gas cost than traditional multi-signature wallets by moving signing transaction off-chain**. 19 | 20 | ### Vision 21 | Currently investing in a range of different cryptocurrencies requires a lot of technical knowledge. This has spawned the need for easy way to invest while reducing entry barriers. Multiple well-known and successful initiatives are on the market like Crypto20, ICONOMI, Melon, Grayscale, etc. 22 | 23 | However the sector of cryptocurrency investment is difficult to enter for smaller, independent investors who possess market knowledge and are experienced in traditional fields but simply lack the technical knowledge to offer a product in the cryptocurrency sector. 24 | 25 | ERCFund strives to provide an safe, extensible implementation of a investment fund for ERC20 tokens. Security and trust is immensly important for lesser known players. For this reason all parts of the fund are rigorously tested and reuse community-audited code from OpenZeppelin. Furthermore it provides the possibility to add external trust parties to the management team which can make prospective investors feel secure by preventing fund managers to mishandle their money. 26 | 27 | ### Architecture 28 | 29 | 30 | In the picture above you can get a rough overview of the implementation. The whole fund structure can be setup with the four Solidity classes: 31 | 32 | - FundOperator.sol 33 | - Fund.sol 34 | - FundToken.sol 35 | - FundWallet.sol 36 | 37 | #### Fund 38 | The fund is the connection piece between all of the classes. It implements the core functionality of a investment fund. The fund can manage an arbitrary number of wallets and can send Ether or ERC20-compatible tokens from these wallets. 39 | 40 | Additionally the fund lets interested individuals purchase and sell "shares" of the fund in the form of a FundToken. These tokens are dynamically created and burned based on the current price of the fund and fees of the fund. This price should be continously updated by the fund operators. 41 | 42 | #### FundToken 43 | The fund token is standard, burnable and mintable ERC20 token. The token itself does not implement any direct functionality of the fund. However it is different to normal burnable tokens in the way that only the owner (the fund) can burn tokens. 44 | 45 | #### FundOperator 46 | The FundOperator class is multi-signature contract on top of the fund class in order to introduce a layer of security. It is different to other multi-signature wallets (like the Gnosis multi-sig wallet) in several ways. 47 | 48 | Owners of the FundOperator are split into two different groups: the fund managers and the trust party. Depending on the actions called in the FundOperator class either only signatures of the fund managers are needed or signatures of both groups. Members of the trust party group could be e.g., an external audit firm, a group of significant investors or generally trusted personalities. 49 | 50 | Still, the trust party group is fully optional! 51 | 52 | The idea behind this trust structure is that the fund managers can normally trade and manage the funds within a defined trusted area (e.g., internal wallets and exchanges). If they want to introduce a new wallet or send to an untrusted wallet they have to get additional signatures. This prevents anybody managing the fund from maliciously moving Ether or tokens. 53 | 54 | Furthermore the FundOperator class implements the addition of cold wallets to the fund, which are especially safe wallets which were never connected to the internet. 55 | This is possible because signing an action for fund happens off-chain instead of on-chain (like the Gnosis multi-sig wallet). 56 | Signing on-chain has pros and cons, the main pro is, is that it is simpler to use, because you do not need a special application to sign your transactions off-chain. 57 | The cons are that transactions are more expensive, because you need to send a confirmation from key holder, also signing off-chain is arguably more secure if done right. 58 | An actively managed fund might need to make hundreds of transactions every day, signing off-chain decreases transaction cost significantly. 59 | 60 | ## FAQ 61 | 62 | #### - [I want to create a fund running on your smart contracts! How would I go about doing this?](FAQ.md#i-want-to-create-a-fund-running-on-your-smart-contracts-how-would-i-go-about-doing-this) 63 | #### - [Why does the fund not implement the withdraw pattern?](FAQ.md#why-does-the-fund-not-implement-the-withdraw-pattern) 64 | #### - [I do not understand the signature verification in the FundOperator class](FAQ.md#i-do-not-understand-the-signature-verification-in-the-fundoperator-class) 65 | #### - [Why are you signing transactions off-chain?](FAQ.md#why-are-you-signing-transactions-off-chain) 66 | #### - [What are the pros of an open-ended fund compared to a closed-end fund?](FAQ.md#what-are-the-pros-of-an-open-ended-fund-compared-to-a-closed-end-fund) 67 | #### - [How am I supposed to change owners of the Fund Operator?](FAQ.md#how-am-i-supposed-to-change-owners-of-the-fund-operator) 68 | 69 | ## Contributing 70 | 71 | I really appreciate any contributions and feedback regarding the project. Please clone the repository and make a pull request in the repository after you have changed something. 72 | 73 | Setup is really easy by simply running `npm install` in your cloned repository. 74 | Afterwards you can run tests, coverage and linting like so: 75 | 76 | ``` 77 | npm test 78 | npm run coverage 79 | npm run lint 80 | ``` 81 | 82 | Before creating a pull request make sure to run all tests and lint your code to check if all changes can be adopted. 83 | 84 | ## Special Thanks To 85 | 86 | - [OpenZeppelin](https://github.com/OpenZeppelin/zeppelin-solidity): I reused many of their community-audited code for simple functions of the fund. I find their initiative great and it was my main resource on learning how to program in Solidity. 87 | - [Christian Lundkvist's simple-multisig](https://github.com/christianlundkvist/simple-multisig): I got my inspiration for the Fund Operator's multi-sig functionality from Christian's simple-multisig wallet implementation. 88 | 89 | ![Security and Privacy Chair](./imgs/secprivlogo.png)\ 90 | For supervising my research project/master thesis and giving me tons of advice. Specifically Prof. Matteo Maffei, Clara Schneidewind and Ilya Grishchenko. 91 | 92 | ## License 93 | 94 | All parts of this repository except for the files in the "imgs/"-folder are licensed under the MIT License (some icons used cannot be relicensed). 95 | 96 | Parts of the repository are copyrighted (c) 2016 Smart Contract Solutions, Inc. (a.k.a OpenZeppelin), namely everything in the "open-zeppelin"-folders and "scripts"-folder. 97 | 98 | Icons used in the graphic above are made by Smashicons, Freepik, Vectors Market, EpicCoders and Gregor Cresnar from www.flaticon.com. 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /contracts/Fund.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.21; 2 | 3 | import "./open-zeppelin/math/SafeMath.sol"; 4 | import "./open-zeppelin/lifecycle/Pausable.sol"; 5 | import "./FundWallet.sol"; 6 | import "./FundToken.sol"; 7 | 8 | // @title Fund 9 | // @dev This is middle component of the fund and provides all the functionality needed to manage an open-ended fund. 10 | // It supports adding a mintable token for representing shares of the fund 11 | // Shares of the fund can be bought after the crowdsale has commenced at a dynamic price set by the owners. 12 | // Similarly shares can also be sold at any point for Ether (some closed-end funds do not provide this feature) 13 | // The fund supports moving Ether from a set of internal wallets. In reality this wallets would be both hot and 14 | // cold wallets if large sums are managed. For the usage of cold wallet a multi-sig management contract is required. 15 | // This fund can be managed from a simple account wallet or by a multi-sig contract. A suggestion for a multi-sig 16 | // contract with a strong focus on trust is presented in the FundOperator contract. 17 | // This class currently provides no support for initial crowdsales, but can be easily extended. 18 | // @author ScJa 19 | contract Fund is FundWallet, Pausable { 20 | using SafeMath for uint256; 21 | 22 | // Fee which is subtracted on any withdraw. Amount requested for withdraw * withdraw fee = Amount received by the 23 | // account withdrawing. This means a withdraw fee of 3% means WITHDRAW_FEE should be 97. 24 | uint256 constant public WITHDRAW_FEE = 97; 25 | // Fee which is subtracted on any purchase. Calculation works the same as for WITHDRAW_FEE 26 | uint256 constant public PURCHASE_FEE = 97; 27 | 28 | // Represents the shares of the fund 29 | FundToken public token; 30 | 31 | // Struct which is used to save the conversion rate from tokens to wei 32 | // Wei * numerator / denominator = tokens 33 | struct Price { 34 | uint256 numerator; 35 | uint256 denominator; 36 | } 37 | 38 | // Current price at which tokens can be bought and sold. It might be helpful to split this into 39 | // purchasePrice and withdrawPrice. 40 | Price public currentPrice; 41 | 42 | // @dev Ensures that the fund has a token already added to it 43 | modifier hasToken { 44 | require(address(token) != address(0)); 45 | _; 46 | } 47 | 48 | // @dev Ensures that the price for tokens is initiated an not zero 49 | modifier priceSet { 50 | require(currentPrice.numerator != 0); 51 | require(currentPrice.denominator != 0); 52 | _; 53 | } 54 | 55 | // @dev Ensures that the address given is not the zero address 56 | // @param _address Address checked 57 | modifier notNull(address _address) { 58 | require(_address != address(0)); 59 | _; 60 | } 61 | 62 | // @dev Event which logs the purchase of tokens 63 | // @param from Address which purchased the tokens 64 | // @param to Address which received the tokens bought by "from" 65 | // @param tokensPurchased Amount of tokens purchased 66 | // @param etherReceived Amount of wei used to purchase the tokens. This wei is received by the fund. 67 | event Purchase(address indexed from, address indexed to, uint256 tokensPurchased, uint256 etherReceived); 68 | 69 | // @dev Event which logs the withdrawal/selling of tokens for Ether 70 | // @param from Address from which tokens are sold from 71 | // @param to Address to which the Ether received for the withdrawal is sent to 72 | // @param tokensWithdrawn Amount of tokens withdrawn/sold 73 | // @param etherSent Amount of wei received in exchange for the tokens sold. Amount of wei sent out by the fund. 74 | event Withdrawal(address indexed from, address indexed to, uint256 tokensWithdrawn, uint256 etherSent); 75 | 76 | // @dev Logs when a withdrawal fails because of insufficient balance of the fund 77 | // @param from Address from which tokens should be sold from 78 | // @param to Address to which the Ether received for the withdrawal should be sent to 79 | // @param tokensWithdrawn Amount of tokens which should have been withdrawn/sold 80 | // @param etherSent Amount of wei which would have been received in exchange for the tokens sold. 81 | // Amount of wei which would have been sent out by the fund. 82 | event FailedWithdrawal(address indexed from, address indexed to, uint256 tokensWithdrawn, uint256 etherSent); 83 | 84 | // @dev Logs the updates of the price for tokens. numerator/denominator = price 85 | // @param numerator The new numerator for the price 86 | // @param denominator The new denominator for the price 87 | event PriceUpdate(uint256 numerator, uint256 denominator); 88 | 89 | // @dev Logs the movement of Ether between wallets 90 | // @param from Wallet from which the Ether was sent 91 | // @param to Address to which the Ether was sent 92 | // @param value Amount of wei sent 93 | event EtherMoved(address indexed from, address indexed to, uint256 value); 94 | 95 | // @dev Logs the movement of tokens between wallets 96 | // @param token The address to the ERC20 compatible token of which tokens are moved 97 | // @param from Address from which tokens were sent 98 | // @param to Address to which the tokens were sent 99 | // @param value Amount of tokens sent 100 | event TokensMoved(address indexed token, address indexed from, address indexed to, uint256 value); 101 | 102 | // @dev Logs when a token was added to the fund. Can only be emitted once 103 | // @param token Address of the token which is added to fund as a representation of shares. 104 | event FundTokenAdded(address token); 105 | 106 | // @dev Simple constructor which only adds the owner 107 | // @param _owner Address of the owner of the fund 108 | function Fund(address _owner) FundWallet( _owner) 109 | public 110 | { 111 | } 112 | 113 | // @dev Function to add a token to the fund in order to represent shares of the fund 114 | // @param _token FundToken which will be used to represent shares 115 | function addToken(FundToken _token) 116 | public 117 | onlyOwner 118 | notNull(_token) 119 | { 120 | require(token == address(0)); 121 | token = _token; 122 | emit FundTokenAdded(address(token)); 123 | } 124 | 125 | // @dev Withdraw function which is used to sell your shares/tokens of the fund in exchange for Ether 126 | // The amount of Ether received in exchange for tokens is calculated based on the current price and withdraw fee 127 | // In the case of a successful withdrawal the tokens received are burned. 128 | // Can fail if the fund does no have enough Ether 129 | // Purposely no withdrawal pattern was implemented because the use case is simple enough here IMO 130 | // @param _to Address to which the Ether received in exchange for the tokens is sent to 131 | // @param _value Amount of tokens to withdrawn/sold 132 | function withdrawTo(address _to, uint256 _value) 133 | external 134 | hasToken 135 | whenNotPaused 136 | priceSet 137 | notNull(_to) 138 | { 139 | require(_value != 0); 140 | require(token.balanceOf(msg.sender) >= _value); 141 | address requestor = msg.sender; 142 | uint256 convertedValue = currentPrice.denominator.mul(_value).div(currentPrice.numerator); 143 | uint256 withdrawValue = convertedValue.mul(WITHDRAW_FEE).div(100); 144 | if (address(this).balance >= withdrawValue) { 145 | token.burn(requestor, _value); 146 | _to.transfer(withdrawValue); 147 | emit Withdrawal(requestor, _to, _value, withdrawValue); 148 | } else { 149 | emit FailedWithdrawal(requestor, _to, _value, withdrawValue); 150 | } 151 | } 152 | 153 | // @dev Purchase function which is used to buy shares/tokens in exchange for Ether 154 | // The amount of tokens received in exchange for Ether is calculated based on the current price and purchase fee 155 | // @param _to Address to which the purchased tokens will be credited to. 156 | function buyTo(address _to) 157 | public 158 | payable 159 | hasToken 160 | whenNotPaused 161 | priceSet 162 | notNull(_to) 163 | { 164 | require(msg.value != 0); 165 | uint256 convertedValue = msg.value.mul(currentPrice.numerator).div(currentPrice.denominator); 166 | uint256 purchaseValue = convertedValue.mul(PURCHASE_FEE).div(100); 167 | token.mint(_to, purchaseValue); 168 | emit Purchase(msg.sender, _to, purchaseValue, msg.value); 169 | } 170 | 171 | // @dev Simple function which updates the current price of the tokens/shares 172 | // @param _numerator Numerator of the currentPrice 173 | // @param _denominator Denominator of the currentPrice 174 | function updatePrice(uint256 _numerator, uint256 _denominator) 175 | public 176 | onlyOwner 177 | { 178 | require(_numerator != 0); 179 | require(_denominator != 0); 180 | currentPrice.numerator = _numerator; 181 | currentPrice.denominator = _denominator; 182 | emit PriceUpdate(_numerator, _denominator); 183 | } 184 | 185 | // @dev Initiates a token transfer between two wallets 186 | // @param _token ERC20-compatible token of which tokens are moved 187 | // @param _from Address from which tokens are sent from 188 | // @param _to Address to which the tokens are sent to 189 | // @param _value Amount of tokens sent 190 | function moveTokens(ERC20 _token, FundWallet _from, address _to, uint256 _value) 191 | public 192 | onlyOwner 193 | { 194 | _from.sendTokens(_token, _to, _value); 195 | emit TokensMoved(address(_token), _from, _to, _value); 196 | } 197 | 198 | // @dev Initiates an Ether transfer between two wallets 199 | // @param _from Address from which the Ether is sent from 200 | // @param _to Address to which the Ether is sent 201 | // @param _value Amount of wei sent 202 | function moveEther(FundWallet _from, address _to, uint256 _value) 203 | public 204 | onlyOwner 205 | { 206 | _from.sendEther(_to, _value); 207 | emit EtherMoved(_from, _to, _value); 208 | } 209 | 210 | // @dev Payable function which is used to add funds. 211 | function addFunds() 212 | public 213 | payable 214 | { 215 | } 216 | 217 | // @dev Payable function which automatically initiates the purchase process for the sender 218 | // This functionality is implement to be more user-friendly and make the purchase process easier. 219 | function () 220 | public 221 | payable 222 | { 223 | buyTo(msg.sender); 224 | } 225 | 226 | } 227 | -------------------------------------------------------------------------------- /contracts/FundOperator.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.21; 2 | 3 | import "./open-zeppelin/math/SafeMath.sol"; 4 | import "./FundToken.sol"; 5 | import "./FundWallet.sol"; 6 | import "./FundToken.sol"; 7 | import "./Fund.sol"; 8 | 9 | // @title FundOperator 10 | // @dev This class is a suggestion for an implementation of a multi-sig operator of the Fund class 11 | // Every function of the Fund class can be called from here, therefore this class should be used as the owner of the Fund 12 | // The multi-sig verification implemented here puts a special focus by implementing two different sets of keys 13 | // used to access the operator. The hotAccounts represent the fund managers while the trustPartyAccounts represent 14 | // external trust accounts such as service providers, exchanged or known personalities. 15 | // The idea is that the normal management of the fund is possible by the fund managers, but if the fund managers 16 | // want to send fund to an unknown/untrusted wallet the transaction has to be confirmed by trust parties as well 17 | // This functionality offers a strong argument for trust and security because the fund managers cannot take of with the money 18 | // However this functionality can be disabled by setting the trust threshold to zero. 19 | // The multi-signature verification method is different from common patterns used by e.g. the Gnosis multi-sig wallet 20 | // All the signing happens off-chain which reduces gas cost drastically because only one transaction is needed to 21 | // authorize an action. 22 | // The signatures used here follow the ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191 23 | // Every function in this class requires signatures in the form of v, r, s. These signatures have to match the 24 | // purpose of the action. These parameters are not described for every function. 25 | // The format in which the signatures have to be given are an array which contains all signatures as follows: 26 | // | hotSignatures | trustPartySignatures (if necessary) | coldSignature (if necessary | 27 | // To see how to sign a transaction in the correct way check out the test cases in ../test/FundOperator.test.js 28 | // @author ScJa 29 | contract FundOperator { 30 | using SafeMath for uint256; 31 | 32 | // Enum which is used to uniquely identify every Action the operator can authorize. This is used for signatures. 33 | enum Action {AddFund, AddToken, AddTrustedWallets, AddColdWallet, EtherTransfer, TokenTransfer, PriceUpdate, Pause} 34 | 35 | // The fund which is managed by the operator 36 | Fund public fund; 37 | 38 | // Used for multi-signature purposes to uniquely identify every transaction 39 | uint256 public nonce; 40 | 41 | // Amount of signatures required from hot accounts for every transaction 42 | uint256 public hotThreshold; 43 | 44 | // Amount of signatures required for special actions which are not part of every day management actions 45 | uint256 public trustPartyThreshold; 46 | 47 | // Saves all hot accounts which manage the fund 48 | mapping(address => bool) public isHotAccount; 49 | 50 | // Saves all trust party accounts 51 | mapping(address => bool) public isTrustPartyAccount; 52 | 53 | // Saves all cold wallets used by the fund and the cold key used to protect the wallet. 54 | // Format: (Wallet storing funds) => (cold account required for access) 55 | mapping(address => address) public coldStorage; 56 | 57 | 58 | // Used for storing funds which can be quickly accessed 59 | mapping(address => bool) public isHotWallet; 60 | 61 | // Set of wallets which includes all wallets owned by the fund but can also contain other trusted such as exchanges 62 | mapping(address => bool) public isTrustedWallet; 63 | 64 | // The following five arrays are not used internally but added for public access to all addresses used 65 | address[] public hotAccounts; 66 | address[] public coldAccounts; 67 | address[] public trustPartyAccounts; 68 | FundWallet[] public hotWallets; 69 | FundWallet[] public coldWallets; 70 | 71 | // @dev Logs which fund is added for the operator to manage. Can only be emitted once 72 | // @param fund Address to fund managed 73 | event FundAdded(address fund); 74 | 75 | // @dev Logs the addition of the token added to the fund which represents the shares of the fund 76 | // @param token Address to the token added to the fund 77 | event FundTokenAuthorized(address token); 78 | 79 | // @dev Logs all hot wallets added to the operator 80 | // @param wallet Address added as a hot wallet 81 | event HotWalletAdded(address indexed wallet); 82 | 83 | // @dev Logs cold wallets added to the operator 84 | // @param wallet Address to the cold wallet 85 | // @param key Address to the cold key which is needed to unlock the wallet. 86 | event ColdWalletAdded(address indexed wallet, address indexed key); 87 | 88 | // @dev Logs when a trusted wallet is added. Both the addition of hot wallets and cold wallets will also emit this event. 89 | // @param wallet Address to the trusted wallet 90 | event TrustedWalletAdded(address indexed wallet); 91 | 92 | // @dev Logs whenever a cold wallet is accessed in a transfer 93 | // @param wallet Address of the cold wallet accessed 94 | event ColdWalletAccessed(address indexed wallet); 95 | 96 | // @dev Logs whenever a transfer of Ether is authorized 97 | // @param from Wallet from which the Ether was sent 98 | // @param to Address to which the Ether was sent 99 | // @param value Amount of wei sent 100 | event EtherTransferAuthorized(address indexed from, address indexed to, uint256 value); 101 | 102 | // @dev Logs the authorization of a transfer of tokens 103 | // @param token The address to the ERC20 compatible token of which tokens are moved 104 | // @param from Address from which tokens were sent 105 | // @param to Address to which the tokens were sent 106 | // @param value Amount of tokens sent 107 | event TokenTransferAuthorized(address indexed token, address indexed from, address indexed to, uint256 value); 108 | 109 | // @dev Logs the authorization of a price update 110 | // @param numerator The new numerator for the price 111 | // @param denominator The new denominator for the price 112 | event PriceUpdateAuthorized(uint256 numerator, uint256 denominator); 113 | 114 | // @dev Logs the authorization of the pausing of the fund 115 | event PauseAuthorized(); 116 | 117 | // @dev Logs the authorization of the unpausing of the fund 118 | event UnpauseAuthorized(); 119 | 120 | // @dev Verifies the operator already has a fund added 121 | modifier hasFund() { 122 | require(address(fund) != 0); 123 | _; 124 | } 125 | 126 | // @dev Verifies an address is not the zero address 127 | // @param _address 128 | modifier notNull(address _address) { 129 | require(_address != 0); 130 | _; 131 | } 132 | 133 | // @dev Constructor which adds all hot accounts and trust party accounts to the fund. No more accounts of this 134 | // type can be added at a later point. To change them redeploy a new version of all components. 135 | // Does not allow for any duplicates. No trustparty accounts have to be added. 136 | // @param _hotTreshold Amount of signatures from hot accounts needed to confirm every action in this class 137 | // @param _trustPartyThreshold Amount of signatures needed from trust parties for extraordinary actions 138 | // @param _hotAccounts Array of addresses of hot accounts 139 | // @param _trustPartyAccounts Array of addresses of trust party accounts 140 | function FundOperator (uint256 _hotThreshold, uint256 _trustPartyThreshold, address[] _hotAccounts, address[] _trustPartyAccounts) 141 | public 142 | { 143 | require(_hotAccounts.length <= 10 && _hotAccounts.length != 0); 144 | require(_trustPartyAccounts.length <= 10); 145 | require(_hotThreshold <= _hotAccounts.length && _hotThreshold != 0); 146 | require(_trustPartyThreshold <= _trustPartyAccounts.length); 147 | 148 | for (uint256 i = 0; i < _hotAccounts.length; i++) { 149 | require(!isHotAccount[_hotAccounts[i]] && _hotAccounts[i] != 0); 150 | isHotAccount[_hotAccounts[i]] = true; 151 | } 152 | 153 | for (i = 0; i < _trustPartyAccounts.length; i++) { 154 | require(!isHotAccount[_trustPartyAccounts[i]]); 155 | require(!isTrustPartyAccount[_trustPartyAccounts[i]] && _trustPartyAccounts[i] != 0); 156 | isTrustPartyAccount[_trustPartyAccounts[i]] = true; 157 | } 158 | 159 | hotThreshold = _hotThreshold; 160 | trustPartyThreshold = _trustPartyThreshold; 161 | hotAccounts = _hotAccounts; 162 | trustPartyAccounts = _trustPartyAccounts; 163 | } 164 | 165 | // @dev Adds the fund to be managed to the operator 166 | // Requires trust party to sign 167 | // @param _fund Address to the fund to be added/managed 168 | function addFund(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, Fund _fund) 169 | external 170 | notNull(_fund) 171 | { 172 | require(address(fund) == 0); 173 | bytes32 preHash = keccak256(this, uint256(Action.AddFund), _fund, nonce); 174 | _verifyHotAction(_sigV, _sigR, _sigS, preHash); 175 | _verifyTrustPartyAction(_sigV, _sigR, _sigS, preHash); 176 | fund = _fund; 177 | isHotWallet[fund] = true; 178 | isTrustedWallet[fund] = true; 179 | emit FundAdded(fund); 180 | nonce = nonce.add(1); 181 | } 182 | 183 | // @dev Adds the token to the fund managed by the operator 184 | // Requires trust party to sign 185 | // @param _token Address of the token to be added 186 | function addToken(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, FundToken _token) 187 | external 188 | hasFund 189 | { 190 | bytes32 preHash = keccak256(this, uint256(Action.AddToken), _token, nonce); 191 | _verifyHotAction(_sigV, _sigR, _sigS, preHash); 192 | _verifyTrustPartyAction(_sigV, _sigR, _sigS, preHash); 193 | emit FundTokenAuthorized(_token); 194 | nonce = nonce.add(1); 195 | fund.addToken(_token); 196 | } 197 | 198 | // @dev Adds an array of trusted wallets or hot wallets. Hot wallets are hot and trusted 199 | // Requires trust party to sign 200 | // @param _wallets Array of wallets to add to the fund 201 | // @param _hot Says whether the wallets added should be both hot and trusted or only trusted 202 | function addTrustedWallets(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, FundWallet[] _wallets, bool _hot) 203 | external 204 | hasFund 205 | { 206 | bytes32 preHash = keccak256(this, uint256(Action.AddTrustedWallets), _wallets, _hot, nonce); 207 | _verifyHotAction(_sigV, _sigR, _sigS, preHash); 208 | _verifyTrustPartyAction(_sigV, _sigR, _sigS, preHash); 209 | for (uint256 i = 0; i < _wallets.length; i++) { 210 | require(!isTrustedWallet[_wallets[i]]); 211 | require(_wallets[i].owner() == address(fund)); 212 | isTrustedWallet[_wallets[i]] = true; 213 | emit TrustedWalletAdded(_wallets[i]); 214 | if (_hot) { 215 | isHotWallet[_wallets[i]] = true; 216 | hotWallets.push(_wallets[i]); 217 | emit HotWalletAdded(_wallets[i]); 218 | } 219 | } 220 | nonce = nonce.add(1); 221 | } 222 | 223 | // @dev Adds a single cold wallet with the corresponding cold key to the fund. 224 | // Requires trsut party to sign 225 | // @param _wallet Address to the cold wallet to add 226 | // @param _key Address to the cold account needed to access the cold wallet at a later point 227 | function addColdWallet(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, FundWallet _wallet, address _key) 228 | external 229 | notNull(_wallet) 230 | { 231 | require(!isTrustedWallet[_wallet]); 232 | require(_wallet.owner() == address(fund)); 233 | bytes32 preHash = keccak256(this, uint256(Action.AddColdWallet), _wallet, _key, nonce); 234 | _verifyHotAction(_sigV, _sigR, _sigS, preHash); 235 | _verifyTrustPartyAction(_sigV, _sigR, _sigS, preHash); 236 | 237 | isTrustedWallet[_wallet] = true; 238 | coldWallets.push(_wallet); 239 | coldStorage[_wallet] = _key; 240 | coldAccounts.push(_key); 241 | 242 | _verifyColdStorageAccess(_sigV[_sigV.length - 1], _sigR[_sigR.length - 1], _sigS[_sigS.length - 1], preHash, _wallet); 243 | emit ColdWalletAdded(_wallet, _key); 244 | emit TrustedWalletAdded(_wallet); 245 | nonce = nonce.add(1); 246 | } 247 | 248 | // @dev Function is used internally to verify access whenever a transfer from a cold wallet is initiated 249 | // See test cases for an example on how the signatures can be generated correctly in Javascript 250 | // @param _preHash Hash to check the signatures against 251 | // @param _wallet Cold wallet to be accessed 252 | function _verifyColdStorageAccess(uint8 _sigV, bytes32 _sigR, bytes32 _sigS, bytes32 _preHash, address _wallet) 253 | internal 254 | { 255 | bytes memory prefix = "\x19Ethereum Signed Message:\n32"; 256 | bytes32 txHash = keccak256(prefix, _preHash); 257 | address recovered = ecrecover(txHash, _sigV, _sigR, _sigS); 258 | require(coldStorage[_wallet] == recovered); 259 | emit ColdWalletAccessed(_wallet); 260 | } 261 | 262 | // @dev Internal function to verify every action taken in this class. Check the signatures given against the 263 | // preHash to check if the all the signatures are correct. The hot signatures have to be the first elements of the 264 | // signature arrays. 265 | // See test cases for an example on how the signatures can be generated correctly in Javascript 266 | // @param _preHash Hash to check the signatures against 267 | function _verifyHotAction(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, bytes32 _preHash) 268 | view 269 | internal 270 | { 271 | require(_sigV.length >= hotThreshold); 272 | require(_sigR.length == _sigS.length && _sigR.length == _sigV.length); 273 | bytes memory prefix = "\x19Ethereum Signed Message:\n32"; 274 | bytes32 txHash = keccak256(prefix, _preHash); 275 | 276 | // Loop is ensuring that there are no duplicates by checking the addresses are strictly increasing 277 | address lastAdd = 0; 278 | for (uint256 i = 0; i < hotThreshold; i++) { 279 | address recovered = ecrecover(txHash, _sigV[i], _sigR[i], _sigS[i]); 280 | require(recovered > lastAdd); 281 | require(isHotAccount[recovered]); 282 | lastAdd = recovered; 283 | } 284 | } 285 | 286 | // @dev Internal function to verify all actions which need special confirmation from the trust parties. 287 | // The trust party signatures have to be appended after the hot signatures. 288 | // See test cases for an example on how the signatures can be generated correctly in Javascript 289 | // @param _preHash Hash to check the signatures against 290 | function _verifyTrustPartyAction(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, bytes32 _preHash) 291 | view 292 | internal 293 | { 294 | require(_sigV.length >= hotThreshold.add(trustPartyThreshold)); 295 | require(_sigR.length == _sigS.length && _sigR.length == _sigV.length); 296 | bytes memory prefix = "\x19Ethereum Signed Message:\n32"; 297 | bytes32 txHash = keccak256(prefix, _preHash); 298 | 299 | // Loop is ensuring that there are no duplicates by checking the addresses are strictly increasing 300 | address lastAdd = 0; 301 | for (uint256 i = hotThreshold; i < hotThreshold + trustPartyThreshold; i++) { 302 | address recovered = ecrecover(txHash, _sigV[i], _sigR[i], _sigS[i]); 303 | require(recovered > lastAdd); 304 | require(isTrustPartyAccount[recovered]); 305 | lastAdd = recovered; 306 | } 307 | } 308 | 309 | // @dev Internal function called for both ether and token transfer in order to reduce duplicate code 310 | // Check whether the address to send from is a wallet of the fund. 311 | // Depending on if it is sending from a cold wallet or sending to an untrusted wallet different signatures are verified 312 | // @param _preHash Hash to check signatures against 313 | // @param _from Wallet to transfer funds from 314 | // @param _to Wallet to transfer funds to 315 | // @param _value Amount of either wei or tokens to send 316 | function _verifyTransfer(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, bytes32 _preHash, FundWallet _from, address _to, uint256 _value) 317 | internal 318 | { 319 | require(isHotWallet[_from] || coldStorage[_from] != 0); 320 | require(_value > 0); 321 | _verifyHotAction(_sigV, _sigR, _sigS, _preHash); 322 | if (coldStorage[_from] != 0) { 323 | // Double length check for safety 324 | require(_sigR.length == _sigS.length && _sigR.length == _sigV.length); 325 | uint256 coldKeyPos = _sigV.length - 1; 326 | _verifyColdStorageAccess(_sigV[coldKeyPos], _sigR[coldKeyPos], _sigS[coldKeyPos], _preHash, _from); 327 | } 328 | if (!isTrustedWallet[_to]) { 329 | _verifyTrustPartyAction(_sigV, _sigR, _sigS, _preHash); 330 | } 331 | nonce = nonce.add(1); 332 | } 333 | 334 | // @dev Authorizes an Ether transfer between two wallets 335 | // If sending to an untrusted wallet requires trust party to sign 336 | // @param _from Address from which the Ether is sent from 337 | // @param _to Address to which the Ether is sent 338 | // @param _value Amount of wei sent 339 | function requestEtherTransfer(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, FundWallet _from, address _to, uint256 _value) 340 | external 341 | hasFund 342 | { 343 | bytes32 preHash = keccak256(this, uint256(Action.EtherTransfer), _from, _to, _value, nonce); 344 | _verifyTransfer(_sigV, _sigR, _sigS, preHash, _from, _to, _value); 345 | 346 | // Triggers external call 347 | fund.moveEther(FundWallet(_from), _to, _value); 348 | emit EtherTransferAuthorized(_from, _to, _value); 349 | } 350 | 351 | // @dev Authorizes a token transfer between two wallets 352 | // If sending to an untrusted wallet requires trust party to sign 353 | // @param _token ERC20-compatible token of which tokens are moved 354 | // @param _from Address from which tokens are sent from 355 | // @param _to Address to which the tokens are sent to 356 | // @param _value Amount of tokens sent 357 | function requestTokenTransfer(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, ERC20 _token, FundWallet _from, address _to, uint256 _value) 358 | external 359 | hasFund 360 | { 361 | bytes32 preHash = keccak256(this, int256(Action.TokenTransfer), _token, _from, _to, _value, nonce); 362 | _verifyTransfer(_sigV, _sigR, _sigS, preHash, _from, _to, _value); 363 | 364 | // Triggers external call 365 | fund.moveTokens(_token, _from, _to, _value); 366 | emit TokenTransferAuthorized(address(_token), _from, _to, _value); 367 | } 368 | 369 | // @dev Authorizes the fund to update the price for a token/share 370 | // @param _numerator Numerator for the new price 371 | // @param _denominator Denominator for the new price 372 | function requestPriceUpdate(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, uint256 _numerator, uint256 _denominator) 373 | external 374 | hasFund 375 | { 376 | // Double check to fail fast 377 | require(_numerator != 0 && _denominator != 0); 378 | bytes32 preHash = keccak256(this, uint256(Action.PriceUpdate), _numerator, _denominator, nonce); 379 | _verifyHotAction(_sigV, _sigR, _sigS, preHash); 380 | nonce = nonce.add(1); 381 | fund.updatePrice(_numerator, _denominator); 382 | emit PriceUpdateAuthorized(_numerator, _denominator); 383 | } 384 | 385 | // @dev Authorizes the pausing or unpausing of the fund 386 | // @param _pause If true the fund will be paused, if false the fund will be unpaused. 387 | function requestPause(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, bool _pause) 388 | external 389 | hasFund 390 | { 391 | bytes32 preHash = keccak256(this, uint256(Action.Pause), _pause, nonce); 392 | _verifyHotAction(_sigV, _sigR, _sigS, preHash); 393 | nonce = nonce.add(1); 394 | if (_pause) { 395 | fund.pause(); 396 | emit PauseAuthorized(); 397 | } else { 398 | fund.unpause(); 399 | emit UnpauseAuthorized(); 400 | } 401 | } 402 | 403 | } 404 | -------------------------------------------------------------------------------- /contracts/FundToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.21; 2 | 3 | import "./open-zeppelin/token/ERC20/MintableToken.sol"; 4 | 5 | // @title FundToken 6 | // @dev This class is a simple mintable, burnable token which is used as the basis of the fund for representing shares. 7 | // With the Fund class a token is used to represent ownership of a fraction of the underlying assets of the fund. 8 | // @author ScJa 9 | contract FundToken is MintableToken { 10 | 11 | // Name of the token 12 | string public name = "FundToken"; 13 | 14 | // Symbol of the token, also used to list it 15 | string public symbol = "FND"; 16 | 17 | // Decimals represents the fraction to which the token can be split. 18 | uint8 public decimals = 18; 19 | 20 | // @dev Event to log the burning of tokens 21 | // @param from Address from which tokens are burned 22 | // @param value Amount of tokens burned 23 | event Burn(address indexed from, uint256 value); 24 | 25 | // @dev Simple constructer which allows for dynamic owner initiation 26 | // @param _owner Address which is set as the owner of the token 27 | function FundToken(address _owner) 28 | public 29 | { 30 | owner = _owner; 31 | totalSupply_ = 0; 32 | } 33 | 34 | // @dev Slightly adapted burn function which allows only the owner of the token to burn tokens 35 | // In a normal context this would be not advised if the owner is an account wallet because it introduces absolute 36 | // power and a big security risk. A burn function like this only makes sense if the owner is a smart contract, 37 | // in our case the Fund class, which allows burning only if initiated by the _holder 38 | // @param _holder Address from which account tokens are burned from 39 | // @param _value Amount of tokens burnt 40 | function burn(address _holder, uint256 _value) 41 | external 42 | onlyOwner 43 | { 44 | // To avoid "double burning" misinformation through events 45 | require(_holder != address(0)); 46 | require(_value <= balances[_holder]); 47 | 48 | balances[_holder] = balances[_holder].sub(_value); 49 | totalSupply_ = totalSupply_.sub(_value); 50 | // Emits two events because there is no clear standard as to which one is used for burning events 51 | emit Burn(_holder, _value); 52 | emit Transfer(_holder, address(0), _value); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /contracts/FundWallet.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.21; 2 | 3 | import "./open-zeppelin/ownership/Ownable.sol"; 4 | import "./open-zeppelin/token/ERC20/ERC20.sol"; 5 | import "./Fund.sol"; 6 | 7 | 8 | // @title FundWallet 9 | // @dev The FundWallet represents one of the basic building blocks of managing Ether and tokens within the fund. 10 | // Basically the contract is a basic ERC20 compatible wallet which has some small adjustments to integrate better 11 | // with the Fund contract. 12 | // @author ScJa 13 | contract FundWallet is Ownable { 14 | 15 | // @dev Event for logging an Ether Transfer 16 | // @param to Address to which the Ether is sent to 17 | // @param value Amount sent in the course of the Ether transfer 18 | // @param balance Remaining balance of wallet after the transfer in wei 19 | event EtherSent(address indexed to, uint256 value, uint256 balance); 20 | 21 | // @dev Event for logging when Ether is received by the wallet 22 | // @param from Address from which the Ether was sent 23 | // @param value Amount received 24 | // @param balance Balance of the wallet after receiving value 25 | event Received(address indexed from, uint256 value, uint256 balance); 26 | 27 | // @dev Event for logging when a token transfer is requested 28 | // @param token Address to the ERC20-compatible token of which tokens are sent 29 | // @param to Address to which the tokens are sent to 30 | // @param value Amount of tokens sent 31 | event TokensSent(address indexed token, address indexed to, uint256 value); 32 | 33 | // @dev Slightly adjusted ownership modifier because the fund itself is also a fund wallet. This allows it to 34 | // to send funds by calling function within this class internally 35 | modifier onlyOwnerOrInternal() { 36 | require(msg.sender == owner || msg.sender == address(this)); 37 | _; 38 | } 39 | 40 | // @dev Checks if address_ is the zero address 41 | // @param _address Address to check 42 | modifier notNull(address _address) { 43 | require(_address != 0); 44 | _; 45 | } 46 | 47 | // @dev Simple constructor which allows to specify a different owner then the msg.sender 48 | // @param _owner Address which will be set to be the owner of this wallet 49 | function FundWallet(address _owner) 50 | public 51 | notNull(_owner) 52 | { 53 | owner = _owner; 54 | } 55 | 56 | 57 | // @dev Function which initiates a simple Ether transfer. It is adjusted to work well with Fund class 58 | // @param _to Address to which Ether is sent 59 | // @param _value Amount of wei sent 60 | function sendEther(address _to, uint256 _value) 61 | public 62 | onlyOwnerOrInternal 63 | notNull(_to) 64 | { 65 | require(_value > 0); 66 | 67 | // Special behaviour here which increases usability for the Fund by avoiding accidental payments instead of 68 | // purchasing tokens. 69 | if (_to == owner) { 70 | Fund fund = Fund(_to); 71 | fund.addFunds.value(_value)(); 72 | } else { 73 | // External call 74 | _to.transfer(_value); 75 | } 76 | emit EtherSent(_to, _value, address(this).balance); 77 | } 78 | 79 | // @dev Function which initiates a simple token transfer 80 | // @param _token ERC20 compatible token of which tokens are sent 81 | // @param _to Address to which tokens are sent to 82 | // @param _value Amount of token sent 83 | function sendTokens(ERC20 _token, address _to, uint256 _value) 84 | public 85 | onlyOwnerOrInternal 86 | notNull(_to) 87 | { 88 | require(_value > 0); 89 | // External call 90 | require(_token.transfer(_to, _value)); 91 | emit TokensSent(_token, _to, _value); 92 | } 93 | 94 | // @dev Default payable function which logs any Ether recieved 95 | function () 96 | public 97 | payable 98 | { 99 | emit Received(msg.sender, msg.value, address(this).balance); 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.17; 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 | function Migrations() public { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/mocks/FundOperatorMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.19; 2 | 3 | import "../FundOperator.sol"; 4 | 5 | // @title FundOperatorMock 6 | // @dev This class is only used in order to be able to test internal functions directly 7 | // @author ScJa 8 | contract FundOperatorMock is FundOperator { 9 | 10 | function FundOperatorMock(uint256 _hotThreshold, uint256 _trustPartyThreshold, address[] _hotAccounts, address[] _trustPartyAccounts) 11 | public 12 | FundOperator(_hotThreshold, _trustPartyThreshold, _hotAccounts, _trustPartyAccounts) { 13 | } 14 | 15 | function verifyHotAction(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, bytes32 _preHash) 16 | external 17 | view 18 | { 19 | _verifyHotAction(_sigV, _sigR, _sigS, _preHash); 20 | } 21 | 22 | function verifyTrustPartyAction(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, bytes32 _preHash) 23 | external 24 | view 25 | { 26 | _verifyTrustPartyAction(_sigV, _sigR, _sigS, _preHash); 27 | } 28 | 29 | function verifyColdStorageAccess(uint8 _sigV, bytes32 _sigR, bytes32 _sigS, bytes32 _preHash, address _wallet) 30 | external 31 | { 32 | _verifyColdStorageAccess(_sigV, _sigR, _sigS, _preHash, _wallet); 33 | } 34 | 35 | function verifyTransfer(uint8[] _sigV, bytes32[] _sigR, bytes32[] _sigS, bytes32 _preHash, FundWallet _from, address _to, uint256 _value) 36 | external 37 | { 38 | _verifyTransfer(_sigV, _sigR, _sigS, _preHash, _from, _to, _value); 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /contracts/open-zeppelin/examples/SimpleToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | 4 | import "../token/ERC20/StandardToken.sol"; 5 | 6 | 7 | /** 8 | * @title SimpleToken 9 | * @dev Very simple ERC20 Token example, where all tokens are pre-assigned to the creator. 10 | * Note they can later distribute these tokens as they wish using `transfer` and other 11 | * `StandardToken` functions. 12 | */ 13 | contract SimpleToken is StandardToken { 14 | 15 | string public constant name = "SimpleToken"; // solium-disable-line uppercase 16 | string public constant symbol = "SIM"; // solium-disable-line uppercase 17 | uint8 public constant decimals = 18; // solium-disable-line uppercase 18 | 19 | uint256 public constant INITIAL_SUPPLY = 10000 * (10 ** uint256(decimals)); 20 | 21 | /** 22 | * @dev Constructor that gives msg.sender all of existing tokens. 23 | */ 24 | function SimpleToken() public { 25 | totalSupply_ = INITIAL_SUPPLY; 26 | balances[msg.sender] = INITIAL_SUPPLY; 27 | Transfer(0x0, msg.sender, INITIAL_SUPPLY); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /contracts/open-zeppelin/lifecycle/Pausable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | 4 | import "../ownership/Ownable.sol"; 5 | 6 | 7 | /** 8 | * @title Pausable 9 | * @dev Base contract which allows children to implement an emergency stop mechanism. 10 | */ 11 | contract Pausable is Ownable { 12 | event Pause(); 13 | event Unpause(); 14 | 15 | bool public paused = false; 16 | 17 | 18 | /** 19 | * @dev Modifier to make a function callable only when the contract is not paused. 20 | */ 21 | modifier whenNotPaused() { 22 | require(!paused); 23 | _; 24 | } 25 | 26 | /** 27 | * @dev Modifier to make a function callable only when the contract is paused. 28 | */ 29 | modifier whenPaused() { 30 | require(paused); 31 | _; 32 | } 33 | 34 | /** 35 | * @dev called by the owner to pause, triggers stopped state 36 | */ 37 | function pause() onlyOwner whenNotPaused public { 38 | paused = true; 39 | Pause(); 40 | } 41 | 42 | /** 43 | * @dev called by the owner to unpause, returns to normal state 44 | */ 45 | function unpause() onlyOwner whenPaused public { 46 | paused = false; 47 | Unpause(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /contracts/open-zeppelin/math/SafeMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | 4 | /** 5 | * @title SafeMath 6 | * @dev Math operations with safety checks that throw on error 7 | */ 8 | library SafeMath { 9 | 10 | /** 11 | * @dev Multiplies two numbers, throws on overflow. 12 | */ 13 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 14 | if (a == 0) { 15 | return 0; 16 | } 17 | uint256 c = a * b; 18 | assert(c / a == b); 19 | return c; 20 | } 21 | 22 | /** 23 | * @dev Integer division of two numbers, truncating the quotient. 24 | */ 25 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 26 | // assert(b > 0); // Solidity automatically throws when dividing by 0 27 | uint256 c = a / b; 28 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 29 | return c; 30 | } 31 | 32 | /** 33 | * @dev Substracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend). 34 | */ 35 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 36 | assert(b <= a); 37 | return a - b; 38 | } 39 | 40 | /** 41 | * @dev Adds two numbers, throws on overflow. 42 | */ 43 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 44 | uint256 c = a + b; 45 | assert(c >= a); 46 | return c; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /contracts/open-zeppelin/ownership/Ownable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | 4 | /** 5 | * @title Ownable 6 | * @dev The Ownable contract has an owner address, and provides basic authorization control 7 | * functions, this simplifies the implementation of "user permissions". 8 | */ 9 | contract Ownable { 10 | address public owner; 11 | 12 | 13 | event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); 14 | 15 | 16 | /** 17 | * @dev The Ownable constructor sets the original `owner` of the contract to the sender 18 | * account. 19 | */ 20 | function Ownable() public { 21 | owner = msg.sender; 22 | } 23 | 24 | /** 25 | * @dev Throws if called by any account other than the owner. 26 | */ 27 | modifier onlyOwner() { 28 | require(msg.sender == owner); 29 | _; 30 | } 31 | 32 | /** 33 | * @dev Allows the current owner to transfer control of the contract to a newOwner. 34 | * @param newOwner The address to transfer ownership to. 35 | */ 36 | function transferOwnership(address newOwner) public onlyOwner { 37 | require(newOwner != address(0)); 38 | OwnershipTransferred(owner, newOwner); 39 | owner = newOwner; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /contracts/open-zeppelin/token/ERC20/BasicToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | 4 | import "./ERC20Basic.sol"; 5 | import "../../math/SafeMath.sol"; 6 | 7 | 8 | /** 9 | * @title Basic token 10 | * @dev Basic version of StandardToken, with no allowances. 11 | */ 12 | contract BasicToken is ERC20Basic { 13 | using SafeMath for uint256; 14 | 15 | mapping(address => uint256) balances; 16 | 17 | uint256 totalSupply_; 18 | 19 | /** 20 | * @dev total number of tokens in existence 21 | */ 22 | function totalSupply() public view returns (uint256) { 23 | return totalSupply_; 24 | } 25 | 26 | /** 27 | * @dev transfer token for a specified address 28 | * @param _to The address to transfer to. 29 | * @param _value The amount to be transferred. 30 | */ 31 | function transfer(address _to, uint256 _value) public returns (bool) { 32 | require(_to != address(0)); 33 | require(_value <= balances[msg.sender]); 34 | 35 | // SafeMath.sub will throw if there is not enough balance. 36 | balances[msg.sender] = balances[msg.sender].sub(_value); 37 | balances[_to] = balances[_to].add(_value); 38 | Transfer(msg.sender, _to, _value); 39 | return true; 40 | } 41 | 42 | /** 43 | * @dev Gets the balance of the specified address. 44 | * @param _owner The address to query the the balance of. 45 | * @return An uint256 representing the amount owned by the passed address. 46 | */ 47 | function balanceOf(address _owner) public view returns (uint256 balance) { 48 | return balances[_owner]; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /contracts/open-zeppelin/token/ERC20/ERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | import "./ERC20Basic.sol"; 4 | 5 | 6 | /** 7 | * @title ERC20 interface 8 | * @dev see https://github.com/ethereum/EIPs/issues/20 9 | */ 10 | contract ERC20 is ERC20Basic { 11 | function allowance(address owner, address spender) public view returns (uint256); 12 | function transferFrom(address from, address to, uint256 value) public returns (bool); 13 | function approve(address spender, uint256 value) public returns (bool); 14 | event Approval(address indexed owner, address indexed spender, uint256 value); 15 | } 16 | -------------------------------------------------------------------------------- /contracts/open-zeppelin/token/ERC20/ERC20Basic.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | 4 | /** 5 | * @title ERC20Basic 6 | * @dev Simpler version of ERC20 interface 7 | * @dev see https://github.com/ethereum/EIPs/issues/179 8 | */ 9 | contract ERC20Basic { 10 | function totalSupply() public view returns (uint256); 11 | function balanceOf(address who) public view returns (uint256); 12 | function transfer(address to, uint256 value) public returns (bool); 13 | event Transfer(address indexed from, address indexed to, uint256 value); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/open-zeppelin/token/ERC20/MintableToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | import "./StandardToken.sol"; 4 | import "../../ownership/Ownable.sol"; 5 | 6 | 7 | /** 8 | * @title Mintable token 9 | * @dev Simple ERC20 Token example, with mintable token creation 10 | * @dev Issue: * https://github.com/OpenZeppelin/zeppelin-solidity/issues/120 11 | * Based on code by TokenMarketNet: https://github.com/TokenMarketNet/ico/blob/master/contracts/MintableToken.sol 12 | */ 13 | contract MintableToken is StandardToken, Ownable { 14 | event Mint(address indexed to, uint256 amount); 15 | event MintFinished(); 16 | 17 | bool public mintingFinished = false; 18 | 19 | 20 | modifier canMint() { 21 | require(!mintingFinished); 22 | _; 23 | } 24 | 25 | /** 26 | * @dev Function to mint tokens 27 | * @param _to The address that will receive the minted tokens. 28 | * @param _amount The amount of tokens to mint. 29 | * @return A boolean that indicates if the operation was successful. 30 | */ 31 | function mint(address _to, uint256 _amount) onlyOwner canMint public returns (bool) { 32 | totalSupply_ = totalSupply_.add(_amount); 33 | balances[_to] = balances[_to].add(_amount); 34 | Mint(_to, _amount); 35 | Transfer(address(0), _to, _amount); 36 | return true; 37 | } 38 | 39 | /** 40 | * @dev Function to stop minting new tokens. 41 | * @return True if the operation was successful. 42 | */ 43 | function finishMinting() onlyOwner canMint public returns (bool) { 44 | mintingFinished = true; 45 | MintFinished(); 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /contracts/open-zeppelin/token/ERC20/StandardToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | import "./BasicToken.sol"; 4 | import "./ERC20.sol"; 5 | 6 | 7 | /** 8 | * @title Standard ERC20 token 9 | * 10 | * @dev Implementation of the basic standard token. 11 | * @dev https://github.com/ethereum/EIPs/issues/20 12 | * @dev Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol 13 | */ 14 | contract StandardToken is ERC20, BasicToken { 15 | 16 | mapping (address => mapping (address => uint256)) internal allowed; 17 | 18 | 19 | /** 20 | * @dev Transfer tokens from one address to another 21 | * @param _from address The address which you want to send tokens from 22 | * @param _to address The address which you want to transfer to 23 | * @param _value uint256 the amount of tokens to be transferred 24 | */ 25 | function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { 26 | require(_to != address(0)); 27 | require(_value <= balances[_from]); 28 | require(_value <= allowed[_from][msg.sender]); 29 | 30 | balances[_from] = balances[_from].sub(_value); 31 | balances[_to] = balances[_to].add(_value); 32 | allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); 33 | Transfer(_from, _to, _value); 34 | return true; 35 | } 36 | 37 | /** 38 | * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. 39 | * 40 | * Beware that changing an allowance with this method brings the risk that someone may use both the old 41 | * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this 42 | * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: 43 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 44 | * @param _spender The address which will spend the funds. 45 | * @param _value The amount of tokens to be spent. 46 | */ 47 | function approve(address _spender, uint256 _value) public returns (bool) { 48 | allowed[msg.sender][_spender] = _value; 49 | Approval(msg.sender, _spender, _value); 50 | return true; 51 | } 52 | 53 | /** 54 | * @dev Function to check the amount of tokens that an owner allowed to a spender. 55 | * @param _owner address The address which owns the funds. 56 | * @param _spender address The address which will spend the funds. 57 | * @return A uint256 specifying the amount of tokens still available for the spender. 58 | */ 59 | function allowance(address _owner, address _spender) public view returns (uint256) { 60 | return allowed[_owner][_spender]; 61 | } 62 | 63 | /** 64 | * @dev Increase the amount of tokens that an owner allowed to a spender. 65 | * 66 | * approve should be called when allowed[_spender] == 0. To increment 67 | * allowed value is better to use this function to avoid 2 calls (and wait until 68 | * the first transaction is mined) 69 | * From MonolithDAO Token.sol 70 | * @param _spender The address which will spend the funds. 71 | * @param _addedValue The amount of tokens to increase the allowance by. 72 | */ 73 | function increaseApproval(address _spender, uint _addedValue) public returns (bool) { 74 | allowed[msg.sender][_spender] = allowed[msg.sender][_spender].add(_addedValue); 75 | Approval(msg.sender, _spender, allowed[msg.sender][_spender]); 76 | return true; 77 | } 78 | 79 | /** 80 | * @dev Decrease the amount of tokens that an owner allowed to a spender. 81 | * 82 | * approve should be called when allowed[_spender] == 0. To decrement 83 | * allowed value is better to use this function to avoid 2 calls (and wait until 84 | * the first transaction is mined) 85 | * From MonolithDAO Token.sol 86 | * @param _spender The address which will spend the funds. 87 | * @param _subtractedValue The amount of tokens to decrease the allowance by. 88 | */ 89 | function decreaseApproval(address _spender, uint _subtractedValue) public returns (bool) { 90 | uint oldValue = allowed[msg.sender][_spender]; 91 | if (_subtractedValue > oldValue) { 92 | allowed[msg.sender][_spender] = 0; 93 | } else { 94 | allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue); 95 | } 96 | Approval(msg.sender, _spender, allowed[msg.sender][_spender]); 97 | return true; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /imgs/ercfund.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScJa/ercfund/4b858793aeed3d0ba9c31fa27778368041e15b20/imgs/ercfund.png -------------------------------------------------------------------------------- /imgs/secprivlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScJa/ercfund/4b858793aeed3d0ba9c31fa27778368041e15b20/imgs/secprivlogo.png -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | module.exports = function(deployer) { 2 | }; 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ercfund", 3 | "description": "An open-ended investment fund implementation on the Ethereum blockchain for managing ERC20 tokens", 4 | "scripts": { 5 | "test": "scripts/test.sh", 6 | "lint": "solium -d .", 7 | "lint:fix": "solium -d . --fix", 8 | "coverage": "scripts/coverage.sh" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/ScJa/ercfund.git" 13 | }, 14 | "author": "Jakob Schneider @ScJa", 15 | "devDependencies": { 16 | "babel-polyfill": "^6.26.0", 17 | "babel-preset-es2015": "^6.18.0", 18 | "babel-preset-stage-2": "^6.24.1", 19 | "babel-preset-stage-3": "^6.17.0", 20 | "babel-register": "^6.23.0", 21 | "chai": "^4.1.2", 22 | "chai-as-promised": "^7.1.1", 23 | "chai-bignumber": "^2.0.2", 24 | "coveralls": "^2.13.1", 25 | "ethereumjs-testrpc": "^6.0.1", 26 | "left-pad": "^1.2.0", 27 | "mocha-lcov-reporter": "^1.3.0", 28 | "solidity-coverage": "^0.4.3", 29 | "solium": "^1.1.2", 30 | "truffle": "^4.0.0", 31 | "web3": "^0.20.6" 32 | }, 33 | "dependencies": { 34 | "solidity-sha3": "^0.4.1", 35 | "web3-utils": "^1.0.0-beta.31" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SOLIDITY_COVERAGE=true scripts/test.sh 4 | cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js 5 | -------------------------------------------------------------------------------- /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 | if [ "$SOLIDITY_COVERAGE" = true ]; then 17 | testrpc_port=8555 18 | else 19 | testrpc_port=8545 20 | fi 21 | 22 | testrpc_running() { 23 | nc -z localhost "$testrpc_port" 24 | } 25 | 26 | start_testrpc() { 27 | # We define 30 accounts with balance 1M ether. 28 | local accounts=( 29 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200,1000000000000000000000000" 30 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201,1000000000000000000000000" 31 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202,1000000000000000000000000" 32 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203,1000000000000000000000000" 33 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204,1000000000000000000000000" 34 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205,1000000000000000000000000" 35 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206,1000000000000000000000000" 36 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000" 37 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000" 38 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000" 39 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501210,1000000000000000000000000" 40 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501211,1000000000000000000000000" 41 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501212,1000000000000000000000000" 42 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501213,1000000000000000000000000" 43 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501214,1000000000000000000000000" 44 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501215,1000000000000000000000000" 45 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501216,1000000000000000000000000" 46 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501217,1000000000000000000000000" 47 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501218,1000000000000000000000000" 48 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501219,1000000000000000000000000" 49 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501220,1000000000000000000000000" 50 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501221,1000000000000000000000000" 51 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501222,1000000000000000000000000" 52 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501223,1000000000000000000000000" 53 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501224,1000000000000000000000000" 54 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501225,1000000000000000000000000" 55 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501226,1000000000000000000000000" 56 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501227,1000000000000000000000000" 57 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501228,1000000000000000000000000" 58 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501229,1000000000000000000000000" 59 | ) 60 | 61 | if [ "$SOLIDITY_COVERAGE" = true ]; then 62 | node_modules/.bin/testrpc-sc --gasLimit 0xfffffffffff --port "$testrpc_port" "${accounts[@]}" > /dev/null & 63 | else 64 | node_modules/.bin/testrpc --gasLimit 0xfffffffffff "${accounts[@]}" > /dev/null & 65 | fi 66 | 67 | testrpc_pid=$! 68 | } 69 | 70 | if testrpc_running; then 71 | echo "Using existing testrpc instance" 72 | else 73 | echo "Starting our own testrpc instance" 74 | start_testrpc 75 | fi 76 | 77 | if [ "$SOLIDITY_COVERAGE" = true ]; then 78 | node_modules/.bin/solidity-coverage || true 79 | else 80 | node_modules/.bin/truffle test "$@" 81 | fi 82 | -------------------------------------------------------------------------------- /test/Fund.test.js: -------------------------------------------------------------------------------- 1 | import ether from "./open-zeppelin/helpers/ether"; 2 | import EVMRevert from "./open-zeppelin/helpers/EVMRevert"; 3 | 4 | const BigNumber = web3.BigNumber; 5 | 6 | const should = require('chai') 7 | .use(require('chai-as-promised')) 8 | .use(require('chai-bignumber')(BigNumber)) 9 | .should(); 10 | 11 | const Fund = artifacts.require("Fund"); 12 | const FundWallet = artifacts.require("FundWallet"); 13 | const FundToken = artifacts.require("FundToken"); 14 | const SimpleToken = artifacts.require("SimpleToken"); 15 | 16 | 17 | contract("Fund", ([owner, sender, receiver]) => { 18 | const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; 19 | const initialBalance = ether(1); 20 | const amountToSend = ether(0.2); 21 | const numerator = 3, denominator = 7; 22 | let fund, token; 23 | 24 | beforeEach(async () => { 25 | fund = await Fund.new(owner, {from: sender}); 26 | }); 27 | 28 | 29 | describe("initialization", () => { 30 | it("should have the correct owner", async () => { 31 | const owner_ = await fund.owner(); 32 | owner_.should.equal(owner); 33 | }); 34 | }); 35 | 36 | describe("addToken", () => { 37 | describe("when the sender is not the owner", () => { 38 | it("should revert", async () => { 39 | token = await FundToken.new(fund.address, {from: sender}); 40 | await fund.addToken(token.address, {from: sender}).should.be.rejectedWith(EVMRevert); 41 | }); 42 | }); 43 | 44 | describe("when the sender is owner", () => { 45 | 46 | describe("when the token address is the zero address", () => { 47 | it("should revert", async () => { 48 | await fund.addToken(ZERO_ADDRESS, {from: sender}).should.be.rejectedWith(EVMRevert); 49 | }); 50 | }); 51 | 52 | describe("when given a valid token", () => { 53 | 54 | describe("when the token is already set", () => { 55 | it("should revert", async () => { 56 | let token1 = await FundToken.new(fund.address, {from: sender}); 57 | let token2 = await FundToken.new(fund.address, {from: sender}); 58 | await fund.addToken(token1.address, {from: owner}); 59 | await fund.addToken(token2.address, {from: owner}).should.be.rejectedWith(EVMRevert); 60 | }); 61 | }); 62 | 63 | describe("when the token is not yet set", () => { 64 | 65 | it("should set the token value", async () => { 66 | await fund.addToken(token.address, {from: owner}); 67 | (await fund.token()).should.equal(token.address); 68 | }); 69 | 70 | it("should emit a FundTokenAdded event", async () => { 71 | let {logs} = await fund.addToken(token.address, {from: owner}); 72 | const tokAddedEvent = logs.find(e => e.event === "FundTokenAdded"); 73 | logs.length.should.equal(1); 74 | tokAddedEvent.args.token.should.equal(token.address); 75 | }); 76 | 77 | }); 78 | 79 | }); 80 | 81 | }); 82 | 83 | }); 84 | 85 | describe("updatePrice", () => { 86 | 87 | describe("when the sender is not the owner", () => { 88 | it("should revert", async () => { 89 | await fund.updatePrice(numerator, denominator, {from: sender}).should.be.rejectedWith(EVMRevert); 90 | }); 91 | }); 92 | 93 | describe("when the sender is owner", () => { 94 | 95 | describe("when the numerator is zero", () => { 96 | it("should revert", async () => { 97 | await fund.updatePrice(0, denominator, {from: owner}).should.be.rejectedWith(EVMRevert); 98 | }); 99 | }); 100 | 101 | describe("when the denominator is zero", () => { 102 | it("should revert", async () => { 103 | await fund.updatePrice(numerator, 0, {from: owner}).should.be.rejectedWith(EVMRevert); 104 | }); 105 | }); 106 | 107 | describe("when given a valid numerator and denominator", () => { 108 | 109 | it("should set the price", async () => { 110 | await fund.updatePrice(numerator, denominator, {from: owner}); 111 | (await fund.currentPrice())[0].should.be.bignumber.equal(numerator); 112 | (await fund.currentPrice())[1].should.be.bignumber.equal(denominator); 113 | }); 114 | 115 | it("should emit a PriceUpdate event", async () => { 116 | let {logs} = await fund.updatePrice(numerator, denominator, {from: owner}); 117 | const updPriceEvent = logs.find(e => e.event === "PriceUpdate"); 118 | logs.length.should.equal(1); 119 | updPriceEvent.args.numerator.should.be.bignumber.equal(numerator); 120 | updPriceEvent.args.denominator.should.be.bignumber.equal(denominator); 121 | }); 122 | 123 | }); 124 | 125 | }); 126 | 127 | }); 128 | 129 | describe("moveEther", () => { 130 | 131 | beforeEach(async () => { 132 | await fund.addFunds({from: sender, value: initialBalance}); 133 | }); 134 | 135 | describe("when the sender is not the owner", () => { 136 | it("should revert", async () => { 137 | await fund.moveEther(fund.address, receiver, amountToSend, {from: sender}) 138 | .should.be.rejectedWith(EVMRevert); 139 | }); 140 | }); 141 | 142 | describe("when the sender is the owner", () => { 143 | 144 | describe("when ether is sent from the fund", () => { 145 | 146 | it("should move the correct amount", async () => { 147 | const balanceReceiverPre = await web3.eth.getBalance(receiver); 148 | await fund.moveEther(fund.address, receiver, amountToSend, {from: owner}); 149 | const balanceWallet = await web3.eth.getBalance(fund.address); 150 | const balanceReceiverPost = await web3.eth.getBalance(receiver); 151 | 152 | balanceWallet.should.be.bignumber.equal(initialBalance.minus(amountToSend)); 153 | balanceReceiverPost.minus(balanceReceiverPre) 154 | .should.be.bignumber.equal(amountToSend); 155 | }); 156 | 157 | it("should emit an EtherMoved event", async () => { 158 | const {logs} = await fund.moveEther(fund.address, receiver, amountToSend, {from: owner}); 159 | const ethMovedEvent = logs.find(e => e.event === "EtherMoved"); 160 | const ethSentEvent = logs.find(e => e.event === "EtherSent"); 161 | 162 | logs.length.should.equal(2); 163 | should.exist(ethMovedEvent); 164 | // SentEther event tested in FundWallet 165 | should.exist(ethSentEvent); 166 | ethMovedEvent.args.from.should.equal(fund.address); 167 | ethMovedEvent.args.to.should.equal(receiver); 168 | ethMovedEvent.args.value.should.be.bignumber.equal(amountToSend); 169 | }); 170 | 171 | }); 172 | 173 | describe("when ether is sent from a wallet owned by the fund to the fund", () => { 174 | it("should move the correct amount", async () => { 175 | let wallet = await FundWallet.new(fund.address, {from: sender}); 176 | await web3.eth.sendTransaction({from: sender, to: wallet.address, value: initialBalance}); 177 | 178 | const balanceReceiverPre = await web3.eth.getBalance(fund.address); 179 | await fund.moveEther(wallet.address, fund.address, amountToSend, {from: owner}); 180 | const balanceWallet = await web3.eth.getBalance(wallet.address); 181 | const balanceReceiverPost = await web3.eth.getBalance(fund.address); 182 | 183 | balanceWallet.should.be.bignumber.equal(initialBalance.minus(amountToSend)); 184 | balanceReceiverPost.minus(balanceReceiverPre) 185 | .should.be.bignumber.equal(amountToSend); 186 | }); 187 | }); 188 | 189 | }); 190 | 191 | }); 192 | 193 | describe("moveTokens", () => { 194 | let erc20Token; 195 | let totalSupply; 196 | let tokensToSend = 100; 197 | 198 | beforeEach(async () => { 199 | erc20Token = await SimpleToken.new({from: sender}); 200 | totalSupply = await erc20Token.INITIAL_SUPPLY(); 201 | await erc20Token.transfer(fund.address, totalSupply, {from: sender}); 202 | }); 203 | 204 | describe("when the sender is not the owner", () => { 205 | it("should revert", async () => { 206 | await fund.moveTokens(erc20Token.address, fund.address, receiver, tokensToSend, {from: sender}) 207 | .should.be.rejectedWith(EVMRevert); 208 | }); 209 | }); 210 | 211 | describe("when the sender is the owner", () => { 212 | 213 | describe("when tokens are moved from the fund", () => { 214 | 215 | it("should move the correct amount", async () => { 216 | await fund.moveTokens(erc20Token.address, fund.address, receiver, tokensToSend, {from: owner}); 217 | const balanceWallet = await erc20Token.balanceOf(fund.address); 218 | const balanceReceiver = await erc20Token.balanceOf(receiver); 219 | 220 | balanceWallet.should.be.bignumber.equal(totalSupply.minus(tokensToSend)); 221 | balanceReceiver.should.be.bignumber.equal(tokensToSend); 222 | }); 223 | 224 | it("should emit a TokensMoved event", async () => { 225 | const {logs} = await fund.moveTokens(erc20Token.address, fund.address, receiver, tokensToSend, {from: owner}); 226 | const tokMovedEvent = logs.find(e => e.event === "TokensMoved"); 227 | const tokSentEvent = logs.find(e => e.event === "TokensSent"); 228 | 229 | logs.length.should.equal(2); 230 | should.exist(tokMovedEvent); 231 | // SentEther event tested in FundWallet 232 | should.exist(tokSentEvent); 233 | tokMovedEvent.args.token.should.equal(erc20Token.address); 234 | tokMovedEvent.args.from.should.equal(fund.address); 235 | tokMovedEvent.args.to.should.equal(receiver); 236 | tokMovedEvent.args.value.should.be.bignumber.equal(tokensToSend); 237 | }); 238 | 239 | }); 240 | 241 | describe("when tokens are moved from a wallet owned by the fund", () => { 242 | it("should move the correct amount", async () => { 243 | let wallet = await FundWallet.new(fund.address, {from: sender}); 244 | await fund.moveTokens(erc20Token.address, fund.address, wallet.address, totalSupply, {from: owner}); 245 | await fund.moveTokens(erc20Token.address, wallet.address, receiver, tokensToSend, {from: owner}); 246 | const balanceWallet = await erc20Token.balanceOf(wallet.address); 247 | const balanceReceiver = await erc20Token.balanceOf(receiver); 248 | 249 | balanceWallet.should.be.bignumber.equal(totalSupply.minus(tokensToSend)); 250 | balanceReceiver.should.be.bignumber.equal(tokensToSend); 251 | }); 252 | }); 253 | 254 | }); 255 | 256 | }); 257 | 258 | describe("buyTo", () => { 259 | let purchaseFee; 260 | 261 | describe("when the fund has no token", () => { 262 | it("should revert", async () => { 263 | await fund.updatePrice(numerator, denominator); 264 | await fund.buyTo(receiver, {from: sender, value: amountToSend}).should.be.rejectedWith(EVMRevert); 265 | }); 266 | }); 267 | 268 | describe("when the price is not set", () => { 269 | it("should revert", async () => { 270 | token = await FundToken.new(fund.address, {from: sender}); 271 | await fund.addToken(token.address, {from: owner}); 272 | await fund.buyTo(receiver, {from: sender, value: amountToSend}).should.be.rejectedWith(EVMRevert); 273 | }); 274 | }); 275 | 276 | describe("when the fund has a token", () => { 277 | 278 | beforeEach(async () => { 279 | token = await FundToken.new(fund.address, {from: sender}); 280 | await fund.addToken(token.address, {from: owner}); 281 | await fund.updatePrice(numerator, denominator); 282 | purchaseFee = await fund.PURCHASE_FEE(); 283 | }); 284 | 285 | describe("when the fund is paused", () => { 286 | it("should revert", async () => { 287 | await fund.pause(); 288 | await fund.buyTo(receiver, {from: sender, value: amountToSend}).should.be.rejectedWith(EVMRevert); 289 | }); 290 | }); 291 | 292 | describe("when the address is zero", () => { 293 | it("should revert", async () => { 294 | await fund.buyTo(ZERO_ADDRESS, {from: sender, value: amountToSend}).should.be.rejectedWith(EVMRevert); 295 | }); 296 | }); 297 | 298 | describe("when the fund is unpaused, the price is set and the address is not zero", () => { 299 | 300 | describe("when the value is zero", () => { 301 | it("should revert", async () => { 302 | await fund.buyTo(receiver, {from: sender}).should.be.rejectedWith(EVMRevert); 303 | }); 304 | }); 305 | 306 | describe("when the value is not zero", () => { 307 | 308 | it("should mint the correct amount to the reciever", async () => { 309 | await fund.buyTo(receiver, {from: sender, value: amountToSend}); 310 | let convertedValue = amountToSend.mul(numerator).dividedToIntegerBy(denominator); 311 | let tokensShouldRecieve = convertedValue.mul(purchaseFee).dividedToIntegerBy(100); 312 | 313 | let recieverBal = await token.balanceOf(receiver); 314 | recieverBal.should.be.bignumber.equal(tokensShouldRecieve); 315 | }); 316 | 317 | it("should emit a Purchase event", async () => { 318 | const {logs} = await fund.buyTo(receiver, {from: sender, value: amountToSend}); 319 | let convertedValue = amountToSend.mul(numerator).dividedToIntegerBy(denominator); 320 | let tokensShouldRecieve = convertedValue.mul(purchaseFee).dividedToIntegerBy(100); 321 | 322 | const purchaseEvent = logs.find(e => e.event === "Purchase"); 323 | logs.length.should.equal(1); 324 | purchaseEvent.args.from.should.equal(sender); 325 | purchaseEvent.args.to.should.equal(receiver); 326 | purchaseEvent.args.tokensPurchased.should.be.bignumber.equal(tokensShouldRecieve); 327 | purchaseEvent.args.etherReceived.should.be.bignumber.equal(amountToSend); 328 | }); 329 | 330 | }); 331 | 332 | }); 333 | 334 | }); 335 | 336 | }); 337 | 338 | describe("withdrawTo", () => { 339 | let withdrawFee, tokensHeld; 340 | const tokensToWithdraw = 100; 341 | 342 | beforeEach(async () => { 343 | token = await FundToken.new(fund.address, {from: sender}); 344 | await fund.addToken(token.address, {from: owner}); 345 | await fund.updatePrice(numerator, denominator); 346 | 347 | withdrawFee = await fund.WITHDRAW_FEE(); 348 | let purchaseFee = await fund.PURCHASE_FEE(); 349 | 350 | let convertedValue = amountToSend.mul(numerator).dividedToIntegerBy(denominator); 351 | tokensHeld = convertedValue.mul(purchaseFee).dividedToIntegerBy(100); 352 | await fund.buyTo(sender, {from: sender, value: amountToSend}); 353 | }); 354 | 355 | describe("when the fund is paused", () => { 356 | it("should revert", async () => { 357 | await fund.pause(); 358 | await fund.withdrawTo(receiver, tokensToWithdraw, {from: sender}).should.be.rejectedWith(EVMRevert); 359 | }); 360 | }); 361 | 362 | describe("when the address is zero", () => { 363 | it("should revert", async () => { 364 | await fund.withdrawTo(ZERO_ADDRESS, tokensToWithdraw, {from: sender}).should.be.rejectedWith(EVMRevert); 365 | }); 366 | }); 367 | 368 | describe("when the fund is unpaused and the address is not zero", () => { 369 | 370 | describe("when the value is zero", () => { 371 | it("should revert", async () => { 372 | await fund.withdrawTo(receiver, 0, {from: sender}).should.be.rejectedWith(EVMRevert); 373 | }); 374 | }); 375 | 376 | describe("when the sender does not have enough tokens", () => { 377 | it("should revert", async () => { 378 | await fund.withdrawTo(receiver, tokensHeld, {from: sender}); 379 | await fund.withdrawTo(receiver, tokensToWithdraw, {from: sender}).should.be.rejectedWith(EVMRevert); 380 | }); 381 | }); 382 | 383 | describe("when the value is not zero and the sender has enough tokens", () => { 384 | 385 | describe("when the fund does not have enough ether to send", () => { 386 | 387 | it("should emit a FailedWithdrawal event", async () => { 388 | let fundBal = await web3.eth.getBalance(fund.address); 389 | await fund.moveEther(fund.address, sender, fundBal); 390 | const {logs} = await fund.withdrawTo(receiver, tokensToWithdraw, {from: sender}); 391 | let convertedValue = Math.trunc(tokensToWithdraw * denominator / numerator); 392 | let etherShouldRecieve = withdrawFee.mul(convertedValue).dividedToIntegerBy(100); 393 | 394 | const purchaseEvent = logs.find(e => e.event === "FailedWithdrawal"); 395 | logs.length.should.equal(1); 396 | purchaseEvent.args.from.should.equal(sender); 397 | purchaseEvent.args.to.should.equal(receiver); 398 | purchaseEvent.args.tokensWithdrawn.should.be.bignumber.equal(tokensToWithdraw); 399 | purchaseEvent.args.etherSent.should.be.bignumber.equal(etherShouldRecieve); 400 | }); 401 | 402 | it("should not burn any tokens", async () => { 403 | let fundBal = await web3.eth.getBalance(fund.address); 404 | let preBal = await token.balanceOf(sender); 405 | await fund.moveEther(fund.address, sender, fundBal); 406 | await fund.withdrawTo(receiver, tokensToWithdraw, {from: sender}); 407 | 408 | (await token.balanceOf(sender)).should.be.bignumber.equal(preBal); 409 | }); 410 | 411 | }); 412 | 413 | it("should burn the correct amount of tokens from the seller", async () => { 414 | let preRecieverTok = await token.balanceOf(sender); 415 | let preSupply = await token.totalSupply(); 416 | await fund.withdrawTo(receiver, tokensToWithdraw, {from: sender}); 417 | 418 | (await token.balanceOf(sender)).should.be.bignumber.equal(preRecieverTok.minus(tokensToWithdraw)); 419 | (await token.totalSupply()).should.be.bignumber.equal(preSupply.minus(tokensToWithdraw)); 420 | }); 421 | 422 | it("should send the correct amount of ether to the reciever", async () => { 423 | let preSenderBal = await web3.eth.getBalance(fund.address) 424 | let preRecieverBal = await web3.eth.getBalance(receiver); 425 | await fund.withdrawTo(receiver, tokensToWithdraw, {from: sender}); 426 | let convertedValue = Math.trunc(tokensToWithdraw * denominator / numerator); 427 | let etherShouldRecieve = withdrawFee.mul(convertedValue).dividedToIntegerBy(100); 428 | 429 | let postSenderBal = await web3.eth.getBalance(fund.address); 430 | let postRecieverBal = await web3.eth.getBalance(receiver); 431 | postSenderBal.should.be.bignumber.equal(preSenderBal.minus(etherShouldRecieve)); 432 | postRecieverBal.should.be.bignumber.equal(preRecieverBal.plus(etherShouldRecieve)); 433 | }); 434 | 435 | it("should emit a Withdrawal event", async () => { 436 | const {logs} = await fund.withdrawTo(receiver, tokensToWithdraw, {from: sender}); 437 | let convertedValue = Math.trunc(tokensToWithdraw * denominator / numerator); 438 | let etherShouldRecieve = withdrawFee.mul(convertedValue).dividedToIntegerBy(100); 439 | 440 | const purchaseEvent = logs.find(e => e.event === "Withdrawal"); 441 | logs.length.should.equal(1); 442 | purchaseEvent.args.from.should.equal(sender); 443 | purchaseEvent.args.to.should.equal(receiver); 444 | purchaseEvent.args.tokensWithdrawn.should.be.bignumber.equal(tokensToWithdraw); 445 | purchaseEvent.args.etherSent.should.be.bignumber.equal(etherShouldRecieve); 446 | }); 447 | 448 | }); 449 | 450 | }); 451 | 452 | }); 453 | 454 | describe("fallback", () => { 455 | 456 | it("should buy tokens to the message sender", async () => { 457 | token = await FundToken.new(fund.address, {from: sender}); 458 | await fund.addToken(token.address, {from: owner}); 459 | await fund.updatePrice(numerator, denominator); 460 | let purchaseFee = await fund.PURCHASE_FEE(); 461 | let withdrawFee = await fund.WITHDRAW_FEE(); 462 | await web3.eth.sendTransaction({from: sender, to: fund.address, value: amountToSend}); 463 | let convertedValue = amountToSend.mul(numerator).dividedToIntegerBy(denominator); 464 | let tokensShouldRecieve = convertedValue.mul(purchaseFee).dividedToIntegerBy(100); 465 | 466 | let recieverBal = await token.balanceOf(sender); 467 | recieverBal.should.be.bignumber.equal(tokensShouldRecieve); 468 | }); 469 | }); 470 | 471 | }); -------------------------------------------------------------------------------- /test/FundOperator.test.js: -------------------------------------------------------------------------------- 1 | import EVMRevert from "./open-zeppelin/helpers/EVMRevert"; 2 | import ether from "./open-zeppelin/helpers/ether"; 3 | 4 | const BigNumber = web3.BigNumber; 5 | const leftPad = require('left-pad'); 6 | const Web3Utils = require('web3-utils'); 7 | const should = require('chai') 8 | .use(require('chai-as-promised')) 9 | .use(require('chai-bignumber')(BigNumber)) 10 | .should(); 11 | 12 | const Fund = artifacts.require("Fund"); 13 | const FundOperator = artifacts.require("FundOperatorMock"); 14 | const FundToken = artifacts.require("FundToken"); 15 | const FundWallet = artifacts.require("FundWallet"); 16 | const SimpleToken = artifacts.require("SimpleToken"); 17 | 18 | const Actions = Object.freeze({ 19 | AddFund: 0, 20 | AddToken: 1, 21 | AddTrustedWallets: 2, 22 | AddColdWallet: 3, 23 | EtherTransfer: 4, 24 | TokenTransfer: 5, 25 | UpdatePrice: 6, 26 | Pause: 7 27 | }); 28 | 29 | 30 | // This test requires a lot of accounts, make sure to start your test environment accordingly 31 | contract("FundOperator", (accounts) => { 32 | const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; 33 | let sender = accounts[0]; 34 | let externalW = accounts[1]; 35 | let coldKey = accounts[2]; 36 | let hotAccounts = accounts.slice(5, 15); 37 | let trustAccounts = accounts.slice(15, 25); 38 | let sortedHotAccounts = hotAccounts.sort(); 39 | let sortedTrustAccounts = trustAccounts.sort(); 40 | let signees = sortedHotAccounts.slice(0, 3).concat(sortedTrustAccounts.slice(0, 3)); 41 | let operator; 42 | 43 | beforeEach(async () => { 44 | operator = await FundOperator.new(3, 3, hotAccounts, trustAccounts, {from: sender}); 45 | }); 46 | 47 | describe("initialization", () => { 48 | 49 | describe("when given more than allowed hotAccounts", () => { 50 | it("should revert", async () => { 51 | await FundOperator.new(3, 3, accounts.slice(5, 16), accounts.slice(16, 26), {from: sender}) 52 | .should.be.rejectedWith(EVMRevert); 53 | 54 | }); 55 | }); 56 | 57 | describe("when given more than allowed trustparties", () => { 58 | it("should revert", async () => { 59 | await FundOperator.new(3, 3, accounts.slice(5, 15), accounts.slice(15, 26), {from: sender}) 60 | .should.be.rejectedWith(EVMRevert); 61 | 62 | }); 63 | }); 64 | 65 | describe("when the owner threshold is set too high", () => { 66 | it("should revert", async () => { 67 | await FundOperator.new(7, 3, accounts.slice(5, 10), accounts.slice(15, 20), {from: sender}) 68 | .should.be.rejectedWith(EVMRevert); 69 | 70 | }); 71 | }); 72 | 73 | describe("when the trustparty threshold is set too high", () => { 74 | it("should revert", async () => { 75 | await FundOperator.new(3, 7, accounts.slice(5, 10), accounts.slice(15, 20), {from: sender}) 76 | .should.be.rejectedWith(EVMRevert); 77 | 78 | }); 79 | }); 80 | 81 | describe("when the zero address is given in the hot accounts", () => { 82 | it("should revert", async () => { 83 | await FundOperator.new(3, 3, accounts.slice(5, 14).concat(ZERO_ADDRESS), accounts.slice(15, 25), {from: sender}) 84 | .should.be.rejectedWith(EVMRevert); 85 | 86 | }); 87 | }); 88 | 89 | describe("when the zero address is given in the trustparty accounts", () => { 90 | it("should revert", async () => { 91 | await FundOperator.new(3, 3, accounts.slice(5, 15), accounts.slice(15, 24).concat(ZERO_ADDRESS), {from: sender}) 92 | .should.be.rejectedWith(EVMRevert); 93 | 94 | }); 95 | }); 96 | 97 | describe("when duplicate in owner array", () => { 98 | it("should revert", async () => { 99 | await FundOperator.new(3, 3, accounts.slice(5, 14).concat(accounts[5]), accounts.slice(15, 25), {from: sender}) 100 | .should.be.rejectedWith(EVMRevert); 101 | 102 | }); 103 | }); 104 | 105 | describe("when duplicate in trustparty array ", () => { 106 | it("should revert", async () => { 107 | await FundOperator.new(3, 3, accounts.slice(5, 15), accounts.slice(15, 24).concat(accounts[15]), {from: sender}) 108 | .should.be.rejectedWith(EVMRevert); 109 | 110 | }); 111 | }); 112 | 113 | describe("when duplicate of owner array in trustparty array ", () => { 114 | it("should revert", async () => { 115 | await FundOperator.new(3, 3, accounts.slice(5, 15), accounts.slice(15, 24).concat(accounts[5]), {from: sender}) 116 | .should.be.rejectedWith(EVMRevert); 117 | 118 | }); 119 | }); 120 | 121 | describe("when given a hot threshold of 0", () => { 122 | it("should revert", async () => { 123 | await FundOperator.new(0, 3, accounts.slice(5, 15), accounts.slice(15, 25), {from: sender}) 124 | .should.be.rejectedWith(EVMRevert); 125 | 126 | }); 127 | }); 128 | 129 | 130 | describe("when given 0 trustparty accounds and a trustparty threshold of 0", () => { 131 | it("should be successful", async () => { 132 | await FundOperator.new(3, 0, accounts.slice(5, 14), [], {from: sender}) 133 | .should.be.fulfilled; 134 | 135 | }); 136 | }); 137 | 138 | describe("when given 10 hotAccounts and trustparty and a threshold of 10", () => { 139 | it("should be successful", async () => { 140 | await FundOperator.new(10, 10, accounts.slice(5, 15), accounts.slice(15, 25), {from: sender}) 141 | .should.be.fulfilled; 142 | 143 | }); 144 | }); 145 | 146 | describe("when given 1 hotAccount and no trustparty and thresholds of 1 and 0 respectively", () => { 147 | it("should be successful", async () => { 148 | await FundOperator.new(1, 0, [accounts[5]], [], {from: sender}) 149 | .should.be.fulfilled; 150 | 151 | }); 152 | }); 153 | 154 | describe("when given 5 hotAccounts and trustparties and a threshold of 3 and 4 respectively", () => { 155 | it("should save all values correctly", async () => { 156 | operator = await FundOperator.new(3, 4, hotAccounts, trustAccounts, {from: sender}); 157 | let firstHotAcc = await operator.hotAccounts(0); 158 | let firstTrustAcc = await operator.trustPartyAccounts(0); 159 | let lastHotAcc = await operator.hotAccounts(4); 160 | let lastTrustAcc = await operator.trustPartyAccounts(4); 161 | 162 | (await operator.hotThreshold()).should.be.bignumber.equal(3); 163 | (await operator.trustPartyThreshold()).should.be.bignumber.equal(4); 164 | firstHotAcc.should.equal(hotAccounts[0]); 165 | firstTrustAcc.should.equal(trustAccounts[0]); 166 | lastHotAcc.should.equal(hotAccounts[4]); 167 | lastTrustAcc.should.equal(trustAccounts[4]); 168 | (await operator.isHotAccount(hotAccounts[0])).should.be.true; 169 | (await operator.isHotAccount(hotAccounts[4])).should.be.true; 170 | (await operator.isTrustPartyAccount(trustAccounts[0])).should.be.true; 171 | (await operator.isTrustPartyAccount(trustAccounts[4])).should.be.true; 172 | }); 173 | }); 174 | 175 | }); 176 | 177 | describe("verifyHotAction", () => { 178 | 179 | describe("when supplying too little correct signatures", () => { 180 | it("should revert", async () => { 181 | let dataToSign = Web3Utils.soliditySha3("Hello"); 182 | let sigs = await createSigs(dataToSign, sortedHotAccounts.slice(0, 2)); 183 | await operator.verifyHotAction(sigs.v, sigs.r, sigs.s, dataToSign).should.be.rejectedWith(EVMRevert); 184 | }); 185 | }); 186 | 187 | describe("when supplying the exact amount of wrong signatures", () => { 188 | it("should revert", async () => { 189 | let dataToSign = Web3Utils.soliditySha3("Hello"); 190 | let sigs = await createSigs(dataToSign, trustAccounts.slice(0, 3).sort()); 191 | await operator.verifyHotAction(sigs.v, sigs.r, sigs.s, dataToSign).should.be.rejectedWith(EVMRevert); 192 | }); 193 | }); 194 | 195 | describe("when supplying v, r, s arrays with different lengths", () => { 196 | it("should revert", async () => { 197 | let dataToSign = Web3Utils.soliditySha3("Hello"); 198 | let sigs = await createSigs(dataToSign, sortedHotAccounts.slice(0, 3)); 199 | await operator.verifyHotAction(sigs.v, sigs.r, sigs.s.slice(1), dataToSign).should.be.rejectedWith(EVMRevert); 200 | }); 201 | }); 202 | 203 | describe("when supplying the exact amount of correct signatures but unsorted", () => { 204 | it("should revert", async () => { 205 | let dataToSign = Web3Utils.soliditySha3("Hello"); 206 | let sigs = await createSigs(dataToSign, sortedHotAccounts.slice(0, 3).reverse()); 207 | await operator.verifyHotAction(sigs.v, sigs.r, sigs.s, dataToSign).should.be.rejectedWith(EVMRevert); 208 | }); 209 | }); 210 | 211 | describe("when supplying the exact amount of correct signatures but with a duplicate", () => { 212 | it("should revert", async () => { 213 | let dataToSign = Web3Utils.soliditySha3("Hello"); 214 | let sigs = await createSigs(dataToSign, sortedHotAccounts.slice(0, 2).concat(sortedHotAccounts[1])); 215 | await operator.verifyHotAction(sigs.v, sigs.r, sigs.s, dataToSign).should.be.rejectedWith(EVMRevert); 216 | }); 217 | }); 218 | 219 | describe("when supplying the exact amount of correct signatures", () => { 220 | it("should be successful", async () => { 221 | let dataToSign = Web3Utils.soliditySha3("Hello"); 222 | let sigs = await createSigs(dataToSign, sortedHotAccounts.slice(0, 3)); 223 | await operator.verifyHotAction(sigs.v, sigs.r, sigs.s, dataToSign).should.be.fulfilled; 224 | }); 225 | }); 226 | 227 | describe("when the hotThreshold is one and given the correct sigs", () => { 228 | it("should be successful", async () => { 229 | operator = await FundOperator.new(1, 3, hotAccounts, trustAccounts, {from: sender}); 230 | let dataToSign = Web3Utils.soliditySha3("Hello");8 231 | let sigs = await createSigs(dataToSign, [hotAccounts[0]]); 232 | await operator.verifyHotAction(sigs.v, sigs.r, sigs.s, dataToSign).should.be.fulfilled; 233 | }); 234 | }); 235 | 236 | // Should throw at a different point though 237 | describe("when supplying too many correct signatures", () => { 238 | it("should be successful", async () => { 239 | let dataToSign = Web3Utils.soliditySha3("Hello"); 240 | let sigs = await createSigs(dataToSign, sortedHotAccounts.slice(0, 4)); 241 | await operator.verifyHotAction(sigs.v, sigs.r, sigs.s, dataToSign).should.be.fulfilled; 242 | }); 243 | }); 244 | 245 | }); 246 | 247 | // Most cases are tested within the verifyHotAction already, as the functions are nearly identical 248 | describe("verifyTrustAction", () => { 249 | 250 | describe("when only the exact amount of correct signatures is given", () => { 251 | it("should revert", async () => { 252 | let dataToSign = Web3Utils.soliditySha3("Hello"); 253 | let sigs = await createSigs(dataToSign, sortedTrustAccounts.slice(0, 3)); 254 | await operator.verifyTrustPartyAction(sigs.v, sigs.r, sigs.s, dataToSign) 255 | .should.be.rejectedWith(EVMRevert); 256 | }); 257 | }); 258 | 259 | // Added in response to a critical code bug 260 | describe("when given the correct amount but only hot sigs", () => { 261 | it("should revert", async () => { 262 | let dataToSign = Web3Utils.soliditySha3("Hello"); 263 | let sigs = await createSigs(dataToSign, sortedHotAccounts.slice(0, 6)); 264 | await operator.verifyTrustPartyAction(sigs.v, sigs.r, sigs.s, dataToSign) 265 | .should.be.rejectedWith(EVMRevert); 266 | }); 267 | }); 268 | 269 | describe("when the exact amount of correct signatures is given and left-padded by hot threshold", () => { 270 | it("should be successful", async () => { 271 | let dataToSign = Web3Utils.soliditySha3("Hello"); 272 | let sigs = await createSigs(dataToSign, signees); 273 | await operator.verifyTrustPartyAction(sigs.v, sigs.r, sigs.s, dataToSign) 274 | .should.be.fulfilled; 275 | }); 276 | }); 277 | 278 | describe("when the trust treshold is 0", () => { 279 | it("should be successful even given random sigs", async () => { 280 | operator = await FundOperator.new(3, 0, hotAccounts, [], {from: sender}); 281 | let dataToSign = Web3Utils.soliditySha3("Hello"); 282 | let sigs = await createSigs(dataToSign, accounts.slice(0, 3)); 283 | await operator.verifyTrustPartyAction(sigs.v, sigs.r, sigs.s, dataToSign) 284 | .should.be.fulfilled; 285 | }); 286 | }); 287 | 288 | }); 289 | 290 | describe("verifyColdAction", () => { 291 | 292 | describe("when signed by a hot account and given a non existent wallet", () => { 293 | it("should revert", async () => { 294 | let dataToSign = Web3Utils.soliditySha3("Hello"); 295 | let sigs = await createSigs(dataToSign, [hotAccounts[0]]); 296 | await operator.verifyColdStorageAccess(sigs.v[0], sigs.r[0], sigs.s[0], dataToSign, hotAccounts[0]) 297 | .should.be.rejectedWith(EVMRevert); 298 | }); 299 | }); 300 | 301 | describe("when given the correct input", () => { 302 | it("should emit an event", async () => { 303 | let fund = await Fund.new(operator.address, {from: sender}); 304 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, 0); 305 | let sigs = await createSigs(dataToSign, signees); 306 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address); 307 | let wallet = await FundWallet.new(fund.address, {from: sender}); 308 | dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddColdWallet, wallet.address, 309 | coldKey, 1); 310 | sigs = await createSigs(dataToSign, signees.concat(coldKey)); 311 | await operator.addColdWallet(sigs.v, sigs.r, sigs.s, wallet.address, coldKey); 312 | 313 | dataToSign = Web3Utils.soliditySha3("Hello"); 314 | sigs = await createSigs(dataToSign, [coldKey]); 315 | let {logs} = await operator.verifyColdStorageAccess(sigs.v[0], sigs.r[0], sigs.s[0], dataToSign, wallet.address); 316 | const event = logs.find(e => e.event === "ColdWalletAccessed"); 317 | logs.length.should.equal(1); 318 | event.args.wallet.should.equal(wallet.address); 319 | }) 320 | }); 321 | 322 | }); 323 | 324 | // All test cases for this method are currently covered in "requestEtherTransfer" 325 | // Something to refactor in the future for cleanliness 326 | // describe("verifyTransfer", () => {}); 327 | 328 | describe("addFund", () => { 329 | let fund, sigs; 330 | 331 | beforeEach(async () => { 332 | fund = await Fund.new(operator.address, {from: sender}); 333 | }); 334 | 335 | describe("when given wrong sigs", () => { 336 | it("should revert", async () => { 337 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, 0); 338 | let wrongSignees = accounts.slice(0, 6).sort(); 339 | sigs = await createSigs(dataToSign, wrongSignees); 340 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address).should.be.rejectedWith(EVMRevert); 341 | }); 342 | }); 343 | 344 | describe("when given the zero address", () => { 345 | it("should revert", async () => { 346 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, ZERO_ADDRESS, 0); 347 | sigs = await createSigs(dataToSign, signees); 348 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address).should.be.rejectedWith(EVMRevert); 349 | }); 350 | }); 351 | 352 | describe("when the fund is already set", () => { 353 | it("should revert", async () => { 354 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, 0); 355 | sigs = await createSigs(dataToSign, signees); 356 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address); 357 | dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, 1); 358 | sigs = await createSigs(dataToSign, signees); 359 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address).should.be.rejectedWith(EVMRevert); 360 | }); 361 | }); 362 | 363 | describe("when the fund is not yet set and a correct fund address is given", () => { 364 | 365 | beforeEach(async () => { 366 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, 0); 367 | sigs = await createSigs(dataToSign, signees); 368 | }); 369 | 370 | it("should set all variables correctly and increase the nonce", async () => { 371 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address); 372 | 373 | (await operator.nonce()).should.be.bignumber.equal(1); 374 | (await operator.fund()).should.equal(fund.address); 375 | (await operator.isHotWallet(fund.address)).should.be.true; 376 | (await operator.isTrustedWallet(fund.address)).should.be.true; 377 | }); 378 | 379 | it("should emit a FundAdded event", async () => { 380 | let {logs} = await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address); 381 | const fundAddedEvent = logs.find(e => e.event === "FundAdded"); 382 | 383 | logs.length.should.equal(1); 384 | fundAddedEvent.args.fund.should.equal(fund.address); 385 | }); 386 | 387 | }); 388 | 389 | }); 390 | 391 | describe("addToken", () => { 392 | let token, fund, sigs; 393 | 394 | beforeEach(async () => { 395 | fund = await Fund.new(operator.address, {from: sender}); 396 | token = await FundToken.new(fund.address, {from: sender}); 397 | }); 398 | 399 | describe("when the fund is not added", () => { 400 | it("should revert", async () => { 401 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddToken, token.address, 0); 402 | sigs = await createSigs(dataToSign, signees); 403 | 404 | await operator.addToken(sigs.v, sigs.r, sigs.s, token.address).should.be.rejectedWith(EVMRevert); 405 | }); 406 | }); 407 | 408 | describe("when the fund is added", () => { 409 | 410 | beforeEach(async () => { 411 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, 0); 412 | sigs = await createSigs(dataToSign, signees); 413 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address); 414 | 415 | dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddToken, token.address, 1); 416 | sigs = await createSigs(dataToSign, signees); 417 | }); 418 | 419 | describe("when given wrong sigs", () => { 420 | it("should revert", async () => { 421 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddToken, token.address, 1); 422 | let wrongSignees = accounts.slice(0, 6).sort(); 423 | sigs = await createSigs(dataToSign, wrongSignees); 424 | await operator.addToken(sigs.v, sigs.r, sigs.s, token.address).should.be.rejectedWith(EVMRevert); 425 | }); 426 | }); 427 | 428 | it("should add the token and increase the nonce", async () => { 429 | await operator.addToken(sigs.v, sigs.r, sigs.s, token.address); 430 | 431 | (await fund.token()).should.equal(token.address); 432 | (await operator.nonce()).should.be.bignumber.equal(2); 433 | }); 434 | 435 | it("should emit a FundTokenAuthorized event", async () => { 436 | let {logs} = await operator.addToken(sigs.v, sigs.r, sigs.s, token.address); 437 | 438 | let event = logs.find(e => e.event === "FundTokenAuthorized"); 439 | logs.length.should.equal(1); 440 | event.args.token.should.equal(token.address); 441 | }); 442 | 443 | }); 444 | 445 | }); 446 | 447 | describe("addTrustedWallets", () => { 448 | let fund; 449 | 450 | beforeEach(async () => { 451 | fund = await Fund.new(operator.address, {from: sender}); 452 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, 0); 453 | let sigs = await createSigs(dataToSign, signees); 454 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address); 455 | }); 456 | 457 | describe("when given wrong signatures", () => { 458 | it("should revert", async () => { 459 | let hotWallets = [(await FundWallet.new(fund.address, {from: sender})).address]; 460 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddTrustedWallets, { 461 | t: "address[]", 462 | v: hotWallets 463 | }, false, 1); 464 | let sigs = await createSigs(dataToSign, sortedHotAccounts.slice(0, 3)); 465 | await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, hotWallets, false).should.be.rejectedWith(EVMRevert); 466 | }); 467 | }); 468 | 469 | describe("when given a wallet that is already a trusted wallet", () => { 470 | it("should revert", async () => { 471 | let hotWallets = [(await FundWallet.new(fund.address, {from: sender})).address]; 472 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddTrustedWallets, { 473 | t: "address[]", 474 | v: hotWallets 475 | }, false, 1); 476 | let sigs = await createSigs(dataToSign, signees); 477 | await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, hotWallets, false); 478 | 479 | dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddTrustedWallets, { 480 | t: "address[]", 481 | v: hotWallets 482 | }, false, 2); 483 | sigs = await createSigs(dataToSign, signees); 484 | await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, hotWallets, false) 485 | .should.be.rejectedWith(EVMRevert); 486 | }); 487 | }); 488 | 489 | // Also cover the no fund added test case 490 | describe("when give a wallet of which the owner is not the fund", () => { 491 | it("should revert", async () => { 492 | let hotWallets = [(await FundWallet.new(operator.address, {from: sender})).address]; 493 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddTrustedWallets, { 494 | t: "address[]", 495 | v: hotWallets 496 | }, false, 1); 497 | let sigs = await createSigs(dataToSign, signees); 498 | await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, hotWallets, false) 499 | .should.be.rejectedWith(EVMRevert); 500 | }); 501 | }); 502 | 503 | describe("when given valid wallet addresses", () => { 504 | 505 | describe("when given multiple hot wallets", () => { 506 | let wallets, sigs; 507 | 508 | beforeEach(async () => { 509 | let wallet1 = await FundWallet.new(fund.address, {from: sender}); 510 | let wallet2 = await FundWallet.new(fund.address, {from: sender}); 511 | wallets = [wallet1.address, wallet2.address]; 512 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddTrustedWallets, { 513 | t: "address[]", 514 | v: wallets 515 | }, true, 1); 516 | sigs = await createSigs(dataToSign, signees); 517 | }); 518 | 519 | it("should add the wallets correctly and increase the nonce", async () => { 520 | await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, wallets, true); 521 | 522 | (await operator.isHotWallet(wallets[0])).should.be.true; 523 | (await operator.isTrustedWallet(wallets[0])).should.be.true; 524 | (await operator.isHotWallet(wallets[1])).should.be.true; 525 | (await operator.isTrustedWallet(wallets[1])).should.be.true; 526 | (await operator.hotWallets(0)).should.equal(wallets[0]); 527 | (await operator.hotWallets(1)).should.equal(wallets[1]); 528 | (await operator.nonce()).should.be.bignumber.equal(2); 529 | }); 530 | 531 | it("should emit events", async () => { 532 | let {logs} = await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, wallets, true); 533 | 534 | let hotEvents = logs.filter(e => e.event === "HotWalletAdded"); 535 | let trustEvent = logs.filter(e => e.event === "TrustedWalletAdded"); 536 | hotEvents.length.should.equal(2) 537 | hotEvents[0].args.wallet.should.equal(wallets[0]); 538 | hotEvents[1].args.wallet.should.equal(wallets[1]); 539 | trustEvent.length.should.equal(2) 540 | trustEvent[0].args.wallet.should.equal(wallets[0]); 541 | trustEvent[1].args.wallet.should.equal(wallets[1]); 542 | }); 543 | 544 | }); 545 | 546 | describe("when given one hot wallet", () => { 547 | it("should add the wallet correctly", async () => { 548 | let hotWallets = [(await FundWallet.new(fund.address, {from: sender})).address]; 549 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddTrustedWallets, { 550 | t: "address[]", 551 | v: hotWallets 552 | }, true, 1); 553 | let sigs = await createSigs(dataToSign, signees); 554 | await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, hotWallets, true); 555 | 556 | (await operator.isHotWallet(hotWallets[0])).should.be.true; 557 | (await operator.isTrustedWallet(hotWallets[0])).should.be.true; 558 | (await operator.hotWallets(0)).should.equal(hotWallets[0]); 559 | }); 560 | }); 561 | 562 | describe("when given one trusted wallet", () => { 563 | it("should not add a hot wallet and should not emit a hot event", async () => { 564 | let trustWallets = [(await FundWallet.new(fund.address, {from: sender})).address]; 565 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddTrustedWallets, { 566 | t: "address[]", 567 | v: trustWallets 568 | }, false, 1); 569 | let sigs = await createSigs(dataToSign, signees); 570 | let {logs} = await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, trustWallets, false); 571 | let hotEvents = logs.filter(e => e.event === "HotWalletAdded"); 572 | 573 | (await operator.isHotWallet(trustWallets[0])).should.be.false; 574 | hotEvents.length.should.equal(0); 575 | }); 576 | }); 577 | 578 | }); 579 | }); 580 | 581 | describe("addColdWallet", () => { 582 | let fund, sigs, wallet; 583 | 584 | beforeEach(async () => { 585 | fund = await Fund.new(operator.address, {from: sender}); 586 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, 0); 587 | let sigs = await createSigs(dataToSign, signees); 588 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address); 589 | }); 590 | 591 | describe("when given wrong signatures", () => { 592 | it("should revert", async () => { 593 | wallet = await FundWallet.new(fund.address, {from: sender}); 594 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddColdWallet, wallet.address, 595 | coldKey, 1); 596 | sigs = await createSigs(dataToSign, signees); 597 | 598 | await operator.addColdWallet(sigs.v, sigs.r, sigs.s, wallet.address, coldKey) 599 | .should.be.rejectedWith(EVMRevert); 600 | }); 601 | }); 602 | 603 | describe("when given a wallet that is already a trusted wallet", () => { 604 | it("should revert", async () => { 605 | wallet = await FundWallet.new(fund.address, {from: sender}); 606 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddTrustedWallets, { 607 | t: "address[]", 608 | v: [wallet.address] 609 | }, false, 1); 610 | let sigs = await createSigs(dataToSign, signees); 611 | await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, [wallet.address], false); 612 | 613 | dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddColdWallet, wallet.address, 614 | coldKey, 2); 615 | sigs = await createSigs(dataToSign, signees.concat(coldKey)); 616 | await operator.addColdWallet(sigs.v, sigs.r, sigs.s, wallet.address, coldKey) 617 | .should.be.rejectedWith(EVMRevert); 618 | }); 619 | }); 620 | 621 | // Also cover the no fund added test case 622 | describe("when give a wallet of which the owner is not the fund", () => { 623 | it("should revert", async () => { 624 | wallet = await FundWallet.new(sender, {from: sender}); 625 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddColdWallet, wallet.address, 626 | coldKey, 1); 627 | sigs = await createSigs(dataToSign, signees.concat(coldKey)); 628 | 629 | await operator.addColdWallet(sigs.v, sigs.r, sigs.s, wallet.address, coldKey) 630 | .should.be.rejectedWith(EVMRevert); 631 | }); 632 | }); 633 | 634 | describe("when given a valid address", () => { 635 | 636 | beforeEach(async () => { 637 | wallet = await FundWallet.new(fund.address, {from: sender}); 638 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddColdWallet, wallet.address, 639 | coldKey, 1); 640 | sigs = await createSigs(dataToSign, signees.concat(coldKey)); 641 | }); 642 | 643 | it("should add the wallet correctly and increase the nonce", async () => { 644 | await operator.addColdWallet(sigs.v, sigs.r, sigs.s, wallet.address, coldKey); 645 | 646 | (await operator.isTrustedWallet(wallet.address)).should.be.true; 647 | (await operator.coldWallets(0)).should.equal(wallet.address); 648 | (await operator.coldStorage(wallet.address)).should.equal(coldKey); 649 | (await operator.coldAccounts(0)).should.equal(coldKey); 650 | (await operator.nonce()).should.be.bignumber.equal(2); 651 | }); 652 | 653 | it("should emit events", async () => { 654 | let {logs} = await operator.addColdWallet(sigs.v, sigs.r, sigs.s, wallet.address, coldKey); 655 | 656 | let coldEvent = logs.find(e => e.event === "ColdWalletAdded"); 657 | let trustEvent = logs.find(e => e.event === "TrustedWalletAdded"); 658 | coldEvent.args.wallet.should.equal(wallet.address); 659 | coldEvent.args.key.should.equal(coldKey); 660 | trustEvent.args.wallet.should.equal(wallet.address); 661 | }); 662 | 663 | }); 664 | 665 | }); 666 | 667 | describe("requestEtherTransfer", () => { 668 | const initialBalance = ether(1); 669 | const amountToSend = ether(0.2); 670 | let fund, hotWalletFrom, hotWalletTo, sigs, nonce; 671 | 672 | 673 | // Creates Operator and adds Fund and HotWallets to it 674 | beforeEach(async () => { 675 | // Keeping track of nonce manually to speed up tests 676 | nonce = 0; 677 | fund = await Fund.new(operator.address, {from: sender}); 678 | hotWalletFrom = await FundWallet.new(fund.address, {from: sender}); 679 | hotWalletTo = await FundWallet.new(fund.address, {from: sender}); 680 | 681 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, nonce++); 682 | sigs = await createSigs(dataToSign, signees); 683 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address); 684 | 685 | let hotWallets = [hotWalletFrom.address, hotWalletTo.address]; 686 | dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddTrustedWallets, { 687 | t: "address[]", 688 | v: hotWallets 689 | }, true, nonce++); 690 | sigs = await createSigs(dataToSign, signees); 691 | await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, hotWallets, true); 692 | await web3.eth.sendTransaction({from: sender, to: hotWalletFrom.address, value: initialBalance}); 693 | }); 694 | 695 | describe("when sending ether from a trusted wallet to a trusted wallet", () => { 696 | it("should revert", async () => { 697 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddTrustedWallets, { 698 | t: "address[]", 699 | v: [externalW] 700 | }, false, nonce++); 701 | sigs = await createSigs(dataToSign, signees); 702 | // await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, [externalW], false); 703 | await web3.eth.sendTransaction({from: sender, to: externalW, value: initialBalance}); 704 | dataToSign = Web3Utils.soliditySha3(operator.address, Actions.EtherTransfer, externalW, 705 | hotWalletTo.address, amountToSend, nonce++); 706 | sigs = await createSigs(dataToSign, signees); 707 | await operator.requestEtherTransfer(sigs.v, sigs.r, sigs.s, externalW, hotWalletTo.address, amountToSend) 708 | .should.be.rejectedWith(EVMRevert); 709 | }); 710 | }); 711 | 712 | 713 | describe("when sending ether from a hot wallet to a trusted wallet and giving only hotAcc sigs", () => { 714 | 715 | beforeEach(async () => { 716 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.EtherTransfer, hotWalletFrom.address, 717 | hotWalletTo.address, amountToSend, nonce++); 718 | sigs = await createSigs(dataToSign, sortedHotAccounts.slice(0, 3)); 719 | }); 720 | 721 | describe("when trying to send more ether than available", () => { 722 | it("should revert", async () => { 723 | await operator.requestEtherTransfer(sigs.v, sigs.r, sigs.s, hotWalletFrom.address, hotWalletTo.address, initialBalance.plus(1)) 724 | .should.be.rejectedWith(EVMRevert); 725 | }); 726 | }); 727 | 728 | describe("when given wrong signatures", () => { 729 | it("should revert", async () => { 730 | await operator.requestEtherTransfer(sigs.v, sigs.r, sigs.r, hotWalletFrom.address, hotWalletTo.address, amountToSend) 731 | .should.be.rejectedWith(EVMRevert); 732 | }); 733 | }); 734 | 735 | it("should send the ether correctly", async () => { 736 | const preWalletFromBal = await web3.eth.getBalance(hotWalletFrom.address); 737 | const preWalletToBal = await web3.eth.getBalance(hotWalletTo.address); 738 | 739 | await operator.requestEtherTransfer(sigs.v, sigs.r, sigs.s, hotWalletFrom.address, hotWalletTo.address, amountToSend); 740 | 741 | const postWalletFromBal = await web3.eth.getBalance(hotWalletFrom.address); 742 | const postWalletToBal = await web3.eth.getBalance(hotWalletTo.address); 743 | postWalletFromBal.should.be.bignumber.equal(preWalletFromBal.minus(amountToSend)); 744 | postWalletToBal.should.be.bignumber.equal(preWalletToBal.plus(amountToSend)); 745 | }); 746 | 747 | it("should increase the nonce", async () => { 748 | const preNonce = await operator.nonce(); 749 | 750 | await operator.requestEtherTransfer(sigs.v, sigs.r, sigs.s, hotWalletFrom.address, hotWalletTo.address, amountToSend); 751 | 752 | const postNonce = await operator.nonce(); 753 | postNonce.should.be.bignumber.equal(preNonce.plus(1)); 754 | }); 755 | 756 | it("should emit events", async () => { 757 | const {logs} = await operator.requestEtherTransfer(sigs.v, sigs.r, sigs.s, hotWalletFrom.address, hotWalletTo.address, amountToSend); 758 | 759 | const event = logs.find(e => e.event === "EtherTransferAuthorized"); 760 | logs.length.should.equal(1); 761 | event.args.from.should.equal(hotWalletFrom.address); 762 | event.args.to.should.equal(hotWalletTo.address); 763 | event.args.value.should.be.bignumber.equal(amountToSend); 764 | }); 765 | 766 | }); 767 | 768 | describe("when sending ether from a hot wallet to an unknown wallet", () => { 769 | 770 | describe("when only hotAccount signatures are given", () => { 771 | it("should revert", async () => { 772 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.EtherTransfer, hotWalletFrom.address, 773 | hotWalletTo.address, amountToSend, nonce); 774 | sigs = await createSigs(dataToSign, sortedHotAccounts.slice(0, 3)); 775 | await operator.requestEtherTransfer(sigs.v, sigs.r, sigs.s, hotWalletFrom.address, externalW, amountToSend) 776 | .should.be.rejectedWith(EVMRevert); 777 | }); 778 | }); 779 | 780 | describe("when hotAccount and trustparty signatures are given", () => { 781 | it("should send the ether correctly", async () => { 782 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.EtherTransfer, hotWalletFrom.address, 783 | externalW, amountToSend, nonce); 784 | sigs = await createSigs(dataToSign, signees); 785 | 786 | const preWalletFromBal = await web3.eth.getBalance(hotWalletFrom.address); 787 | const preExternalBal = await web3.eth.getBalance(externalW); 788 | 789 | await operator.requestEtherTransfer(sigs.v, sigs.r, sigs.s, hotWalletFrom.address, externalW, amountToSend); 790 | 791 | const postWalletFromBal = await web3.eth.getBalance(hotWalletFrom.address); 792 | const postExternalBal = await web3.eth.getBalance(externalW); 793 | postWalletFromBal.should.be.bignumber.equal(preWalletFromBal.minus(amountToSend)); 794 | postExternalBal.should.be.bignumber.equal(preExternalBal.plus(amountToSend)); 795 | }); 796 | }); 797 | 798 | }); 799 | 800 | describe("when sending ether from a cold wallet to a trusted wallet", () => { 801 | let coldWallet; 802 | 803 | beforeEach(async () => { 804 | coldWallet = await FundWallet.new(fund.address, {from: sender}); 805 | await web3.eth.sendTransaction({from: sender, to: coldWallet.address, value: initialBalance}); 806 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddColdWallet, coldWallet.address, 807 | coldKey, nonce++); 808 | sigs = await createSigs(dataToSign, signees.concat(coldKey)); 809 | await operator.addColdWallet(sigs.v, sigs.r, sigs.s, coldWallet.address, coldKey); 810 | }); 811 | 812 | describe("when not given a correct cold key", () => { 813 | it("should revert", async () => { 814 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.EtherTransfer, coldWallet.address, 815 | hotWalletTo.address, amountToSend, nonce++); 816 | sigs = await createSigs(dataToSign, signees); 817 | await operator.requestEtherTransfer(sigs.v, sigs.r, sigs.s, coldWallet.address, hotWalletTo.address, amountToSend) 818 | .should.be.rejectedWith(EVMRevert); 819 | }); 820 | }); 821 | 822 | describe("when given the correct cold key", () => { 823 | it("should send the ether correctly", async () => { 824 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.EtherTransfer, coldWallet.address, 825 | hotWalletTo.address, amountToSend, nonce++); 826 | sigs = await createSigs(dataToSign, signees.concat(coldKey)); 827 | const preColdBal = await web3.eth.getBalance(coldWallet.address); 828 | const preReceiverBal = await web3.eth.getBalance(hotWalletTo.address); 829 | 830 | await operator.requestEtherTransfer(sigs.v, sigs.r, sigs.s, coldWallet.address, hotWalletTo.address, 831 | amountToSend); 832 | (await web3.eth.getBalance(coldWallet.address)).should.be.bignumber 833 | .equal(preColdBal.minus(amountToSend)); 834 | (await web3.eth.getBalance(hotWalletTo.address)).should.be.bignumber 835 | .equal(preReceiverBal.plus(amountToSend)); 836 | 837 | }); 838 | 839 | }); 840 | 841 | }); 842 | 843 | }); 844 | 845 | // Many negative test cases are covered in requestEtherTransfer because the use the same function for checking 846 | // Signatures 847 | describe("requestTokenTransfer", () => { 848 | const initialBalance = 100000; 849 | const tokensToSend = 100; 850 | let fund, hotWalletFrom, hotWalletTo, token, sigs, nonce; 851 | 852 | // Creates Operator and adds Fund and HotWallets to it 853 | beforeEach(async () => { 854 | // Keeping track of nonce manually to speed up tests 855 | nonce = 0; 856 | fund = await Fund.new(operator.address, {from: sender}); 857 | hotWalletFrom = await FundWallet.new(fund.address, {from: sender}); 858 | hotWalletTo = await FundWallet.new(fund.address, {from: sender}); 859 | 860 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, nonce++); 861 | sigs = await createSigs(dataToSign, signees); 862 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address); 863 | 864 | let hotWallets = [hotWalletFrom.address, hotWalletTo.address]; 865 | dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddTrustedWallets, { 866 | t: "address[]", 867 | v: hotWallets 868 | }, true, nonce++); 869 | sigs = await createSigs(dataToSign, signees); 870 | await operator.addTrustedWallets(sigs.v, sigs.r, sigs.s, hotWallets, true); 871 | 872 | token = await SimpleToken.new({from: sender}); 873 | await token.transfer(hotWalletFrom.address, initialBalance, {from: sender}); 874 | }); 875 | 876 | describe("when sending tokens from a hot wallet to a hot wallet", () => { 877 | 878 | describe("when trying to authorize more tokens than available", () => { 879 | it("should revert", async () => { 880 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.TokenTransfer, token.address, 881 | hotWalletFrom.address, hotWalletTo.address, initialBalance + 1, nonce++); 882 | sigs = await createSigs(dataToSign, sortedHotAccounts); 883 | await operator.requestTokenTransfer(sigs.v, sigs.r, sigs.s, token.address, 884 | hotWalletFrom.address, hotWalletTo.address, initialBalance + 1) 885 | .should.be.rejectedWith(EVMRevert); 886 | }); 887 | }); 888 | 889 | describe("when sending a valid amount", () => { 890 | 891 | beforeEach(async () => { 892 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.TokenTransfer, token.address, 893 | hotWalletFrom.address, hotWalletTo.address, tokensToSend, nonce++); 894 | sigs = await createSigs(dataToSign, sortedHotAccounts); 895 | }); 896 | 897 | it("should move the right amount of tokens and increase the nonce", async () => { 898 | let preNonce = await operator.nonce(); 899 | await operator.requestTokenTransfer(sigs.v, sigs.r, sigs.s, token.address, 900 | hotWalletFrom.address, hotWalletTo.address, tokensToSend); 901 | 902 | (await token.balanceOf(hotWalletFrom.address)).should.be.bignumber.equal(initialBalance - tokensToSend); 903 | (await token.balanceOf(hotWalletTo.address)).should.be.bignumber.equal(tokensToSend); 904 | (await operator.nonce()).should.be.bignumber.equal(preNonce.plus(1)); 905 | }); 906 | 907 | it("should emit an event", async () => { 908 | let {logs} = await operator.requestTokenTransfer(sigs.v, sigs.r, sigs.s, token.address, 909 | hotWalletFrom.address, hotWalletTo.address, tokensToSend); 910 | 911 | let event = logs.find(e => e.event === "TokenTransferAuthorized"); 912 | logs.length.should.equal(1); 913 | event.args.token.should.equal(token.address); 914 | event.args.from.should.equal(hotWalletFrom.address); 915 | event.args.to.should.equal(hotWalletTo.address); 916 | event.args.value.should.be.bignumber.equal(tokensToSend); 917 | }); 918 | 919 | }); 920 | 921 | }); 922 | 923 | }); 924 | 925 | describe("requestPriceUpdate", () => { 926 | let num = 3; 927 | let den = 7; 928 | let fund, sigs; 929 | 930 | beforeEach(async () => { 931 | fund = await Fund.new(operator.address, {from: sender}); 932 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, 0); 933 | sigs = await createSigs(dataToSign, signees); 934 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address); 935 | dataToSign = Web3Utils.soliditySha3(operator.address, Actions.UpdatePrice, num, den, 1); 936 | sigs = await createSigs(dataToSign, hotAccounts); 937 | }); 938 | 939 | describe("when given valid price", () => { 940 | 941 | it("should update price and nonce", async () => { 942 | let preNonce = await operator.nonce(); 943 | await operator.requestPriceUpdate(sigs.v, sigs.r, sigs.s, num, den); 944 | 945 | let price = await fund.currentPrice(); 946 | price[0].should.be.bignumber.equal(num); 947 | price[1].should.be.bignumber.equal(den); 948 | (await operator.nonce()).should.be.bignumber.equal(preNonce.plus(1)); 949 | }); 950 | 951 | it("should emit an event", async () => { 952 | let {logs} = await operator.requestPriceUpdate(sigs.v, sigs.r, sigs.s, num, den); 953 | let event = logs.find(e => e.event === "PriceUpdateAuthorized"); 954 | logs.length.should.equal(1); 955 | event.args.numerator.should.be.bignumber.equal(num); 956 | event.args.denominator.should.be.bignumber.equal(den); 957 | }); 958 | 959 | }); 960 | 961 | }); 962 | 963 | describe("requestPause", () => { 964 | let fund, sigs; 965 | 966 | beforeEach(async () => { 967 | fund = await Fund.new(operator.address, {from: sender}); 968 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.AddFund, fund.address, 0); 969 | sigs = await createSigs(dataToSign, signees); 970 | await operator.addFund(sigs.v, sigs.r, sigs.s, fund.address); 971 | dataToSign = Web3Utils.soliditySha3(operator.address, Actions.Pause, true, 1); 972 | sigs = await createSigs(dataToSign, signees); 973 | }); 974 | 975 | describe("when pausing the fund", () => { 976 | 977 | describe("when given wrong signatures", () => { 978 | it("should revert", async () => { 979 | await operator.requestPause(sigs.v, sigs.r, sigs.r, true) 980 | .should.be.rejectedWith(EVMRevert); 981 | }); 982 | }); 983 | 984 | it("should pause the fund and increase the nonce", async () => { 985 | let preNonce = await operator.nonce(); 986 | await operator.requestPause(sigs.v, sigs.r, sigs.s, true); 987 | 988 | (await operator.nonce()).should.be.bignumber.equal(preNonce.plus(1)); 989 | (await fund.paused()).should.be.true; 990 | }); 991 | 992 | it("should emit an event", async () => { 993 | let {logs} = await operator.requestPause(sigs.v, sigs.r, sigs.s, true); 994 | let event = logs.find(e => e.event === "PauseAuthorized"); 995 | logs.length.should.equal(1); 996 | should.exist(event); 997 | }); 998 | 999 | }); 1000 | 1001 | describe("when unpausing a paused fund", () => { 1002 | 1003 | beforeEach(async () => { 1004 | await operator.requestPause(sigs.v, sigs.r, sigs.s, true); 1005 | let dataToSign = Web3Utils.soliditySha3(operator.address, Actions.Pause, false, 2); 1006 | sigs = await createSigs(dataToSign, signees); 1007 | }); 1008 | 1009 | it("should unpause the fund", async () => { 1010 | await operator.requestPause(sigs.v, sigs.r, sigs.s, false); 1011 | (await fund.paused()).should.be.false; 1012 | }); 1013 | 1014 | it("should emit an event", async () => { 1015 | let {logs} = await operator.requestPause(sigs.v, sigs.r, sigs.s, false); 1016 | let event = logs.find(e => e.event === "UnpauseAuthorized"); 1017 | logs.length.should.equal(1); 1018 | should.exist(event); 1019 | }); 1020 | 1021 | }); 1022 | 1023 | }); 1024 | 1025 | 1026 | }); 1027 | 1028 | let createSigs = async (dataToSign, accounts) => { 1029 | let r = [], s = [], v = []; 1030 | for (let i = 0; i < accounts.length; i++) { 1031 | let signature = await web3.eth.sign(accounts[i], dataToSign); 1032 | // First two bytes are 0x 1033 | r.push("0x" + signature.slice(2, 66)); 1034 | s.push("0x" + signature.slice(66, 130)); 1035 | // In order to conform with ecrecover add 27 1036 | v.push(web3.toDecimal(signature.slice(130, 132)) + 27); 1037 | } 1038 | 1039 | return {v: v, r: r, s: s}; 1040 | }; 1041 | 1042 | 1043 | -------------------------------------------------------------------------------- /test/FundToken.test.js: -------------------------------------------------------------------------------- 1 | import EVMRevert from "./open-zeppelin/helpers/EVMRevert"; 2 | 3 | const BigNumber = web3.BigNumber; 4 | 5 | const should = require('chai') 6 | .use(require('chai-as-promised')) 7 | .use(require('chai-bignumber')(BigNumber)) 8 | .should(); 9 | 10 | const FundToken = artifacts.require("FundToken"); 11 | 12 | contract("FundToken", ([sender, owner]) => { 13 | const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; 14 | 15 | let token; 16 | 17 | beforeEach(async () => { 18 | token = await FundToken.new(owner, {from: sender}); 19 | }); 20 | 21 | describe("initialization", () => { 22 | 23 | it("should have a name", async () => { 24 | const name = await token.name(); 25 | name.should.equal("FundToken"); 26 | }); 27 | 28 | it("should have a symbol", async () => { 29 | const symbol = await token.symbol(); 30 | symbol.should.equal("FND"); 31 | }); 32 | 33 | it("should have 18 decimals", async () => { 34 | const decimals = await token.decimals(); 35 | decimals.should.be.bignumber.equal("18"); 36 | }); 37 | 38 | it("should have a total supply of 0", async () => { 39 | const totalSupply = await token.totalSupply(); 40 | totalSupply.should.be.bignumber.equal(0); 41 | }); 42 | 43 | it("should have an owner", async () => { 44 | const owner_ = await token.owner(); 45 | owner_.should.equal(owner); 46 | }); 47 | 48 | }); 49 | 50 | describe("burn", () => { 51 | const initialTokens = 100; 52 | 53 | beforeEach(async () => { 54 | await token.mint(owner, initialTokens, {from: owner}); 55 | }); 56 | 57 | describe("when the sender is not the owner", () => { 58 | it("should revert", async () => { 59 | await token.burn(owner, 50, {from: sender}).should.be.rejectedWith(EVMRevert); 60 | }); 61 | }); 62 | 63 | describe("when the sender is the owner", () => { 64 | 65 | describe("when trying to burn more tokens than the address has", () => { 66 | it("should revert", async () => { 67 | await token.burn(owner, 101, {from: owner}).should.be.rejectedWith(EVMRevert); 68 | }); 69 | }); 70 | 71 | describe("when trying to burn all of the tokens an address has", () => { 72 | it("should burn all tokens", async () => { 73 | await token.burn(owner, initialTokens, {from: owner}); 74 | const balance = await token.balanceOf(owner); 75 | const totalSupply = await token.totalSupply(); 76 | 77 | balance.should.be.bignumber.equal(0); 78 | totalSupply.should.be.bignumber.equal(0); 79 | }); 80 | }); 81 | 82 | describe("when trying to burn less tokens than an address has", () => { 83 | const amountBurned = 50; 84 | 85 | describe("when trying to burn from the zero address", () => { 86 | it("should revert", async () => { 87 | await token.mint(ZERO_ADDRESS, initialTokens, {from: owner}); 88 | await token.burn(ZERO_ADDRESS, amountBurned, {from: owner}).should.be.rejectedWith(EVMRevert); 89 | }); 90 | }); 91 | 92 | it("should burn the correct amount", async () => { 93 | await token.burn(owner, amountBurned, {from: owner}); 94 | const balance = await token.balanceOf(owner); 95 | const totalSupply = await token.totalSupply(); 96 | 97 | balance.should.be.bignumber.equal(initialTokens - amountBurned); 98 | totalSupply.should.be.bignumber.equal(initialTokens - amountBurned); 99 | }); 100 | 101 | it("should emit a burn and a transfer event", async () => { 102 | const {logs} = await token.burn(owner, amountBurned, {from: owner}); 103 | const burnEvent = logs.find(e => e.event === "Burn"); 104 | const transferEvent = logs.find(e => e.event === "Transfer"); 105 | 106 | logs.length.should.equal(2); 107 | 108 | should.exist(burnEvent); 109 | burnEvent.args.from.should.equal(owner); 110 | burnEvent.args.value.should.be.bignumber.equal(amountBurned); 111 | 112 | should.exist(transferEvent); 113 | transferEvent.args.from.should.equal(owner); 114 | transferEvent.args.to.should.equal(ZERO_ADDRESS); 115 | transferEvent.args.value.should.be.bignumber.equal(amountBurned); 116 | }); 117 | 118 | }); 119 | 120 | }); 121 | 122 | }); 123 | 124 | }); 125 | -------------------------------------------------------------------------------- /test/FundWallet.test.js: -------------------------------------------------------------------------------- 1 | import EVMRevert from "./open-zeppelin/helpers/EVMRevert"; 2 | import ether from "./open-zeppelin/helpers/ether"; 3 | 4 | const BigNumber = web3.BigNumber; 5 | 6 | const should = require('chai') 7 | .use(require('chai-as-promised')) 8 | .use(require('chai-bignumber')(BigNumber)) 9 | .should(); 10 | 11 | const FundWallet = artifacts.require("FundWallet"); 12 | const SimpleToken = artifacts.require("SimpleToken"); 13 | 14 | contract("FundWallet", ([owner, sender, receiver]) => { 15 | const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; 16 | 17 | let wallet; 18 | const initialBalance = ether(1); 19 | const amountToSend = ether(0.2); 20 | 21 | beforeEach(async () => { 22 | wallet = await FundWallet.new(owner, {from: sender}); 23 | }); 24 | 25 | 26 | describe("initialization", () => { 27 | 28 | describe("when the owner is the zero address", () => { 29 | it("should revert", async () => { 30 | await FundWallet.new(ZERO_ADDRESS, {from: sender}).should.be.rejectedWith(EVMRevert); 31 | }); 32 | }); 33 | 34 | it("should have an owner", async () => { 35 | const owner_ = await wallet.owner(); 36 | owner_.should.equal(owner); 37 | }); 38 | 39 | }); 40 | 41 | describe("fallback", () => { 42 | 43 | it("should receive ether sent", async () => { 44 | await wallet.send(initialBalance).should.be.fulfilled; 45 | }); 46 | 47 | it("should emit received event", async () => { 48 | const {logs} = await wallet.sendTransaction({value: initialBalance, from: sender}); 49 | const event = logs.find(e => e.event === "Received"); 50 | const balance = await web3.eth.getBalance(wallet.address); 51 | 52 | logs.length.should.equal(1); 53 | should.exist(event); 54 | event.args.from.should.equal(sender); 55 | event.args.value.should.be.bignumber.equal(initialBalance); 56 | event.args.balance.should.be.bignumber.equal(balance); 57 | }); 58 | 59 | }); 60 | 61 | // When sent to Owner is tested in Fund.test.js 62 | describe("sendEther", () => { 63 | 64 | beforeEach(async () => { 65 | await wallet.sendTransaction({value: initialBalance, from: sender}); 66 | }); 67 | 68 | describe("when the sender is not the owner", () => { 69 | it("should revert", async () => { 70 | await wallet.sendEther(receiver, amountToSend, {from: sender}).should.be.rejectedWith(EVMRevert); 71 | }); 72 | }); 73 | 74 | describe("when the sender is the owner", () => { 75 | 76 | it("should transfer the correct amount", async () => { 77 | const balanceReceiverPre = await web3.eth.getBalance(receiver); 78 | await wallet.sendEther(receiver, amountToSend, {from: owner}); 79 | const balanceWallet = await web3.eth.getBalance(wallet.address); 80 | const balanceReceiverPost = await web3.eth.getBalance(receiver); 81 | 82 | balanceWallet.should.be.bignumber.equal(initialBalance - amountToSend); 83 | balanceReceiverPost.minus(balanceReceiverPre) 84 | .should.be.bignumber.equal(amountToSend); 85 | }); 86 | 87 | it("should emit a SentEther event", async () => { 88 | const {logs} = await wallet.sendEther(receiver, amountToSend, {from: owner}); 89 | const event = logs.find(e => e.event === "EtherSent"); 90 | const balance = await web3.eth.getBalance(wallet.address); 91 | 92 | logs.length.should.equal(1); 93 | should.exist(event); 94 | event.args.to.should.equal(receiver); 95 | event.args.value.should.be.bignumber.equal(amountToSend); 96 | event.args.balance.should.be.bignumber.equal(balance); 97 | }); 98 | 99 | }); 100 | 101 | }); 102 | 103 | describe("sendTokens", () => { 104 | let token; 105 | const initialBalance = 100000; 106 | const tokensToSend = 100; 107 | 108 | beforeEach(async () => { 109 | token = await SimpleToken.new({from: sender}); 110 | await token.transfer(wallet.address, initialBalance, {from: sender}); 111 | }); 112 | 113 | describe("when the sender is not the owner", () => { 114 | it("should revert", async () => { 115 | await wallet.sendTokens(token.address, receiver, tokensToSend, {from: sender}) 116 | .should.be.rejectedWith(EVMRevert); 117 | }); 118 | }); 119 | 120 | describe("when the sender is the owner", () => { 121 | 122 | describe("when the sender does not have enough tokens", () => { 123 | it("should revert", async () => { 124 | await wallet.sendTokens(token.address, receiver, initialBalance + 1, {from: owner}) 125 | .should.be.rejectedWith(EVMRevert); 126 | }); 127 | }); 128 | 129 | it("should transfer the correct amount", async () => { 130 | await wallet.sendTokens(token.address, receiver, tokensToSend, {from: owner}); 131 | const balanceReceiver = await token.balanceOf(receiver); 132 | 133 | balanceReceiver.should.be.bignumber.equal(tokensToSend); 134 | }); 135 | 136 | it("should emit a SentTokens event", async () => { 137 | const {logs} = await wallet.sendTokens(token.address, receiver, tokensToSend, {from: owner}); 138 | const event = logs.find(e => e.event === "TokensSent"); 139 | 140 | logs.length.should.equal(1); 141 | should.exist(event); 142 | event.args.token.should.equal(token.address); 143 | event.args.to.should.equal(receiver); 144 | event.args.value.should.be.bignumber.equal(tokensToSend); 145 | }); 146 | 147 | }); 148 | 149 | }); 150 | 151 | }); -------------------------------------------------------------------------------- /test/open-zeppelin/helpers/EVMRevert.js: -------------------------------------------------------------------------------- 1 | export default 'revert'; 2 | -------------------------------------------------------------------------------- /test/open-zeppelin/helpers/ether.js: -------------------------------------------------------------------------------- 1 | export default function ether (n) { 2 | return new web3.BigNumber(web3.toWei(n, 'ether')); 3 | } 4 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // See 3 | // to customize your Truffle configuration! 4 | }; 5 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('babel-polyfill'); 3 | 4 | module.exports = { 5 | networks: { 6 | development: { 7 | host: "localhost", 8 | port: 8545, 9 | network_id: "*" // Match any network id 10 | }, 11 | coverage: { 12 | host: 'localhost', 13 | network_id: '*', 14 | port: 8555, 15 | gas: 0xfffffffffff, 16 | gasPrice: 0x01, 17 | }, 18 | } 19 | }; 20 | 21 | --------------------------------------------------------------------------------