├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .solcover.js ├── .soliumignore ├── .soliumrc.json ├── .travis.yml ├── README.md ├── contracts ├── AbstractDeployer.sol ├── BasicMultiToken.sol ├── FeeBasicMultiToken.sol ├── FeeFundMultiToken.sol ├── FeeMultiToken.sol ├── FundMultiToken.sol ├── Migrations.sol ├── MultiToken.sol ├── RemoteToken.sol ├── ext │ ├── CheckedERC20.sol │ ├── ERC1003Token.sol │ ├── EtherToken.sol │ └── ExternalCall.sol ├── implementation │ ├── AstraBasicMultiToken.sol │ ├── AstraMultiToken.sol │ ├── EOSToken.sol │ └── deployers │ │ ├── AstraBasicMultiTokenDeployer.sol │ │ └── AstraMultiTokenDeployer.sol ├── interface │ ├── IBasicMultiToken.sol │ ├── IFundMultiToken.sol │ ├── IMultiToken.sol │ └── IMultiTokenInfo.sol └── network │ ├── MultiBuyer.sol │ ├── MultiChanger.sol │ ├── MultiSeller.sol │ ├── MultiShopper.sol │ ├── MultiTokenInfo.sol │ └── MultiTokenNetwork.sol ├── docs ├── css │ ├── checkbox.css │ └── list.css ├── index.html └── js │ ├── lib │ └── web3.min.js │ ├── multitoken.js │ └── web3.min.js ├── index.html ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package-lock.json ├── package.json ├── scripts ├── arbiter.js ├── coverage.sh └── test.sh ├── test ├── BasicMultiToken.js ├── MultiToken.js ├── MultiTokenInfo.js ├── helpers │ ├── EVMRevert.js │ ├── EVMThrow.js │ ├── advanceToBlock.js │ ├── assertJump.js │ ├── assertRevert.js │ ├── ether.js │ ├── expectEvent.js │ ├── expectThrow.js │ ├── increaseTime.js │ ├── latestTime.js │ ├── makeInterfaceId.js │ ├── merkleTree.js │ ├── sendTransaction.js │ ├── sign.js │ ├── transactionMined.js │ └── web3.js └── impl │ ├── BadToken.sol │ ├── BrokenTransferFromToken.sol │ ├── BrokenTransferToken.sol │ └── Token.sol └── truffle.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | truffle.js 3 | js/ 4 | coverage/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : [ 3 | "standard", 4 | "plugin:promise/recommended" 5 | ], 6 | "plugins": [ 7 | "promise" 8 | ], 9 | "env": { 10 | "browser" : true, 11 | "node" : true, 12 | "mocha" : true, 13 | "jest" : true 14 | }, 15 | "globals" : { 16 | "artifacts": false, 17 | "contract": false, 18 | "assert": false, 19 | "web3": false 20 | }, 21 | "rules": { 22 | 23 | // Strict mode 24 | "strict": [2, "global"], 25 | 26 | // Code style 27 | "indent": [2, 4], 28 | "quotes": [2, "single"], 29 | "semi": ["error", "always"], 30 | "space-before-function-paren": ["error", "always"], 31 | "no-use-before-define": 0, 32 | "no-unused-expressions": "off", 33 | "eqeqeq": [2, "smart"], 34 | "dot-notation": [2, {"allowKeywords": true, "allowPattern": ""}], 35 | "no-redeclare": [2, {"builtinGlobals": true}], 36 | "no-trailing-spaces": [2, { "skipBlankLines": true }], 37 | "eol-last": 1, 38 | "comma-spacing": [2, {"before": false, "after": true}], 39 | "camelcase": [2, {"properties": "always"}], 40 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 41 | "comma-dangle": [1, "always-multiline"], 42 | "no-dupe-args": 2, 43 | "no-dupe-keys": 2, 44 | "no-debugger": 0, 45 | "no-undef": 2, 46 | "object-curly-spacing": [2, "always"], 47 | "max-len": [2, 200, 2], 48 | "generator-star-spacing": ["error", "before"], 49 | "promise/avoid-new": 0, 50 | "promise/always-return": 0 51 | } 52 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | .DS_Store 4 | .node-xmlhttprequest-sync-* 5 | .idea/* 6 | 7 | coverage.json 8 | coverage/ 9 | coverageEnv/ 10 | allFiredEvents 11 | scTopics -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | copyPackages: ['openzeppelin-solidity'], 3 | skipFiles: ['ext/', 'network/', 'implementation/', 'interface/'], 4 | norpc: true 5 | } 6 | -------------------------------------------------------------------------------- /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:all", 3 | "plugins": ["security"], 4 | "rules": { 5 | "error-reason": "off", 6 | "indentation": ["error", 4], 7 | "lbrace": "off", 8 | "linebreak-style": ["error", "unix"], 9 | "max-len": ["error", 200], 10 | "arg-overflow": "off", 11 | "no-constant": ["error"], 12 | "no-empty-blocks": "off", 13 | "quotes": ["error", "double"], 14 | "uppercase": "off", 15 | "visibility-first": "error", 16 | "function-order" : "off", 17 | 18 | "security/enforce-explicit-visibility": ["error"], 19 | "security/no-block-members": ["warning"], 20 | "security/no-inline-assembly": ["warning"] 21 | } 22 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # https://github.com/sc-forks/solidity-coverage/blob/master/docs/faq.md 3 | # 4 | 5 | sudo: required 6 | dist: trusty 7 | language: node_js 8 | node_js: 9 | - '8' 10 | install: 11 | - npm install 12 | script: 13 | - npm run lint 14 | - npm run test 15 | after_script: 16 | - npm run coverage && cat coverage/lcov.info | coveralls 17 | branches: 18 | only: 19 | - master 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultiToken 2 | 3 | [![Build Status](https://travis-ci.org/multitoken/MultiToken.svg?branch=master)](https://travis-ci.org/multitoken/MultiToken) 4 | [![Coverage Status](https://coveralls.io/repos/github/multitoken/MultiToken/badge.svg)](https://coveralls.io/github/multitoken/MultiToken) 5 | 6 | ERC20 token solidity smart contract allowing aggreagate any number of ERC20 tokens in any proportion 7 | 8 | # Installation 9 | 10 | 1. Install local packages with `npm install` 11 | 2. Run tests with `npm test` 12 | 13 | On macOS you also need to install watchman: `brew install watchman` 14 | -------------------------------------------------------------------------------- /contracts/AbstractDeployer.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 4 | 5 | 6 | contract AbstractDeployer is Ownable { 7 | function title() public view returns(string); 8 | 9 | function createMultiToken() internal returns(address); 10 | 11 | function deploy(bytes data) 12 | external onlyOwner returns(address result) 13 | { 14 | address mtkn = createMultiToken(); 15 | // solium-disable-next-line security/no-low-level-calls 16 | require(mtkn.call(data), "Bad arbitrary call"); 17 | Ownable(mtkn).transferOwnership(msg.sender); 18 | return mtkn; 19 | } 20 | } -------------------------------------------------------------------------------- /contracts/BasicMultiToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; 5 | import "openzeppelin-solidity/contracts/token/ERC20/DetailedERC20.sol"; 6 | import "openzeppelin-solidity/contracts/introspection/SupportsInterfaceWithLookup.sol"; 7 | import "./ext/CheckedERC20.sol"; 8 | import "./ext/ERC1003Token.sol"; 9 | import "./interface/IBasicMultiToken.sol"; 10 | 11 | 12 | contract BasicMultiToken is Ownable, StandardToken, DetailedERC20, ERC1003Token, IBasicMultiToken, SupportsInterfaceWithLookup { 13 | using CheckedERC20 for ERC20; 14 | using CheckedERC20 for DetailedERC20; 15 | 16 | ERC20[] private _tokens; 17 | uint private _inLendingMode; 18 | bool private _bundlingEnabled = true; 19 | 20 | event Bundle(address indexed who, address indexed beneficiary, uint256 value); 21 | event Unbundle(address indexed who, address indexed beneficiary, uint256 value); 22 | event BundlingStatus(bool enabled); 23 | 24 | modifier notInLendingMode { 25 | require(_inLendingMode == 0, "Operation can't be performed while lending"); 26 | _; 27 | } 28 | 29 | modifier whenBundlingEnabled { 30 | require(_bundlingEnabled, "Bundling is disabled"); 31 | _; 32 | } 33 | 34 | constructor() 35 | public DetailedERC20("", "", 0) 36 | { 37 | } 38 | 39 | function init(ERC20[] tokens, string theName, string theSymbol, uint8 theDecimals) public { 40 | require(decimals == 0, "constructor: decimals should be zero"); 41 | require(theDecimals > 0, "constructor: _decimals should not be zero"); 42 | require(bytes(theName).length > 0, "constructor: name should not be empty"); 43 | require(bytes(theSymbol).length > 0, "constructor: symbol should not be empty"); 44 | require(tokens.length >= 2, "Contract does not support less than 2 inner tokens"); 45 | 46 | name = theName; 47 | symbol = theSymbol; 48 | decimals = theDecimals; 49 | _tokens = tokens; 50 | 51 | _registerInterface(InterfaceId_IBasicMultiToken); 52 | } 53 | 54 | function tokensCount() public view returns(uint) { 55 | return _tokens.length; 56 | } 57 | 58 | function tokens(uint i) public view returns(ERC20) { 59 | return _tokens[i]; 60 | } 61 | 62 | function inLendingMode() public view returns(uint) { 63 | return _inLendingMode; 64 | } 65 | 66 | function bundlingEnabled() public view returns(bool) { 67 | return _bundlingEnabled; 68 | } 69 | 70 | function bundleFirstTokens(address beneficiary, uint256 amount, uint256[] tokenAmounts) public whenBundlingEnabled notInLendingMode { 71 | require(totalSupply_ == 0, "bundleFirstTokens: This method can be used with zero total supply only"); 72 | _bundle(beneficiary, amount, tokenAmounts); 73 | } 74 | 75 | function bundle(address beneficiary, uint256 amount) public whenBundlingEnabled notInLendingMode { 76 | require(totalSupply_ != 0, "This method can be used with non zero total supply only"); 77 | uint256[] memory tokenAmounts = new uint256[](_tokens.length); 78 | for (uint i = 0; i < _tokens.length; i++) { 79 | tokenAmounts[i] = _tokens[i].balanceOf(this).mul(amount).div(totalSupply_); 80 | } 81 | _bundle(beneficiary, amount, tokenAmounts); 82 | } 83 | 84 | function unbundle(address beneficiary, uint256 value) public notInLendingMode { 85 | unbundleSome(beneficiary, value, _tokens); 86 | } 87 | 88 | function unbundleSome(address beneficiary, uint256 value, ERC20[] someTokens) public notInLendingMode { 89 | _unbundle(beneficiary, value, someTokens); 90 | } 91 | 92 | // Admin methods 93 | 94 | function disableBundling() public onlyOwner { 95 | require(_bundlingEnabled, "Bundling is already disabled"); 96 | _bundlingEnabled = false; 97 | emit BundlingStatus(false); 98 | } 99 | 100 | function enableBundling() public onlyOwner { 101 | require(!_bundlingEnabled, "Bundling is already enabled"); 102 | _bundlingEnabled = true; 103 | emit BundlingStatus(true); 104 | } 105 | 106 | // Internal methods 107 | 108 | function _bundle(address beneficiary, uint256 amount, uint256[] tokenAmounts) internal { 109 | require(amount != 0, "Bundling amount should be non-zero"); 110 | require(_tokens.length == tokenAmounts.length, "Lenghts of _tokens and tokenAmounts array should be equal"); 111 | 112 | for (uint i = 0; i < _tokens.length; i++) { 113 | require(tokenAmounts[i] != 0, "Token amount should be non-zero"); 114 | _tokens[i].checkedTransferFrom(msg.sender, this, tokenAmounts[i]); 115 | } 116 | 117 | totalSupply_ = totalSupply_.add(amount); 118 | balances[beneficiary] = balances[beneficiary].add(amount); 119 | emit Bundle(msg.sender, beneficiary, amount); 120 | emit Transfer(0, beneficiary, amount); 121 | } 122 | 123 | function _unbundle(address beneficiary, uint256 value, ERC20[] someTokens) internal { 124 | require(someTokens.length > 0, "Array of someTokens can't be empty"); 125 | 126 | uint256 totalSupply = totalSupply_; 127 | balances[msg.sender] = balances[msg.sender].sub(value); 128 | totalSupply_ = totalSupply.sub(value); 129 | emit Unbundle(msg.sender, beneficiary, value); 130 | emit Transfer(msg.sender, 0, value); 131 | 132 | for (uint i = 0; i < someTokens.length; i++) { 133 | for (uint j = 0; j < i; j++) { 134 | require(someTokens[i] != someTokens[j], "unbundleSome: should not unbundle same token multiple times"); 135 | } 136 | uint256 tokenAmount = someTokens[i].balanceOf(this).mul(value).div(totalSupply); 137 | someTokens[i].checkedTransfer(beneficiary, tokenAmount); 138 | } 139 | } 140 | 141 | // Instant Loans 142 | 143 | function lend(address to, ERC20 token, uint256 amount, address target, bytes data) public payable { 144 | uint256 prevBalance = token.balanceOf(this); 145 | token.asmTransfer(to, amount); 146 | _inLendingMode += 1; 147 | require(caller().makeCall.value(msg.value)(target, data), "lend: arbitrary call failed"); 148 | _inLendingMode -= 1; 149 | require(token.balanceOf(this) >= prevBalance, "lend: lended token must be refilled"); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /contracts/FeeBasicMultiToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 5 | import "./ext/CheckedERC20.sol"; 6 | import "./BasicMultiToken.sol"; 7 | 8 | 9 | contract FeeBasicMultiToken is Ownable, BasicMultiToken { 10 | using CheckedERC20 for ERC20; 11 | 12 | uint256 constant public TOTAL_PERCENTS = 1000000; 13 | uint256 internal _lendFee; 14 | 15 | function lendFee() public view returns(uint256) { 16 | return _lendFee; 17 | } 18 | 19 | function setLendFee(uint256 theLendFee) public onlyOwner { 20 | require(theLendFee <= 30000, "setLendFee: fee should be not greater than 3%"); 21 | _lendFee = theLendFee; 22 | } 23 | 24 | function lend(address to, ERC20 token, uint256 amount, address target, bytes data) public payable { 25 | uint256 expectedBalance = token.balanceOf(this).mul(TOTAL_PERCENTS.add(_lendFee)).div(TOTAL_PERCENTS); 26 | super.lend(to, token, amount, target, data); 27 | require(token.balanceOf(this) >= expectedBalance, "lend: tokens must be returned with lend fee"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/FeeFundMultiToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "./FundMultiToken.sol"; 4 | import "./FeeMultiToken.sol"; 5 | 6 | 7 | contract FeeFundMultiToken is FundMultiToken, FeeMultiToken { 8 | } 9 | -------------------------------------------------------------------------------- /contracts/FeeMultiToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 5 | import "./ext/CheckedERC20.sol"; 6 | import "./FeeBasicMultiToken.sol"; 7 | import "./MultiToken.sol"; 8 | 9 | 10 | contract FeeMultiToken is MultiToken, FeeBasicMultiToken { 11 | using CheckedERC20 for ERC20; 12 | 13 | uint256 internal _changeFee; 14 | uint256 internal _referralFee; 15 | 16 | function changeFee() public view returns(uint256) { 17 | return _changeFee; 18 | } 19 | 20 | function referralFee() public view returns(uint256) { 21 | return _referralFee; 22 | } 23 | 24 | function setChangeFee(uint256 theChangeFee) public onlyOwner { 25 | require(theChangeFee <= 30000, "setChangeFee: fee should be not greater than 3%"); 26 | _changeFee = theChangeFee; 27 | } 28 | 29 | function setReferralFee(uint256 theReferralFee) public onlyOwner { 30 | require(theReferralFee <= 500000, "setReferralFee: fee should be not greater than 50% of changeFee"); 31 | _referralFee = theReferralFee; 32 | } 33 | 34 | function getReturn(address fromToken, address toToken, uint256 amount) public view returns(uint256 returnAmount) { 35 | returnAmount = super.getReturn(fromToken, toToken, amount).mul(TOTAL_PERCENTS.sub(_changeFee)).div(TOTAL_PERCENTS); 36 | } 37 | 38 | function change(address fromToken, address toToken, uint256 amount, uint256 minReturn) public returns(uint256 returnAmount) { 39 | returnAmount = changeWithRef(fromToken, toToken, amount, minReturn, 0); 40 | } 41 | 42 | function changeWithRef(address fromToken, address toToken, uint256 amount, uint256 minReturn, address ref) public returns(uint256 returnAmount) { 43 | returnAmount = super.change(fromToken, toToken, amount, minReturn); 44 | uint256 refferalAmount = returnAmount 45 | .mul(_changeFee).div(TOTAL_PERCENTS.sub(_changeFee)) 46 | .mul(_referralFee).div(TOTAL_PERCENTS); 47 | 48 | ERC20(toToken).checkedTransfer(ref, refferalAmount); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /contracts/FundMultiToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 4 | import "./interface/IFundMultiToken.sol"; 5 | import "./MultiToken.sol"; 6 | 7 | 8 | contract OwnableMultiTokenMixin is Ownable, MultiToken { 9 | // 10 | } 11 | 12 | 13 | contract ManageableOrOwnableMultiTokenMixin is OwnableMultiTokenMixin { 14 | // solium-disable-next-line security/no-tx-origin 15 | address private _manager = tx.origin; 16 | 17 | modifier onlyManager { 18 | require(msg.sender == _manager, "Access denied"); 19 | _; 20 | } 21 | 22 | modifier onlyOwnerOrManager { 23 | require(msg.sender == owner || msg.sender == _manager, "Access denied"); 24 | _; 25 | } 26 | 27 | function manager() public view returns(address) { 28 | return _manager; 29 | } 30 | 31 | function transferManager(address newManager) public onlyManager { 32 | require(newManager != address(0), "newManager can't be zero address"); 33 | _manager = newManager; 34 | } 35 | } 36 | 37 | 38 | contract LockableMultiTokenMixin is ManageableOrOwnableMultiTokenMixin { 39 | mapping(address => bool) private _tokenIsLocked; 40 | 41 | function lockToken(address token) public onlyOwnerOrManager { 42 | _tokenIsLocked[token] = true; 43 | } 44 | 45 | function tokenIsLocked(address token) public view returns(bool) { 46 | return _tokenIsLocked[token]; 47 | } 48 | 49 | function getReturn(address fromToken, address toToken, uint256 amount) public view returns(uint256 returnAmount) { 50 | if (!_tokenIsLocked[fromToken] && !_tokenIsLocked[toToken]) { 51 | returnAmount = super.getReturn(fromToken, toToken, amount); 52 | } 53 | } 54 | 55 | function change(address fromToken, address toToken, uint256 amount, uint256 minReturn) public returns(uint256 returnAmount) { 56 | require(!_tokenIsLocked[fromToken], "The _fromToken is locked for exchange by multitoken owner"); 57 | require(!_tokenIsLocked[toToken], "The _toToken is locked for exchange by multitoken owner"); 58 | returnAmount = super.change(fromToken, toToken, amount, minReturn); 59 | } 60 | } 61 | 62 | 63 | contract FundMultiToken is IFundMultiToken, LockableMultiTokenMixin { 64 | mapping(address => uint256) private _nextWeights; 65 | uint256 private _nextMinimalWeight; 66 | uint256 private _nextWeightStartBlock; 67 | uint256 private _nextWeightBlockDelay = 100; 68 | uint256 private _nextWeightBlockDelayUpdate; 69 | 70 | event WeightsChanged(uint256 startingBlockNumber, uint256 endingBlockNumber, uint256 nextWeightBlockDelay); 71 | 72 | function init(ERC20[] tokens, uint256[] tokenWeights, string theName, string theSymbol, uint8 theDecimals) public { 73 | super.init(tokens, tokenWeights, theName, theSymbol, theDecimals); 74 | _registerInterface(InterfaceId_IFundMultiToken); 75 | } 76 | 77 | function nextWeights(address token) public view returns(uint256) { 78 | return _nextWeights[token]; 79 | } 80 | 81 | function nextWeightStartBlock() public view returns(uint256) { 82 | return _nextWeightStartBlock; 83 | } 84 | 85 | function nextWeightBlockDelay() public view returns(uint256) { 86 | return _nextWeightBlockDelay; 87 | } 88 | 89 | function weights(address token) public view returns(uint256) { 90 | if (_nextWeightStartBlock == 0) { 91 | return weights(token); 92 | } 93 | 94 | uint256 blockProgress = block.number - _nextWeightStartBlock; 95 | if (blockProgress < _nextWeightBlockDelay) { 96 | linearInterpolation(weights(token), _nextWeights[token], blockProgress, _nextWeightBlockDelay); 97 | } 98 | return _nextWeights[token]; 99 | } 100 | 101 | function setNextWeightBlockDelay(uint256 theNextWeightBlockDelay) public onlyOwner { 102 | if (block.number > _nextWeightStartBlock.add(_nextWeightBlockDelay)) { 103 | _nextWeightBlockDelay = theNextWeightBlockDelay; 104 | } else { 105 | _nextWeightBlockDelayUpdate = theNextWeightBlockDelay; 106 | } 107 | } 108 | 109 | function changeWeights(uint256[] theNextWeights) public onlyManager { 110 | require(theNextWeights.length == tokensCount(), "theNextWeights array length should match tokens length"); 111 | require(block.number.sub(_nextWeightStartBlock) > _nextWeightBlockDelay, "Previous weights changed is not completed yet"); 112 | 113 | // Migrate previous weights 114 | if (_nextWeightStartBlock != 0) { 115 | for (uint i = 0; i < tokensCount(); i++) { 116 | setWeight(tokens(i), _nextWeights[tokens(i)]); 117 | } 118 | _minimalWeight = _nextMinimalWeight; 119 | if (_nextWeightBlockDelayUpdate > 0) { 120 | _nextWeightBlockDelay = _nextWeightBlockDelayUpdate; 121 | _nextWeightBlockDelayUpdate = 0; 122 | } 123 | } 124 | 125 | uint256 nextMinimalWeight = 0; 126 | _nextWeightStartBlock = block.number; 127 | for (i = 0; i < tokensCount(); i++) { 128 | require(theNextWeights[i] != 0, "The theNextWeights array should not contains zeros"); 129 | _nextWeights[tokens(i)] = theNextWeights[i]; 130 | if (nextMinimalWeight == 0 || theNextWeights[i] < nextMinimalWeight) { 131 | nextMinimalWeight = theNextWeights[i]; 132 | } 133 | } 134 | _nextMinimalWeight = nextMinimalWeight; 135 | } 136 | 137 | function getReturn(address fromToken, address toToken, uint256 amount) public view returns(uint256) { 138 | if (fromToken == toToken) { 139 | return 0; 140 | } 141 | 142 | uint256 blockProgress = block.number - _nextWeightStartBlock; 143 | uint256 scaledFromWeight = _minimalWeight; 144 | uint256 scaledToWeight = weights(fromToken); 145 | uint256 scaledMinWeight = weights(toToken); 146 | if (blockProgress < _nextWeightBlockDelay) { 147 | scaledFromWeight = linearInterpolation(weights(fromToken), _nextWeights[fromToken], blockProgress, _nextWeightBlockDelay); 148 | scaledToWeight = linearInterpolation(weights(toToken), _nextWeights[toToken], blockProgress, _nextWeightBlockDelay); 149 | scaledMinWeight = linearInterpolation(_minimalWeight, _nextMinimalWeight, blockProgress, _nextWeightBlockDelay); 150 | } 151 | 152 | // uint256 fromBalance = ERC20(fromToken).balanceOf(this); 153 | // uint256 toBalance = ERC20(toToken).balanceOf(this); 154 | return amount.mul(ERC20(toToken).balanceOf(this)).mul(scaledFromWeight).div( 155 | amount.mul(scaledFromWeight).div(scaledMinWeight).add(ERC20(fromToken).balanceOf(this)).mul(scaledToWeight) 156 | ); 157 | } 158 | 159 | function change(address fromToken, address toToken, uint256 amount, uint256 minReturn) public whenChangesEnabled notInLendingMode returns(uint256) { 160 | if (block.number > _nextWeightStartBlock.add(_nextWeightBlockDelay)) { 161 | _nextWeightStartBlock = 0; 162 | } 163 | return super.change(fromToken, toToken, amount, minReturn); 164 | } 165 | 166 | function _bundle(address beneficiary, uint256 amount, uint256[] tokenAmounts) internal { 167 | if (totalSupply_ > 0) { 168 | _nextWeightBlockDelay = _nextWeightBlockDelay.mul(totalSupply_.add(amount)).div(totalSupply_); 169 | } else { 170 | _nextWeightBlockDelay = 100; 171 | } 172 | return super._bundle(beneficiary, amount, tokenAmounts); 173 | } 174 | 175 | function _unbundle(address beneficiary, uint256 value, ERC20[] someTokens) internal { 176 | if (totalSupply_ > value) { 177 | _nextWeightBlockDelay = _nextWeightBlockDelay.mul(totalSupply_.sub(value)).div(totalSupply_); 178 | } else { 179 | _nextWeightBlockDelay = 100; 180 | } 181 | return super._unbundle(beneficiary, value, someTokens); 182 | } 183 | 184 | function linearInterpolation(uint256 a, uint256 b, uint256 _mul, uint256 _notDiv) internal pure returns(uint256) { 185 | if (a < b) { 186 | return a.mul(_notDiv).add(b.sub(a).mul(_mul)); 187 | } 188 | return b.mul(_notDiv).add(a.sub(b).mul(_mul)); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | 4 | contract Migrations { 5 | 6 | address public owner; 7 | uint public lastCompletedMigration; 8 | 9 | modifier restricted() { 10 | if (msg.sender == owner) _; 11 | } 12 | 13 | constructor() public { 14 | owner = msg.sender; 15 | } 16 | 17 | function setCompleted(uint completed) public restricted { 18 | lastCompletedMigration = completed; 19 | } 20 | 21 | function upgrade(address newAddress) public restricted { 22 | Migrations upgraded = Migrations(newAddress); 23 | upgraded.setCompleted(lastCompletedMigration); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /contracts/MultiToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "./ext/CheckedERC20.sol"; 4 | import "./interface/IMultiToken.sol"; 5 | import "./BasicMultiToken.sol"; 6 | 7 | 8 | contract MultiToken is IMultiToken, BasicMultiToken { 9 | using CheckedERC20 for ERC20; 10 | 11 | mapping(address => uint256) private _weights; 12 | uint256 internal _minimalWeight; 13 | bool private _changesEnabled = true; 14 | 15 | event ChangesDisabled(); 16 | 17 | modifier whenChangesEnabled { 18 | require(_changesEnabled, "Operation can't be performed because changes are disabled"); 19 | _; 20 | } 21 | 22 | function weights(address _token) public view returns(uint256) { 23 | return _weights[_token]; 24 | } 25 | 26 | function changesEnabled() public view returns(bool) { 27 | return _changesEnabled; 28 | } 29 | 30 | function init(ERC20[] tokens, uint256[] tokenWeights, string theName, string theSymbol, uint8 theDecimals) public { 31 | super.init(tokens, theName, theSymbol, theDecimals); 32 | require(tokenWeights.length == tokens.length, "Lenghts of tokens and tokenWeights array should be equal"); 33 | 34 | uint256 minimalWeight = 0; 35 | for (uint i = 0; i < tokens.length; i++) { 36 | require(tokenWeights[i] != 0, "The tokenWeights array should not contains zeros"); 37 | require(_weights[tokens[i]] == 0, "The tokens array have duplicates"); 38 | _weights[tokens[i]] = tokenWeights[i]; 39 | if (minimalWeight == 0 || tokenWeights[i] < minimalWeight) { 40 | minimalWeight = tokenWeights[i]; 41 | } 42 | } 43 | _minimalWeight = minimalWeight; 44 | 45 | _registerInterface(InterfaceId_IMultiToken); 46 | } 47 | 48 | function getReturn(address fromToken, address toToken, uint256 amount) public view returns(uint256 returnAmount) { 49 | if (_weights[fromToken] > 0 && _weights[toToken] > 0 && fromToken != toToken) { 50 | uint256 fromBalance = ERC20(fromToken).balanceOf(this); 51 | uint256 toBalance = ERC20(toToken).balanceOf(this); 52 | returnAmount = amount.mul(toBalance).mul(_weights[fromToken]).div( 53 | amount.mul(_weights[fromToken]).div(_minimalWeight).add(fromBalance).mul(_weights[toToken]) 54 | ); 55 | } 56 | } 57 | 58 | function change(address fromToken, address toToken, uint256 amount, uint256 minReturn) public whenChangesEnabled notInLendingMode returns(uint256 returnAmount) { 59 | returnAmount = getReturn(fromToken, toToken, amount); 60 | require(returnAmount > 0, "The return amount is zero"); 61 | require(returnAmount >= minReturn, "The return amount is less than minReturn value"); 62 | 63 | ERC20(fromToken).checkedTransferFrom(msg.sender, this, amount); 64 | ERC20(toToken).checkedTransfer(msg.sender, returnAmount); 65 | 66 | emit Change(fromToken, toToken, msg.sender, amount, returnAmount); 67 | } 68 | 69 | // Admin methods 70 | 71 | function disableChanges() public onlyOwner { 72 | require(_changesEnabled, "Changes are already disabled"); 73 | _changesEnabled = false; 74 | emit ChangesDisabled(); 75 | } 76 | 77 | // Internal methods 78 | 79 | function setWeight(address token, uint256 newWeight) internal { 80 | _weights[token] = newWeight; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /contracts/RemoteToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/BurnableToken.sol"; 5 | import "openzeppelin-solidity/contracts/token/ERC20/DetailedERC20.sol"; 6 | import "openzeppelin-solidity/contracts/ECRecovery.sol"; 7 | 8 | 9 | contract RemoteToken is MintableToken, BurnableToken { 10 | mapping(bytes32 => bool) private _spentSignature; 11 | 12 | modifier isUpToDate(uint256 blockNumber) { 13 | require(block.number <= blockNumber, "Signature is outdated"); 14 | _; 15 | } 16 | 17 | modifier spendSignature(bytes32 r) { 18 | require(!_spentSignature[r], "Signature was used"); 19 | _spentSignature[r] = true; 20 | _; 21 | } 22 | 23 | constructor() public { 24 | } 25 | 26 | function depositEther() public payable onlyOwner { 27 | } 28 | 29 | function withdrawEther(uint256 value) public onlyOwner { 30 | msg.sender.transfer(value); 31 | } 32 | 33 | function mint(address /*to*/, uint256 amount) public onlyOwner returns(bool) { 34 | return super.mint(this, amount); 35 | } 36 | 37 | function burn(uint256 amount) public onlyOwner { 38 | _burn(this, amount); 39 | } 40 | 41 | function buy( 42 | uint256 priceMul, 43 | uint256 priceDiv, 44 | uint256 blockNumber, 45 | bytes32 r, 46 | bytes32 s, 47 | uint8 v 48 | ) 49 | public 50 | payable 51 | spendSignature(r) 52 | isUpToDate(blockNumber) 53 | returns(uint256 amount) 54 | { 55 | bytes memory data = abi.encodePacked(this.buy.selector, msg.value, priceMul, priceDiv, blockNumber); 56 | require(checkOwnerSignature(data, r, s, v), "Signature is invalid"); 57 | amount = msg.value.mul(priceMul).div(priceDiv); 58 | require(this.transfer(msg.sender, amount), "There are no enough tokens available for buying"); 59 | } 60 | 61 | function sell( 62 | uint256 amount, 63 | uint256 priceMul, 64 | uint256 priceDiv, 65 | uint256 blockNumber, 66 | bytes32 r, 67 | bytes32 s, 68 | uint8 v 69 | ) 70 | public 71 | spendSignature(r) 72 | isUpToDate(blockNumber) 73 | returns(uint256 value) 74 | { 75 | bytes memory data = abi.encodePacked(this.sell.selector, amount, priceMul, priceDiv, blockNumber); 76 | require(checkOwnerSignature(data, r, s, v), "Signature is invalid"); 77 | require(this.transferFrom(msg.sender, this, amount), "There are not enough tokens available for selling"); 78 | value = amount.mul(priceMul).div(priceDiv); 79 | msg.sender.transfer(value); 80 | } 81 | 82 | function checkOwnerSignature( 83 | bytes data, 84 | bytes32 r, 85 | bytes32 s, 86 | uint8 v 87 | ) public view returns(bool) { 88 | require(v == 0 || v == 1 || v == 27 || v == 28, "Signature version is invalid"); 89 | bytes32 messageHash = keccak256(data); 90 | bytes32 signedHash = ECRecovery.toEthSignedMessageHash(messageHash); 91 | return owner == ecrecover(signedHash, v < 27 ? v + 27 : v, r, s); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contracts/ext/CheckedERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 5 | 6 | 7 | library CheckedERC20 { 8 | using SafeMath for uint; 9 | 10 | function isContract(address addr) internal view returns(bool result) { 11 | // solium-disable-next-line security/no-inline-assembly 12 | assembly { 13 | result := gt(extcodesize(addr), 0) 14 | } 15 | } 16 | 17 | function handleReturnBool() internal pure returns(bool result) { 18 | // solium-disable-next-line security/no-inline-assembly 19 | assembly { 20 | switch returndatasize() 21 | case 0 { // not a std erc20 22 | result := 1 23 | } 24 | case 32 { // std erc20 25 | returndatacopy(0, 0, 32) 26 | result := mload(0) 27 | } 28 | default { // anything else, should revert for safety 29 | revert(0, 0) 30 | } 31 | } 32 | } 33 | 34 | function handleReturnBytes32() internal pure returns(bytes32 result) { 35 | // solium-disable-next-line security/no-inline-assembly 36 | assembly { 37 | switch eq(returndatasize(), 32) // not a std erc20 38 | case 1 { 39 | returndatacopy(0, 0, 32) 40 | result := mload(0) 41 | } 42 | 43 | switch gt(returndatasize(), 32) // std erc20 44 | case 1 { 45 | returndatacopy(0, 64, 32) 46 | result := mload(0) 47 | } 48 | 49 | switch lt(returndatasize(), 32) // anything else, should revert for safety 50 | case 1 { 51 | revert(0, 0) 52 | } 53 | } 54 | } 55 | 56 | function asmTransfer(address token, address to, uint256 value) internal returns(bool) { 57 | require(isContract(token)); 58 | // solium-disable-next-line security/no-low-level-calls 59 | require(token.call(bytes4(keccak256("transfer(address,uint256)")), to, value)); 60 | return handleReturnBool(); 61 | } 62 | 63 | function asmTransferFrom(address token, address from, address to, uint256 value) internal returns(bool) { 64 | require(isContract(token)); 65 | // solium-disable-next-line security/no-low-level-calls 66 | require(token.call(bytes4(keccak256("transferFrom(address,address,uint256)")), from, to, value)); 67 | return handleReturnBool(); 68 | } 69 | 70 | function asmApprove(address token, address spender, uint256 value) internal returns(bool) { 71 | require(isContract(token)); 72 | // solium-disable-next-line security/no-low-level-calls 73 | require(token.call(bytes4(keccak256("approve(address,uint256)")), spender, value)); 74 | return handleReturnBool(); 75 | } 76 | 77 | // 78 | 79 | function checkedTransfer(ERC20 token, address to, uint256 value) internal { 80 | if (value > 0) { 81 | uint256 balance = token.balanceOf(this); 82 | asmTransfer(token, to, value); 83 | require(token.balanceOf(this) == balance.sub(value), "checkedTransfer: Final balance didn't match"); 84 | } 85 | } 86 | 87 | function checkedTransferFrom(ERC20 token, address from, address to, uint256 value) internal { 88 | if (value > 0) { 89 | uint256 toBalance = token.balanceOf(to); 90 | asmTransferFrom(token, from, to, value); 91 | require(token.balanceOf(to) == toBalance.add(value), "checkedTransfer: Final balance didn't match"); 92 | } 93 | } 94 | 95 | // 96 | 97 | function asmName(address token) internal view returns(bytes32) { 98 | require(isContract(token)); 99 | // solium-disable-next-line security/no-low-level-calls 100 | require(token.call(bytes4(keccak256("name()")))); 101 | return handleReturnBytes32(); 102 | } 103 | 104 | function asmSymbol(address token) internal view returns(bytes32) { 105 | require(isContract(token)); 106 | // solium-disable-next-line security/no-low-level-calls 107 | require(token.call(bytes4(keccak256("symbol()")))); 108 | return handleReturnBytes32(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /contracts/ext/ERC1003Token.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 5 | 6 | 7 | contract ERC1003Caller is Ownable { 8 | function makeCall(address target, bytes data) external payable onlyOwner returns (bool) { 9 | // solium-disable-next-line security/no-call-value 10 | return target.call.value(msg.value)(data); 11 | } 12 | } 13 | 14 | 15 | contract ERC1003Token is ERC20 { 16 | ERC1003Caller private _caller = new ERC1003Caller(); 17 | address[] internal _sendersStack; 18 | 19 | function caller() public view returns(ERC1003Caller) { 20 | return _caller; 21 | } 22 | 23 | function approveAndCall(address to, uint256 value, bytes data) public payable returns (bool) { 24 | _sendersStack.push(msg.sender); 25 | approve(to, value); 26 | require(_caller.makeCall.value(msg.value)(to, data)); 27 | _sendersStack.length -= 1; 28 | return true; 29 | } 30 | 31 | function transferAndCall(address to, uint256 value, bytes data) public payable returns (bool) { 32 | transfer(to, value); 33 | require(_caller.makeCall.value(msg.value)(to, data)); 34 | return true; 35 | } 36 | 37 | function transferFrom(address from, address to, uint256 value) public returns (bool) { 38 | address spender = (from != address(_caller)) ? from : _sendersStack[_sendersStack.length - 1]; 39 | return super.transferFrom(spender, to, value); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/ext/EtherToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/BurnableToken.sol"; 5 | 6 | 7 | contract EtherToken is MintableToken, BurnableToken { 8 | constructor() public { 9 | delete owner; 10 | } 11 | 12 | function() public payable { 13 | deposit(); 14 | } 15 | 16 | function deposit() public payable { 17 | depositTo(msg.sender); 18 | } 19 | 20 | function depositTo(address to) public payable { 21 | owner = to; 22 | mint(to, msg.value); 23 | delete owner; 24 | } 25 | 26 | function withdraw(uint amount) public { 27 | withdrawTo(msg.sender, amount); 28 | } 29 | 30 | function withdrawTo(address to, uint amount) public { 31 | burn(amount); 32 | to.transfer(amount); 33 | } 34 | 35 | function withdrawFrom(address from, uint amount) public { 36 | this.transferFrom(from, this, amount); 37 | this.burn(amount); 38 | from.transfer(amount); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/ext/ExternalCall.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | 4 | library ExternalCall { 5 | // Source: https://github.com/gnosis/MultiSigWallet/blob/master/contracts/MultiSigWallet.sol 6 | // call has been separated into its own function in order to take advantage 7 | // of the Solidity's code generator to produce a loop that copies tx.data into memory. 8 | function externalCall(address destination, uint value, bytes data, uint dataOffset, uint dataLength) internal returns(bool result) { 9 | // solium-disable-next-line security/no-inline-assembly 10 | assembly { 11 | let x := mload(0x40) // "Allocate" memory for output (0x40 is where "free memory" pointer is stored by convention) 12 | let d := add(data, 32) // First 32 bytes are the padded length of data, so exclude that 13 | result := call( 14 | sub(gas, 34710), // 34710 is the value that solidity is currently emitting 15 | // It includes callGas (700) + callVeryLow (3, to pay for SUB) + callValueTransferGas (9000) + 16 | // callNewAccountGas (25000, in case the destination address does not exist and needs creating) 17 | destination, 18 | value, 19 | add(d, dataOffset), 20 | dataLength, // Size of the input (in bytes) - this is what fixes the padding problem 21 | x, 22 | 0 // Output is ignored, therefore the output size is zero 23 | ) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/implementation/AstraBasicMultiToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "../FeeBasicMultiToken.sol"; 4 | 5 | 6 | contract AstraBasicMultiToken is FeeBasicMultiToken { 7 | function init(ERC20[] tokens, string theName, string theSymbol, uint8 /*theDecimals*/) public { 8 | super.init(tokens, theName, theSymbol, 18); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/implementation/AstraMultiToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "../FeeMultiToken.sol"; 4 | 5 | 6 | contract AstraMultiToken is FeeMultiToken { 7 | function init(ERC20[] tokens, uint256[] tokenWeights, string theName, string theSymbol, uint8 /*theDecimals*/) public { 8 | super.init(tokens, tokenWeights, theName, theSymbol, 18); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contracts/implementation/EOSToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "../RemoteToken.sol"; 4 | 5 | 6 | contract EOSToken is RemoteToken, DetailedERC20 { 7 | constructor() public DetailedERC20("EOSToken", "EOST", 18) { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/implementation/deployers/AstraBasicMultiTokenDeployer.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "../../AbstractDeployer.sol"; 4 | import "../AstraBasicMultiToken.sol"; 5 | 6 | 7 | contract AstraBasicMultiTokenDeployer is AbstractDeployer { 8 | function title() public view returns(string) { 9 | return "AstraBasicMultiTokenDeployer"; 10 | } 11 | 12 | function createMultiToken() internal returns(address) { 13 | return new AstraBasicMultiToken(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/implementation/deployers/AstraMultiTokenDeployer.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "../../AbstractDeployer.sol"; 4 | import "../AstraMultiToken.sol"; 5 | 6 | 7 | contract AstraMultiTokenDeployer is AbstractDeployer { 8 | function title() public view returns(string) { 9 | return "AstraMultiTokenDeployer"; 10 | } 11 | 12 | function createMultiToken() internal returns(address) { 13 | return new AstraMultiToken(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/interface/IBasicMultiToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 4 | 5 | 6 | contract IBasicMultiToken is ERC20 { 7 | event Bundle(address indexed who, address indexed beneficiary, uint256 value); 8 | event Unbundle(address indexed who, address indexed beneficiary, uint256 value); 9 | 10 | function tokensCount() public view returns(uint256); 11 | function tokens(uint i) public view returns(ERC20); 12 | function bundlingEnabled() public view returns(bool); 13 | 14 | function bundleFirstTokens(address _beneficiary, uint256 _amount, uint256[] _tokenAmounts) public; 15 | function bundle(address _beneficiary, uint256 _amount) public; 16 | 17 | function unbundle(address _beneficiary, uint256 _value) public; 18 | function unbundleSome(address _beneficiary, uint256 _value, ERC20[] _tokens) public; 19 | 20 | // Owner methods 21 | function disableBundling() public; 22 | function enableBundling() public; 23 | 24 | bytes4 public constant InterfaceId_IBasicMultiToken = 0xd5c368b6; 25 | /** 26 | * 0xd5c368b6 === 27 | * bytes4(keccak256('tokensCount()')) ^ 28 | * bytes4(keccak256('tokens(uint256)')) ^ 29 | * bytes4(keccak256('bundlingEnabled()')) ^ 30 | * bytes4(keccak256('bundleFirstTokens(address,uint256,uint256[])')) ^ 31 | * bytes4(keccak256('bundle(address,uint256)')) ^ 32 | * bytes4(keccak256('unbundle(address,uint256)')) ^ 33 | * bytes4(keccak256('unbundleSome(address,uint256,address[])')) ^ 34 | * bytes4(keccak256('disableBundling()')) ^ 35 | * bytes4(keccak256('enableBundling()')) 36 | */ 37 | } 38 | -------------------------------------------------------------------------------- /contracts/interface/IFundMultiToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "./IMultiToken.sol"; 4 | 5 | 6 | contract IFundMultiToken is IMultiToken { 7 | function tokenIsLocked(address token) public view returns(bool); 8 | function nextWeights(address token) public view returns(uint256); 9 | function nextWeightStartBlock() public view returns(uint256); 10 | function nextWeightBlockDelay() public view returns(uint256); 11 | 12 | // Manager methods 13 | function changeWeights(uint256[] theNextWeights) public; 14 | 15 | // Owner methods 16 | function lockToken(address token) public; 17 | function setNextWeightBlockDelay(uint256 theNextWeightBlockDelay) public; 18 | 19 | bytes4 public constant InterfaceId_IFundMultiToken = 0xc123b9ad; 20 | /** 21 | * 0xc123b9ad === 22 | * InterfaceId_IMultiToken(0x81624e24) ^ 23 | * bytes4(keccak256('tokenIsLocked(address)')) ^ 24 | * bytes4(keccak256('nextWeights(address)')) ^ 25 | * bytes4(keccak256('nextWeightStartBlock()')) ^ 26 | * bytes4(keccak256('nextWeightBlockDelay()')) ^ 27 | * bytes4(keccak256('changeWeights(uint256[])')) ^ 28 | * bytes4(keccak256('lockToken(address)')) ^ 29 | * bytes4(keccak256('setNextWeightBlockDelay(uint256)')) 30 | */ 31 | } 32 | -------------------------------------------------------------------------------- /contracts/interface/IMultiToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "./IBasicMultiToken.sol"; 4 | 5 | 6 | contract IMultiToken is IBasicMultiToken { 7 | event Update(); 8 | event Change(address indexed _fromToken, address indexed _toToken, address indexed _changer, uint256 _amount, uint256 _return); 9 | 10 | function weights(address _token) public view returns(uint256); 11 | function changesEnabled() public view returns(bool); 12 | 13 | function getReturn(address _fromToken, address _toToken, uint256 _amount) public view returns (uint256 returnAmount); 14 | function change(address _fromToken, address _toToken, uint256 _amount, uint256 _minReturn) public returns (uint256 returnAmount); 15 | 16 | // Owner methods 17 | function disableChanges() public; 18 | 19 | bytes4 public constant InterfaceId_IMultiToken = 0x81624e24; 20 | /** 21 | * 0x81624e24 === 22 | * InterfaceId_IBasicMultiToken(0xd5c368b6) ^ 23 | * bytes4(keccak256('weights(address)')) ^ 24 | * bytes4(keccak256('changesEnabled()')) ^ 25 | * bytes4(keccak256('getReturn(address,address,uint256)')) ^ 26 | * bytes4(keccak256('change(address,address,uint256,uint256)')) ^ 27 | * bytes4(keccak256('disableChanges()')) 28 | */ 29 | } 30 | -------------------------------------------------------------------------------- /contracts/interface/IMultiTokenInfo.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "./IBasicMultiToken.sol"; 4 | import "./IMultiToken.sol"; 5 | 6 | 7 | contract IMultiTokenInfo { 8 | function allTokens(IBasicMultiToken _mtkn) public view returns(ERC20[] _tokens); 9 | 10 | function allBalances(IBasicMultiToken _mtkn) public view returns(uint256[] _balances); 11 | 12 | function allDecimals(IBasicMultiToken _mtkn) public view returns(uint8[] _decimals); 13 | 14 | function allNames(IBasicMultiToken _mtkn) public view returns(bytes32[] _names); 15 | 16 | function allSymbols(IBasicMultiToken _mtkn) public view returns(bytes32[] _symbols); 17 | 18 | function allTokensBalancesDecimalsNamesSymbols(IBasicMultiToken _mtkn) public view returns( 19 | ERC20[] _tokens, 20 | uint256[] _balances, 21 | uint8[] _decimals, 22 | bytes32[] _names, 23 | bytes32[] _symbols 24 | ); 25 | 26 | // MultiToken 27 | 28 | function allWeights(IMultiToken _mtkn) public view returns(uint256[] _weights); 29 | 30 | function allTokensBalancesDecimalsNamesSymbolsWeights(IMultiToken _mtkn) public view returns( 31 | ERC20[] _tokens, 32 | uint256[] _balances, 33 | uint8[] _decimals, 34 | bytes32[] _names, 35 | bytes32[] _symbols, 36 | uint256[] _weights 37 | ); 38 | 39 | bytes4 public constant InterfaceId_IMultiTokenInfo = 0x6d429c45; 40 | /** 41 | * 0x6d429c45 === 42 | * bytes4(keccak256('allTokens(address)')) ^ 43 | * bytes4(keccak256('allBalances(address)')) ^ 44 | * bytes4(keccak256('allDecimals(address)')) ^ 45 | * bytes4(keccak256('allNames(address)')) ^ 46 | * bytes4(keccak256('allSymbols(address)')) ^ 47 | * bytes4(keccak256('allTokensBalancesDecimalsNamesSymbols(address)')) ^ 48 | * bytes4(keccak256('allWeights(address)')) ^ 49 | * bytes4(keccak256('allTokensBalancesDecimalsNamesSymbolsWeights(address)')) 50 | */ 51 | } -------------------------------------------------------------------------------- /contracts/network/MultiBuyer.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 4 | import "../interface/IMultiToken.sol"; 5 | import "../ext/CheckedERC20.sol"; 6 | import "./MultiShopper.sol"; 7 | 8 | 9 | contract MultiBuyer is MultiShopper { 10 | using CheckedERC20 for ERC20; 11 | 12 | function buy( 13 | IMultiToken mtkn, 14 | uint256 minimumReturn, 15 | bytes callDatas, 16 | uint[] starts // including 0 and LENGTH values 17 | ) 18 | public 19 | payable 20 | { 21 | change(callDatas, starts); 22 | 23 | uint mtknTotalSupply = mtkn.totalSupply(); // optimization totalSupply 24 | uint256 bestAmount = uint256(-1); 25 | for (uint i = mtkn.tokensCount(); i > 0; i--) { 26 | ERC20 token = mtkn.tokens(i - 1); 27 | if (token.allowance(this, mtkn) == 0) { 28 | token.asmApprove(mtkn, uint256(-1)); 29 | } 30 | 31 | uint256 amount = mtknTotalSupply.mul(token.balanceOf(this)).div(token.balanceOf(mtkn)); 32 | if (amount < bestAmount) { 33 | bestAmount = amount; 34 | } 35 | } 36 | 37 | require(bestAmount >= minimumReturn, "buy: return value is too low"); 38 | mtkn.bundle(msg.sender, bestAmount); 39 | if (address(this).balance > 0) { 40 | msg.sender.transfer(address(this).balance); 41 | } 42 | for (i = mtkn.tokensCount(); i > 0; i--) { 43 | token = mtkn.tokens(i - 1); 44 | if (token.balanceOf(this) > 0) { 45 | token.asmTransfer(msg.sender, token.balanceOf(this)); 46 | } 47 | } 48 | } 49 | 50 | function buyFirstTokens( 51 | IMultiToken mtkn, 52 | bytes callDatas, 53 | uint[] starts, // including 0 and LENGTH values 54 | uint ethPriceMul, 55 | uint ethPriceDiv 56 | ) 57 | public 58 | payable 59 | { 60 | change(callDatas, starts); 61 | 62 | uint tokensCount = mtkn.tokensCount(); 63 | uint256[] memory amounts = new uint256[](tokensCount); 64 | for (uint i = 0; i < tokensCount; i++) { 65 | ERC20 token = mtkn.tokens(i); 66 | amounts[i] = token.balanceOf(this); 67 | if (token.allowance(this, mtkn) == 0) { 68 | token.asmApprove(mtkn, uint256(-1)); 69 | } 70 | } 71 | 72 | mtkn.bundleFirstTokens(msg.sender, msg.value.mul(ethPriceMul).div(ethPriceDiv), amounts); 73 | if (address(this).balance > 0) { 74 | msg.sender.transfer(address(this).balance); 75 | } 76 | for (i = mtkn.tokensCount(); i > 0; i--) { 77 | token = mtkn.tokens(i - 1); 78 | if (token.balanceOf(this) > 0) { 79 | token.asmTransfer(msg.sender, token.balanceOf(this)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /contracts/network/MultiChanger.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 4 | import "../interface/IMultiToken.sol"; 5 | import "../ext/CheckedERC20.sol"; 6 | import "../ext/ExternalCall.sol"; 7 | 8 | 9 | contract IEtherToken is ERC20 { 10 | function deposit() public payable; 11 | function withdraw(uint256 amount) public; 12 | } 13 | 14 | 15 | contract MultiChanger { 16 | using SafeMath for uint256; 17 | using CheckedERC20 for ERC20; 18 | using ExternalCall for address; 19 | 20 | function() public payable { 21 | // solium-disable-next-line security/no-tx-origin 22 | require(tx.origin != msg.sender); 23 | } 24 | 25 | function change(bytes callDatas, uint[] starts) public payable { // starts should include 0 and callDatas.length 26 | for (uint i = 0; i < starts.length - 1; i++) { 27 | require(address(this).externalCall(0, callDatas, starts[i], starts[i + 1] - starts[i])); 28 | } 29 | } 30 | 31 | // Ether 32 | 33 | function sendEthValue(address target, uint256 value) external { 34 | // solium-disable-next-line security/no-call-value 35 | require(target.call.value(value)()); 36 | } 37 | 38 | function sendEthProportion(address target, uint256 mul, uint256 div) external { 39 | uint256 value = address(this).balance.mul(mul).div(div); 40 | // solium-disable-next-line security/no-call-value 41 | require(target.call.value(value)()); 42 | } 43 | 44 | // Ether token 45 | 46 | function depositEtherTokenAmount(IEtherToken etherToken, uint256 amount) external { 47 | etherToken.deposit.value(amount)(); 48 | } 49 | 50 | function depositEtherTokenProportion(IEtherToken etherToken, uint256 mul, uint256 div) external { 51 | uint256 amount = address(this).balance.mul(mul).div(div); 52 | etherToken.deposit.value(amount)(); 53 | } 54 | 55 | function withdrawEtherTokenAmount(IEtherToken etherToken, uint256 amount) external { 56 | etherToken.withdraw(amount); 57 | } 58 | 59 | function withdrawEtherTokenProportion(IEtherToken etherToken, uint256 mul, uint256 div) external { 60 | uint256 amount = etherToken.balanceOf(this).mul(mul).div(div); 61 | etherToken.withdraw(amount); 62 | } 63 | 64 | // Token 65 | 66 | function transferTokenAmount(address target, ERC20 fromToken, uint256 amount) external { 67 | require(fromToken.asmTransfer(target, amount)); 68 | } 69 | 70 | function transferTokenProportion(address target, ERC20 fromToken, uint256 mul, uint256 div) external { 71 | uint256 amount = fromToken.balanceOf(this).mul(mul).div(div); 72 | require(fromToken.asmTransfer(target, amount)); 73 | } 74 | 75 | function transferFromTokenAmount(ERC20 fromToken, uint256 amount) external { 76 | // solium-disable-next-line security/no-tx-origin 77 | require(fromToken.asmTransferFrom(tx.origin, this, amount)); 78 | } 79 | 80 | function transferFromTokenProportion(ERC20 fromToken, uint256 mul, uint256 div) external { 81 | uint256 amount = fromToken.balanceOf(this).mul(mul).div(div); 82 | // solium-disable-next-line security/no-tx-origin 83 | require(fromToken.asmTransferFrom(tx.origin, this, amount)); 84 | } 85 | 86 | // MultiToken 87 | 88 | function multitokenChangeAmount(IMultiToken mtkn, ERC20 fromToken, ERC20 toToken, uint256 minReturn, uint256 amount) external { 89 | if (fromToken.allowance(this, mtkn) == 0) { 90 | fromToken.asmApprove(mtkn, uint256(-1)); 91 | } 92 | mtkn.change(fromToken, toToken, amount, minReturn); 93 | } 94 | 95 | function multitokenChangeProportion(IMultiToken mtkn, ERC20 fromToken, ERC20 toToken, uint256 minReturn, uint256 mul, uint256 div) external { 96 | uint256 amount = fromToken.balanceOf(this).mul(mul).div(div); 97 | this.multitokenChangeAmount(mtkn, fromToken, toToken, minReturn, amount); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /contracts/network/MultiSeller.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 5 | import { IMultiToken } from "../interface/IMultiToken.sol"; 6 | import "../ext/CheckedERC20.sol"; 7 | import "./MultiShopper.sol"; 8 | 9 | 10 | contract MultiSeller is MultiShopper { 11 | using CheckedERC20 for ERC20; 12 | using CheckedERC20 for IMultiToken; 13 | 14 | function() public payable { 15 | // solium-disable-next-line security/no-tx-origin 16 | require(tx.origin != msg.sender); 17 | } 18 | 19 | function sellForOrigin( 20 | IMultiToken mtkn, 21 | uint256 amount, 22 | bytes callDatas, 23 | uint[] starts // including 0 and LENGTH values 24 | ) 25 | public 26 | { 27 | sell( 28 | mtkn, 29 | amount, 30 | callDatas, 31 | starts, 32 | tx.origin // solium-disable-line security/no-tx-origin 33 | ); 34 | } 35 | 36 | function sell( 37 | IMultiToken mtkn, 38 | uint256 amount, 39 | bytes callDatas, 40 | uint[] starts, // including 0 and LENGTH values 41 | address to 42 | ) 43 | public 44 | { 45 | mtkn.asmTransferFrom(msg.sender, this, amount); 46 | mtkn.unbundle(this, amount); 47 | change(callDatas, starts); 48 | to.transfer(address(this).balance); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /contracts/network/MultiShopper.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 5 | import "openzeppelin-solidity/contracts/ownership/CanReclaimToken.sol"; 6 | import "../interface/IMultiToken.sol"; 7 | import "../ext/CheckedERC20.sol"; 8 | import "../ext/ExternalCall.sol"; 9 | 10 | 11 | contract IEtherToken is ERC20 { 12 | function deposit() public payable; 13 | function withdraw(uint256 amount) public; 14 | } 15 | 16 | 17 | contract IBancorNetwork { 18 | function convert( 19 | address[] path, 20 | uint256 amount, 21 | uint256 minReturn 22 | ) 23 | public 24 | payable 25 | returns(uint256); 26 | 27 | function claimAndConvert( 28 | address[] path, 29 | uint256 amount, 30 | uint256 minReturn 31 | ) 32 | public 33 | payable 34 | returns(uint256); 35 | } 36 | 37 | 38 | contract IKyberNetworkProxy { 39 | function trade( 40 | address src, 41 | uint srcAmount, 42 | address dest, 43 | address destAddress, 44 | uint maxDestAmount, 45 | uint minConversionRate, 46 | address walletId 47 | ) 48 | public 49 | payable 50 | returns(uint); 51 | } 52 | 53 | 54 | contract MultiShopper is CanReclaimToken { 55 | using SafeMath for uint256; 56 | using CheckedERC20 for ERC20; 57 | using ExternalCall for address; 58 | 59 | function change(bytes callDatas, uint[] starts) public payable { // starts should include 0 and callDatas.length 60 | for (uint i = 0; i < starts.length - 1; i++) { 61 | require(address(this).externalCall(0, callDatas, starts[i], starts[i + 1] - starts[i])); 62 | } 63 | } 64 | 65 | function sendEthValue(address target, bytes data, uint256 value) external { 66 | // solium-disable-next-line security/no-call-value 67 | require(target.call.value(value)(data)); 68 | } 69 | 70 | function sendEthProportion(address target, bytes data, uint256 mul, uint256 div) external { 71 | uint256 value = address(this).balance.mul(mul).div(div); 72 | // solium-disable-next-line security/no-call-value 73 | require(target.call.value(value)(data)); 74 | } 75 | 76 | function approveTokenAmount(address target, bytes data, ERC20 fromToken, uint256 amount) external { 77 | if (fromToken.allowance(this, target) != 0) { 78 | fromToken.asmApprove(target, 0); 79 | } 80 | fromToken.asmApprove(target, amount); 81 | // solium-disable-next-line security/no-low-level-calls 82 | require(target.call(data)); 83 | } 84 | 85 | function approveTokenProportion(address target, bytes data, ERC20 fromToken, uint256 mul, uint256 div) external { 86 | uint256 amount = fromToken.balanceOf(this).mul(mul).div(div); 87 | if (fromToken.allowance(this, target) != 0) { 88 | fromToken.asmApprove(target, 0); 89 | } 90 | fromToken.asmApprove(target, amount); 91 | // solium-disable-next-line security/no-low-level-calls 92 | require(target.call(data)); 93 | } 94 | 95 | function transferTokenAmount(address target, bytes data, ERC20 fromToken, uint256 amount) external { 96 | require(fromToken.asmTransfer(target, amount)); 97 | if (data.length != 0) { 98 | // solium-disable-next-line security/no-low-level-calls 99 | require(target.call(data)); 100 | } 101 | } 102 | 103 | function transferTokenProportion(address target, bytes data, ERC20 fromToken, uint256 mul, uint256 div) external { 104 | uint256 amount = fromToken.balanceOf(this).mul(mul).div(div); 105 | require(fromToken.asmTransfer(target, amount)); 106 | if (data.length != 0) { 107 | // solium-disable-next-line security/no-low-level-calls 108 | require(target.call(data)); 109 | } 110 | } 111 | 112 | function transferTokenProportionToOrigin(ERC20 token, uint256 mul, uint256 div) external { 113 | uint256 amount = token.balanceOf(this).mul(mul).div(div); 114 | // solium-disable-next-line security/no-tx-origin 115 | require(token.asmTransfer(tx.origin, amount)); 116 | } 117 | 118 | // Multitoken 119 | 120 | function multitokenChangeAmount(IMultiToken mtkn, ERC20 fromToken, ERC20 toToken, uint256 minReturn, uint256 amount) external { 121 | if (fromToken.allowance(this, mtkn) == 0) { 122 | fromToken.asmApprove(mtkn, uint256(-1)); 123 | } 124 | mtkn.change(fromToken, toToken, amount, minReturn); 125 | } 126 | 127 | function multitokenChangeProportion(IMultiToken mtkn, ERC20 fromToken, ERC20 toToken, uint256 minReturn, uint256 mul, uint256 div) external { 128 | uint256 amount = fromToken.balanceOf(this).mul(mul).div(div); 129 | this.multitokenChangeAmount(mtkn, fromToken, toToken, minReturn, amount); 130 | } 131 | 132 | // Ether token 133 | 134 | function withdrawEtherTokenAmount(IEtherToken etherToken, uint256 amount) external { 135 | etherToken.withdraw(amount); 136 | } 137 | 138 | function withdrawEtherTokenProportion(IEtherToken etherToken, uint256 mul, uint256 div) external { 139 | uint256 amount = etherToken.balanceOf(this).mul(mul).div(div); 140 | etherToken.withdraw(amount); 141 | } 142 | 143 | // Bancor Network 144 | 145 | function bancorSendEthValue(IBancorNetwork bancor, address[] path, uint256 value) external { 146 | bancor.convert.value(value)(path, value, 1); 147 | } 148 | 149 | function bancorSendEthProportion(IBancorNetwork bancor, address[] path, uint256 mul, uint256 div) external { 150 | uint256 value = address(this).balance.mul(mul).div(div); 151 | bancor.convert.value(value)(path, value, 1); 152 | } 153 | 154 | function bancorApproveTokenAmount(IBancorNetwork bancor, address[] path, uint256 amount) external { 155 | if (ERC20(path[0]).allowance(this, bancor) == 0) { 156 | ERC20(path[0]).asmApprove(bancor, uint256(-1)); 157 | } 158 | bancor.claimAndConvert(path, amount, 1); 159 | } 160 | 161 | function bancorApproveTokenProportion(IBancorNetwork bancor, address[] path, uint256 mul, uint256 div) external { 162 | uint256 amount = ERC20(path[0]).balanceOf(this).mul(mul).div(div); 163 | if (ERC20(path[0]).allowance(this, bancor) == 0) { 164 | ERC20(path[0]).asmApprove(bancor, uint256(-1)); 165 | } 166 | bancor.claimAndConvert(path, amount, 1); 167 | } 168 | 169 | function bancorTransferTokenAmount(IBancorNetwork bancor, address[] path, uint256 amount) external { 170 | ERC20(path[0]).asmTransfer(bancor, amount); 171 | bancor.convert(path, amount, 1); 172 | } 173 | 174 | function bancorTransferTokenProportion(IBancorNetwork bancor, address[] path, uint256 mul, uint256 div) external { 175 | uint256 amount = ERC20(path[0]).balanceOf(this).mul(mul).div(div); 176 | ERC20(path[0]).asmTransfer(bancor, amount); 177 | bancor.convert(path, amount, 1); 178 | } 179 | 180 | function bancorAlreadyTransferedTokenAmount(IBancorNetwork bancor, address[] path, uint256 amount) external { 181 | bancor.convert(path, amount, 1); 182 | } 183 | 184 | function bancorAlreadyTransferedTokenProportion(IBancorNetwork bancor, address[] path, uint256 mul, uint256 div) external { 185 | uint256 amount = ERC20(path[0]).balanceOf(bancor).mul(mul).div(div); 186 | bancor.convert(path, amount, 1); 187 | } 188 | 189 | // Kyber Network 190 | 191 | function kyberSendEthProportion(IKyberNetworkProxy kyber, ERC20 fromToken, address toToken, uint256 mul, uint256 div) external { 192 | uint256 value = address(this).balance.mul(mul).div(div); 193 | kyber.trade.value(value)( 194 | fromToken, 195 | value, 196 | toToken, 197 | this, 198 | 1 << 255, 199 | 0, 200 | 0 201 | ); 202 | } 203 | 204 | function kyberApproveTokenAmount(IKyberNetworkProxy kyber, ERC20 fromToken, address toToken, uint256 amount) external { 205 | if (fromToken.allowance(this, kyber) == 0) { 206 | fromToken.asmApprove(kyber, uint256(-1)); 207 | } 208 | kyber.trade( 209 | fromToken, 210 | amount, 211 | toToken, 212 | this, 213 | 1 << 255, 214 | 0, 215 | 0 216 | ); 217 | } 218 | 219 | function kyberApproveTokenProportion(IKyberNetworkProxy kyber, ERC20 fromToken, address toToken, uint256 mul, uint256 div) external { 220 | uint256 amount = fromToken.balanceOf(this).mul(mul).div(div); 221 | this.kyberApproveTokenAmount(kyber, fromToken, toToken, amount); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /contracts/network/MultiTokenInfo.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/DetailedERC20.sol"; 4 | import "openzeppelin-solidity/contracts/introspection/SupportsInterfaceWithLookup.sol"; 5 | import "../interface/IBasicMultiToken.sol"; 6 | import "../interface/IMultiToken.sol"; 7 | import "../interface/IMultiTokenInfo.sol"; 8 | import "../ext/CheckedERC20.sol"; 9 | 10 | 11 | contract MultiTokenInfo is IMultiTokenInfo, SupportsInterfaceWithLookup { 12 | using CheckedERC20 for DetailedERC20; 13 | 14 | constructor() public { 15 | _registerInterface(InterfaceId_IMultiTokenInfo); 16 | } 17 | 18 | // BasicMultiToken 19 | 20 | function allTokens(IBasicMultiToken mtkn) public view returns(ERC20[] tokens) { 21 | tokens = new ERC20[](mtkn.tokensCount()); 22 | for (uint i = 0; i < tokens.length; i++) { 23 | tokens[i] = mtkn.tokens(i); 24 | } 25 | } 26 | 27 | function allBalances(IBasicMultiToken mtkn) public view returns(uint256[] balances) { 28 | balances = new uint256[](mtkn.tokensCount()); 29 | for (uint i = 0; i < balances.length; i++) { 30 | balances[i] = mtkn.tokens(i).balanceOf(mtkn); 31 | } 32 | } 33 | 34 | function allDecimals(IBasicMultiToken mtkn) public view returns(uint8[] decimals) { 35 | decimals = new uint8[](mtkn.tokensCount()); 36 | for (uint i = 0; i < decimals.length; i++) { 37 | decimals[i] = DetailedERC20(mtkn.tokens(i)).decimals(); 38 | } 39 | } 40 | 41 | function allNames(IBasicMultiToken mtkn) public view returns(bytes32[] names) { 42 | names = new bytes32[](mtkn.tokensCount()); 43 | for (uint i = 0; i < names.length; i++) { 44 | names[i] = DetailedERC20(mtkn.tokens(i)).asmName(); 45 | } 46 | } 47 | 48 | function allSymbols(IBasicMultiToken mtkn) public view returns(bytes32[] symbols) { 49 | symbols = new bytes32[](mtkn.tokensCount()); 50 | for (uint i = 0; i < symbols.length; i++) { 51 | symbols[i] = DetailedERC20(mtkn.tokens(i)).asmSymbol(); 52 | } 53 | } 54 | 55 | function allTokensBalancesDecimalsNamesSymbols(IBasicMultiToken mtkn) public view returns( 56 | ERC20[] tokens, 57 | uint256[] balances, 58 | uint8[] decimals, 59 | bytes32[] names, 60 | bytes32[] symbols 61 | ) { 62 | tokens = allTokens(mtkn); 63 | balances = allBalances(mtkn); 64 | decimals = allDecimals(mtkn); 65 | names = allNames(mtkn); 66 | symbols = allSymbols(mtkn); 67 | } 68 | 69 | // MultiToken 70 | 71 | function allWeights(IMultiToken mtkn) public view returns(uint256[] weights) { 72 | weights = new uint256[](mtkn.tokensCount()); 73 | for (uint i = 0; i < weights.length; i++) { 74 | weights[i] = mtkn.weights(mtkn.tokens(i)); 75 | } 76 | } 77 | 78 | function allTokensBalancesDecimalsNamesSymbolsWeights(IMultiToken mtkn) public view returns( 79 | ERC20[] tokens, 80 | uint256[] balances, 81 | uint8[] decimals, 82 | bytes32[] names, 83 | bytes32[] symbols, 84 | uint256[] weights 85 | ) { 86 | (tokens, balances, decimals, names, symbols) = allTokensBalancesDecimalsNamesSymbols(mtkn); 87 | weights = allWeights(mtkn); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /contracts/network/MultiTokenNetwork.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 4 | import "openzeppelin-solidity/contracts/lifecycle/Pausable.sol"; 5 | import "../AbstractDeployer.sol"; 6 | import "../interface/IMultiToken.sol"; 7 | 8 | 9 | contract MultiTokenNetwork is Pausable { 10 | address[] private _multitokens; 11 | AbstractDeployer[] private _deployers; 12 | 13 | event NewMultitoken(address indexed mtkn); 14 | event NewDeployer(uint256 indexed index, address indexed oldDeployer, address indexed newDeployer); 15 | 16 | function multitokensCount() public view returns(uint256) { 17 | return _multitokens.length; 18 | } 19 | 20 | function multitokens(uint i) public view returns(address) { 21 | return _multitokens[i]; 22 | } 23 | 24 | function allMultitokens() public view returns(address[]) { 25 | return _multitokens; 26 | } 27 | 28 | function deployersCount() public view returns(uint256) { 29 | return _deployers.length; 30 | } 31 | 32 | function deployers(uint i) public view returns(AbstractDeployer) { 33 | return _deployers[i]; 34 | } 35 | 36 | function allWalletBalances(address wallet) public view returns(uint256[]) { 37 | uint256[] memory balances = new uint256[](_multitokens.length); 38 | for (uint i = 0; i < _multitokens.length; i++) { 39 | balances[i] = ERC20(_multitokens[i]).balanceOf(wallet); 40 | } 41 | return balances; 42 | } 43 | 44 | function deleteMultitoken(uint index) public onlyOwner { 45 | require(index < _multitokens.length, "deleteMultitoken: index out of range"); 46 | if (index != _multitokens.length - 1) { 47 | _multitokens[index] = _multitokens[_multitokens.length - 1]; 48 | } 49 | _multitokens.length -= 1; 50 | } 51 | 52 | function deleteDeployer(uint index) public onlyOwner { 53 | require(index < _deployers.length, "deleteDeployer: index out of range"); 54 | if (index != _deployers.length - 1) { 55 | _deployers[index] = _deployers[_deployers.length - 1]; 56 | } 57 | _deployers.length -= 1; 58 | } 59 | 60 | function disableBundlingMultitoken(uint index) public onlyOwner { 61 | IBasicMultiToken(_multitokens[index]).disableBundling(); 62 | } 63 | 64 | function enableBundlingMultitoken(uint index) public onlyOwner { 65 | IBasicMultiToken(_multitokens[index]).enableBundling(); 66 | } 67 | 68 | function disableChangesMultitoken(uint index) public onlyOwner { 69 | IMultiToken(_multitokens[index]).disableChanges(); 70 | } 71 | 72 | function addDeployer(AbstractDeployer deployer) public onlyOwner whenNotPaused { 73 | require(deployer.owner() == address(this), "addDeployer: first set MultiTokenNetwork as owner"); 74 | emit NewDeployer(_deployers.length, address(0), deployer); 75 | _deployers.push(deployer); 76 | } 77 | 78 | function setDeployer(uint256 index, AbstractDeployer deployer) public onlyOwner whenNotPaused { 79 | require(deployer.owner() == address(this), "setDeployer: first set MultiTokenNetwork as owner"); 80 | emit NewDeployer(index, _deployers[index], deployer); 81 | _deployers[index] = deployer; 82 | } 83 | 84 | function deploy(uint256 index, bytes data) public whenNotPaused { 85 | address mtkn = _deployers[index].deploy(data); 86 | _multitokens.push(mtkn); 87 | emit NewMultitoken(mtkn); 88 | } 89 | 90 | function makeCall(address target, uint256 value, bytes data) public onlyOwner { 91 | // solium-disable-next-line security/no-call-value 92 | require(target.call.value(value)(data), "Arbitrary call failed"); 93 | } 94 | } -------------------------------------------------------------------------------- /docs/css/checkbox.css: -------------------------------------------------------------------------------- 1 | /* The container */ 2 | .container { 3 | display: block; 4 | position: relative; 5 | padding-left: 35px; 6 | margin-bottom: 12px; 7 | cursor: pointer; 8 | font-size: 22px; 9 | -webkit-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | } 14 | 15 | /* Hide the browser's default checkbox */ 16 | .container input { 17 | position: absolute; 18 | opacity: 0; 19 | cursor: pointer; 20 | } 21 | 22 | /* Create a custom checkbox */ 23 | .checkmark { 24 | position: absolute; 25 | top: 0; 26 | left: 0; 27 | height: 25px; 28 | width: 25px; 29 | background-color: #eee; 30 | } 31 | 32 | /* On mouse-over, add a grey background color */ 33 | .container:hover input ~ .checkmark { 34 | background-color: #ccc; 35 | } 36 | 37 | /* When the checkbox is checked, add a blue background */ 38 | .container input:checked ~ .checkmark { 39 | background-color: #2196F3; 40 | } 41 | 42 | /* Create the checkmark/indicator (hidden when not checked) */ 43 | .checkmark:after { 44 | content: ""; 45 | position: absolute; 46 | display: none; 47 | } 48 | 49 | /* Show the checkmark when checked */ 50 | .container input:checked ~ .checkmark:after { 51 | display: block; 52 | } 53 | 54 | /* Style the checkmark/indicator */ 55 | .container .checkmark:after { 56 | left: 9px; 57 | top: 5px; 58 | width: 5px; 59 | height: 10px; 60 | border: solid white; 61 | border-width: 0 3px 3px 0; 62 | -webkit-transform: rotate(45deg); 63 | -ms-transform: rotate(45deg); 64 | transform: rotate(45deg); 65 | } -------------------------------------------------------------------------------- /docs/css/list.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | #myInput { 6 | background-position: 10px 12px; 7 | background-repeat: no-repeat; 8 | width: 100%; 9 | font-size: 16px; 10 | padding: 12px 20px 12px 40px; 11 | border: 1px solid #ddd; 12 | margin-bottom: 12px; 13 | } 14 | 15 | #myUL { 16 | list-style-type: none; 17 | padding: 0; 18 | margin: 0; 19 | } 20 | 21 | #myUL li a { 22 | border: 1px solid #ddd; 23 | margin-top: -1px; /* Prevent double borders */ 24 | background-color: #f6f6f6; 25 | padding: 12px; 26 | text-decoration: none; 27 | font-size: 18px; 28 | color: black; 29 | display: block 30 | } 31 | 32 | #myUL li a:hover:not(.header) { 33 | background-color: #eee; 34 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MultiToken 5 | 6 | 7 | 8 | 9 | 10 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 |
37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 |
49 | 50 | 51 |
52 |
53 | 54 | 55 |
56 |
57 | 58 | 59 |
60 |
61 | 62 | 66 |
67 |
68 |
69 |
70 | 73 |
74 | 75 |
76 |
77 |
78 |
79 |
80 |
81 | 82 | 83 |
84 |
85 | 86 | 87 |
88 |
89 | 90 | 91 |
92 |
93 | 94 | 95 |
96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
#NameDecimalsBalanceWeightAddress
113 | 114 | 115 | 116 |
117 |
118 | 119 | 120 |
121 |
122 | 123 | 124 |
125 |
126 |
127 | 128 | 129 |
130 | 131 |
132 |
133 | ETH 134 |
135 | 136 |
137 | 138 |
139 |
140 | 141 |
142 |
143 | MTKN 144 |
145 | 146 |
147 | 148 |
149 |
150 | 151 | 152 |
153 |
154 |
155 |
156 |
157 | 160 |
161 | 162 |
163 |
164 |
165 |
166 | 167 | 168 |
169 |
170 |
171 | 172 | 173 |
174 |
175 | 176 | 177 |
178 |
179 |
180 |
181 | 182 | 183 |
184 |
185 | 186 | 187 |
188 |
189 |
190 |
191 | 192 |
193 |
194 | % 195 |
196 | 197 |
198 |
199 |
200 | 201 |
202 |
203 | % 204 |
205 | 206 |
207 |
208 |
209 | 210 | 211 |
212 |
213 |
214 | 215 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | If you are not redirected automatically, follow this link to site. 12 | 13 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | // var Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function (deployer) { 4 | // deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | // const MultiToken = artifacts.require("MultiToken"); 2 | 3 | module.exports = function (deployer) { 4 | // deployer.deploy(MultiToken); 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MultiToken", 3 | "version": "1.0.0", 4 | "description": "ERC20 token solidity smart contract allowing aggreagate ERC20 tokens", 5 | "main": "y", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "devDependencies": { 10 | "chai": "^4.1.2", 11 | "chai-as-promised": "^7.1.1", 12 | "chai-bignumber": "^2.0.2", 13 | "coveralls": "^3.0.2", 14 | "eslint": "^4.19.1", 15 | "eslint-config-defaults": "^9.0.0", 16 | "eslint-config-standard": "^11.0.0", 17 | "eslint-plugin-import": "^2.14.0", 18 | "eslint-plugin-node": "^6.0.1", 19 | "eslint-plugin-promise": "^3.8.0", 20 | "eslint-plugin-react": "^7.11.1", 21 | "eslint-plugin-standard": "^3.1.0", 22 | "ethereumjs-abi": "^0.6.5", 23 | "ethereumjs-tx": "^1.3.7", 24 | "ethereumjs-util": "^5.2.0", 25 | "ethereumjs-wallet": "^0.6.2", 26 | "ethjs-abi": "^0.2.1", 27 | "ganache-cli": "^6.1.8", 28 | "ganache-core": "^2.2.1", 29 | "node-fetch": "^2.2.0", 30 | "openzeppelin-solidity": "^1.12.0", 31 | "solidity-coverage": "^0.5.11", 32 | "solium": "^1.1.8", 33 | "truffle": "^4.1.14", 34 | "web3": "^1.0.0-beta.36", 35 | "web3-provider-engine": "^14.0.6", 36 | "web3-utils": "^1.0.0-beta.36" 37 | }, 38 | "scripts": { 39 | "test": "scripts/test.sh", 40 | "coverage": "scripts/coverage.sh", 41 | "lint:js": "eslint .", 42 | "lint:js:fix": "eslint . --fix", 43 | "lint:sol": "solium -d .", 44 | "lint:sol:fix": "solium -d . --fix", 45 | "lint": "npm run lint:js && npm run lint:sol", 46 | "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", 47 | "rpc": "scripts/rpc.sh" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "git+https://github.com/k06a/MultiToken.git" 52 | }, 53 | "keywords": [ 54 | "solidity", 55 | "truffle" 56 | ], 57 | "author": "Anton Bukov", 58 | "license": "MIT", 59 | "bugs": { 60 | "url": "https://github.com/k06a/MultiToken/issues" 61 | }, 62 | "homepage": "https://github.com/k06a/MultiToken#readme" 63 | } 64 | -------------------------------------------------------------------------------- /scripts/arbiter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const privateKey = '0x668a369e87c01da5bfca9851e6ee86d760e17ee7912d77b7dffe8e0cdf63bcb5'; 4 | const multiTokensAddresses = [ 5 | '0x5C4fC01E5d687F1d2C627bA7DE3A59c33aEFaA35', 6 | ]; 7 | 8 | // eslint-disable-next-line max-len 9 | const erc20ABI = [{ 'anonymous': false, 'inputs': [{ 'indexed': true, 'name': 'from', 'type': 'address' }, { 'indexed': true, 'name': 'to', 'type': 'address' }, { 'indexed': false, 'name': 'value', 'type': 'uint256' }], 'name': 'Transfer', 'type': 'event' }, { 'constant': true, 'inputs': [], 'name': 'totalSupply', 'outputs': [{ 'name': '', 'type': 'uint256' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': true, 'inputs': [{ 'name': 'who', 'type': 'address' }], 'name': 'balanceOf', 'outputs': [{ 'name': '', 'type': 'uint256' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': false, 'inputs': [{ 'name': 'to', 'type': 'address' }, { 'name': 'value', 'type': 'uint256' }], 'name': 'transfer', 'outputs': [{ 'name': '', 'type': 'bool' }], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function' }]; 10 | // eslint-disable-next-line max-len 11 | const multiABI = [{ 'constant': true, 'inputs': [], 'name': 'name', 'outputs': [{ 'name': '', 'type': 'string' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': false, 'inputs': [{ 'name': '_spender', 'type': 'address' }, { 'name': '_value', 'type': 'uint256' }], 'name': 'approve', 'outputs': [{ 'name': '', 'type': 'bool' }], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function' }, { 'constant': false, 'inputs': [{ 'name': '_value', 'type': 'uint256' }, { 'name': 'someTokens', 'type': 'address[]' }], 'name': 'unbundleSome', 'outputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function' }, { 'constant': true, 'inputs': [], 'name': 'totalSupply', 'outputs': [{ 'name': '', 'type': 'uint256' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': false, 'inputs': [{ 'name': '_from', 'type': 'address' }, { 'name': '_to', 'type': 'address' }, { 'name': '_value', 'type': 'uint256' }], 'name': 'transferFrom', 'outputs': [{ 'name': '', 'type': 'bool' }], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function' }, { 'constant': true, 'inputs': [], 'name': 'decimals', 'outputs': [{ 'name': '', 'type': 'uint8' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': false, 'inputs': [{ 'name': '_to', 'type': 'address' }, { 'name': '_amount', 'type': 'uint256' }], 'name': 'bundle', 'outputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function' }, { 'constant': false, 'inputs': [{ 'name': '_value', 'type': 'uint256' }], 'name': 'unbundle', 'outputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function' }, { 'constant': true, 'inputs': [{ 'name': '', 'type': 'uint256' }], 'name': 'tokens', 'outputs': [{ 'name': '', 'type': 'address' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': false, 'inputs': [{ 'name': '_spender', 'type': 'address' }, { 'name': '_subtractedValue', 'type': 'uint256' }], 'name': 'decreaseApproval', 'outputs': [{ 'name': '', 'type': 'bool' }], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function' }, { 'constant': true, 'inputs': [], 'name': 'res', 'outputs': [{ 'name': '', 'type': 'uint256' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': true, 'inputs': [{ 'name': '_owner', 'type': 'address' }], 'name': 'balanceOf', 'outputs': [{ 'name': '', 'type': 'uint256' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': true, 'inputs': [], 'name': 'symbol', 'outputs': [{ 'name': '', 'type': 'string' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': true, 'inputs': [{ 'name': '', 'type': 'address' }], 'name': 'weights', 'outputs': [{ 'name': '', 'type': 'uint256' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': false, 'inputs': [{ 'name': '_to', 'type': 'address' }, { 'name': '_value', 'type': 'uint256' }], 'name': 'transfer', 'outputs': [{ 'name': '', 'type': 'bool' }], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function' }, { 'constant': false, 'inputs': [{ 'name': '_spender', 'type': 'address' }, { 'name': '_addedValue', 'type': 'uint256' }], 'name': 'increaseApproval', 'outputs': [{ 'name': '', 'type': 'bool' }], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function' }, { 'constant': true, 'inputs': [{ 'name': '_owner', 'type': 'address' }, { 'name': '_spender', 'type': 'address' }], 'name': 'allowance', 'outputs': [{ 'name': '', 'type': 'uint256' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': false, 'inputs': [{ 'name': '_to', 'type': 'address' }, { 'name': '_amount', 'type': 'uint256' }, { 'name': '_tokenAmounts', 'type': 'uint256[]' }], 'name': 'bundleFirstTokens', 'outputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function' }, { 'inputs': [{ 'name': '_tokens', 'type': 'address[]' }, { 'name': '_weights', 'type': 'uint256[]' }, { 'name': '_name', 'type': 'string' }, { 'name': '_symbol', 'type': 'string' }, { 'name': '_decimals', 'type': 'uint8' }], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'constructor' }, { 'anonymous': false, 'inputs': [], 'name': 'Update', 'type': 'event' }, { 'anonymous': false, 'inputs': [{ 'indexed': true, 'name': '_fromToken', 'type': 'address' }, { 'indexed': true, 'name': '_toToken', 'type': 'address' }, { 'indexed': true, 'name': '_changer', 'type': 'address' }, { 'indexed': false, 'name': '_amount', 'type': 'uint256' }, { 'indexed': false, 'name': '_return', 'type': 'uint256' }], 'name': 'Change', 'type': 'event' }, { 'anonymous': false, 'inputs': [{ 'indexed': true, 'name': 'minter', 'type': 'address' }, { 'indexed': false, 'name': 'value', 'type': 'uint256' }], 'name': 'Bundle', 'type': 'event' }, { 'anonymous': false, 'inputs': [{ 'indexed': true, 'name': 'burner', 'type': 'address' }, { 'indexed': false, 'name': 'value', 'type': 'uint256' }], 'name': 'Unbundle', 'type': 'event' }, { 'anonymous': false, 'inputs': [{ 'indexed': true, 'name': 'owner', 'type': 'address' }, { 'indexed': true, 'name': 'spender', 'type': 'address' }, { 'indexed': false, 'name': 'value', 'type': 'uint256' }], 'name': 'Approval', 'type': 'event' }, { 'anonymous': false, 'inputs': [{ 'indexed': true, 'name': 'from', 'type': 'address' }, { 'indexed': true, 'name': 'to', 'type': 'address' }, { 'indexed': false, 'name': 'value', 'type': 'uint256' }], 'name': 'Transfer', 'type': 'event' }, { 'constant': true, 'inputs': [], 'name': 'tokensCount', 'outputs': [{ 'name': 'count', 'type': 'uint16' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': true, 'inputs': [{ 'name': '_tokenIndex', 'type': 'uint16' }], 'name': 'tokens', 'outputs': [{ 'name': 'tokenAddress', 'type': 'address' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': true, 'inputs': [{ 'name': '_fromToken', 'type': 'address' }, { 'name': '_toToken', 'type': 'address' }, { 'name': '_amount', 'type': 'uint256' }], 'name': 'getReturn', 'outputs': [{ 'name': 'returnAmount', 'type': 'uint256' }], 'payable': false, 'stateMutability': 'view', 'type': 'function' }, { 'constant': false, 'inputs': [{ 'name': '_fromToken', 'type': 'address' }, { 'name': '_toToken', 'type': 'address' }, { 'name': '_amount', 'type': 'uint256' }, { 'name': '_minReturn', 'type': 'uint256' }], 'name': 'change', 'outputs': [{ 'name': 'returnAmount', 'type': 'uint256' }], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function' }]; 12 | 13 | const Web3 = require('web3'); 14 | const fetch = require('node-fetch'); 15 | 16 | process.on('unhandledRejection', (reason, p) => { 17 | console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); 18 | }); 19 | 20 | (async function () { 21 | // const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')); 22 | // const web3 = new Web3(new Web3.providers.HttpProvider('https://ropsten.infura.io/')); 23 | // const web3 = new Web3(new Web3.providers.WebsocketProvider('ws://localhost:8546')); 24 | const web3 = new Web3(new Web3.providers.WebsocketProvider('ws://ropsten.infura.io/ws')); 25 | // const web3 = new Web3(new Web3.providers.HttpProvider('https://mainnet.infura.io/GOGw1ym3Hu5NytWUre29')); 26 | 27 | if (privateKey.length !== 66) { 28 | console.log('privateKey should be of length 66.' + (privateKey.length === 64 ? ' Prepend with "0x".' : '')); 29 | return; 30 | } 31 | const account = web3.eth.accounts.privateKeyToAccount(privateKey); 32 | console.log('account = ' + account.address); 33 | 34 | const bancorData = await (await fetch('https://api.bancor.network/0.1/currencies/tokens?limit=100&skip=0')).json(); 35 | const bancorTokens = bancorData.data.currencies.page; 36 | const bancorPriceByToken = {}; 37 | const bancorCurrencyIdByToken = {}; 38 | for (let bancorToken of bancorTokens) { 39 | // console.log(bancorToken); 40 | bancorPriceByToken[bancorToken.details.contractAddress] = bancorToken.price; 41 | bancorCurrencyIdByToken[bancorToken.details.contractAddress] = bancorToken.originalCurrencyId; 42 | } 43 | 44 | for (let multiTokenAddress of multiTokensAddresses) { 45 | const multiToken = new web3.eth.Contract(multiABI, multiTokenAddress); 46 | const tokensCount = await multiToken.methods.tokensCount().call(); 47 | 48 | const tokens = await Promise.all( 49 | Array.from({ length: tokensCount }, (x, i) => multiToken.methods.tokens(i).call()) 50 | ); 51 | 52 | // Token weights 53 | const weights = await Promise.all( 54 | tokens.map(ta => multiToken.methods.weights(ta).call()) 55 | ); 56 | const tokenWeights = new Map(weights.map((w, i) => [tokens[i], w])); 57 | 58 | // Token amounts 59 | const amounts = await Promise.all( 60 | Array.from(tokens, ta => new web3.eth.Contract(erc20ABI, ta)) 61 | .map(t => t.methods.balanceOf(multiTokenAddress).call()) 62 | ); 63 | const tokenAmounts = new Map(amounts.map((a, i) => [tokens[i], a])); 64 | 65 | // Find profit n^2 (need optimize) 66 | let bestTokenA; 67 | let bestTokenB; 68 | let bestRatio; 69 | for (let tokenAddressA of tokens) { 70 | for (let tokenAddressB of tokens) { 71 | if (tokenAddressA === tokenAddressB) { 72 | continue; 73 | } 74 | 75 | const mutiTokenPrice = tokenAmounts[tokenAddressA] * tokenWeights[tokenAddressA] / (tokenAmounts[tokenAddressB] * tokenWeights[tokenAddressB]); 76 | const bancorTokenPrice = bancorPriceByToken[tokenAddressA] / bancorPriceByToken[tokenAddressB]; 77 | if (mutiTokenPrice / bancorTokenPrice > bestRatio) { 78 | bestRatio = mutiTokenPrice / bancorTokenPrice; 79 | bestTokenA = tokenAddressA; 80 | bestTokenB = tokenAddressB; 81 | } 82 | } 83 | } 84 | 85 | // If more than 1% 86 | // if (bestRatio > 1.01) { 87 | bestRatio = 1.01; 88 | bestTokenA = tokens[0]; 89 | bestTokenB = tokens[1]; 90 | const bestTokenAmountA = tokenAmounts.get(bestTokenA); 91 | const bestTokenAmountB = tokenAmounts.get(bestTokenB); 92 | 93 | const percent = (bestRatio - 1) / 2; 94 | const amount = bestTokenAmountA * percent; 95 | const minimumReturn = bestTokenAmountB - bestTokenAmountB / (1 + percent); 96 | console.log(amount, minimumReturn); 97 | // const multiGas = await multiToken.methods.change(bestTokenB, bestTokenA, amount, minimumReturn).estimateGas(); 98 | 99 | const fromCurrencyId = bancorCurrencyIdByToken[bestTokenB]; 100 | const toCurrencyId = bancorCurrencyIdByToken[bestTokenA]; 101 | const ownerAddress = account.address; 102 | const json = await (await fetch('https://api.bancor.network/0.1/currencies/convert' + 103 | '?fromCurrencyId=' + fromCurrencyId + 104 | '&toCurrencyId=' + toCurrencyId + 105 | '&amount=' + amount + 106 | '&minimumReturn=' + minimumReturn + 107 | '&ownerAddress=' + ownerAddress)).json(); 108 | console.log(json); 109 | } 110 | 111 | // var prevBalance = 0; 112 | 113 | // web3.eth.subscribe('newBlockHeaders', async function (error, blockHeader) { 114 | // if (error) { 115 | // console.log('error: ' + error); 116 | // return; 117 | // } 118 | 119 | // console.log('================================================================'); 120 | // console.log('block number = ' + blockHeader.number); 121 | 122 | // const [ 123 | // balance, 124 | // nonce, 125 | // ] = (await Promise.all([ 126 | // web3.eth.getBalance(account.address), 127 | // web3.eth.getTransactionCount(account.address), 128 | // ])).map(v => web3.utils.toBN(v)); 129 | 130 | // // const balance = web3.utils.toBN(await web3.eth.getBalance(account.address)); 131 | // console.log('balance = ' + balance); 132 | // // const nonce = await web3.eth.getTransactionCount(account.address); 133 | // console.log('nonce = ' + nonce); 134 | 135 | // if (balance.toString() == prevBalance) { 136 | // console.log('Skipping known balance'); 137 | // return; 138 | // } 139 | // prevBalance = balance.toString(); 140 | 141 | // const gas = web3.utils.toBN('21000'); 142 | // const maxGasPrice = balance.div(gas); 143 | // console.log('maxGasPrice = ' + maxGasPrice.toString()); 144 | // const bestGasPrice = maxGasPrice.mul(web3.utils.toBN('1000')).div(web3.utils.toBN('1045')); 145 | // console.log('bestGasPrice = ' + bestGasPrice.toString()); 146 | // const value = balance.sub(bestGasPrice.mul(gas)); 147 | // console.log('value = ' + value.toString()); 148 | 149 | // // If less then 0.1 Gwei 150 | // if (bestGasPrice.lt(web3.utils.toBN('100000000'))) { 151 | // console.log('Not enough balance to pay tx fee'); 152 | // return; 153 | // } 154 | 155 | // const txData = { 156 | // from: account.address, 157 | // to: wallet, 158 | // value: value, 159 | // gasPrice: bestGasPrice, 160 | // gas: gas, 161 | // nonce: nonce, 162 | // }; 163 | // const transaction = new Tx(txData); 164 | // transaction.sign(new Buffer(privateKey.substr(2), 'hex')); 165 | // const serializedTx = transaction.serialize().toString('hex'); 166 | // web3.eth.sendSignedTransaction('0x' + serializedTx).on('transactionHash', function (hash) { 167 | // console.log('transaction = https://etherscan.io/tx/' + hash); 168 | // }); 169 | // }).on('data', async function (blockHeader) { 170 | // }); 171 | })(); 172 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SOLIDITY_COVERAGE=true scripts/test.sh 4 | -------------------------------------------------------------------------------- /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 ganache instance that we started (if we started one and if it's still running). 11 | if [ -n "$ganache_pid" ] && ps -p $ganache_pid > /dev/null; then 12 | kill -9 $ganache_pid 13 | fi 14 | } 15 | 16 | if [ "$SOLIDITY_COVERAGE" = true ]; then 17 | ganache_port=8555 18 | else 19 | ganache_port=9545 20 | fi 21 | 22 | ganache_running() { 23 | nc -z localhost "$ganache_port" 24 | } 25 | 26 | start_ganache() { 27 | # We define 10 accounts with balance 1M ether, needed for high-value tests. 28 | local accounts=( 29 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200,1000000000000000000000000000000" 30 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201,1000000000000000000000000000000" 31 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202,1000000000000000000000000000000" 32 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203,1000000000000000000000000000000" 33 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204,1000000000000000000000000000000" 34 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205,1000000000000000000000000000000" 35 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206,1000000000000000000000000000000" 36 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000000000" 37 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000000000" 38 | --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000000000" 39 | ) 40 | 41 | if [ "$SOLIDITY_COVERAGE" = true ]; then 42 | node_modules/.bin/testrpc-sc --gasLimit 0xfffffffffff --port "$ganache_port" "${accounts[@]}" > /dev/null & 43 | else 44 | node_modules/.bin/ganache-cli --gasLimit 0xfffffffffff --port "$ganache_port" "${accounts[@]}" > /dev/null & 45 | fi 46 | 47 | ganache_pid=$! 48 | } 49 | 50 | if ganache_running; then 51 | echo "Using existing ganache instance" 52 | else 53 | echo "Starting our own ganache instance" 54 | start_ganache 55 | fi 56 | 57 | if [ "$SOLC_NIGHTLY" = true ]; then 58 | echo "Downloading solc nightly" 59 | wget -q https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/soljson-nightly.js -O /tmp/soljson.js && find . -name soljson.js -exec cp /tmp/soljson.js {} \; 60 | fi 61 | 62 | truffle version 63 | 64 | if [ "$SOLIDITY_COVERAGE" = true ]; then 65 | node_modules/.bin/solidity-coverage 66 | 67 | if [ "$CONTINUOUS_INTEGRATION" = true ]; then 68 | cat coverage/lcov.info | node_modules/.bin/coveralls 69 | fi 70 | else 71 | node_modules/.bin/truffle test "$@" 72 | fi 73 | -------------------------------------------------------------------------------- /test/BasicMultiToken.js: -------------------------------------------------------------------------------- 1 | const EVMRevert = require('./helpers/EVMRevert'); 2 | const EVMThrow = require('./helpers/EVMThrow'); 3 | 4 | require('chai') 5 | .use(require('chai-as-promised')) 6 | .use(require('chai-bignumber')(web3.BigNumber)) 7 | .should(); 8 | 9 | const Token = artifacts.require('Token.sol'); 10 | const BadToken = artifacts.require('BadToken.sol'); 11 | const BrokenTransferToken = artifacts.require('BrokenTransferToken.sol'); 12 | const BrokenTransferFromToken = artifacts.require('BrokenTransferFromToken.sol'); 13 | const BasicMultiToken = artifacts.require('BasicMultiToken.sol'); 14 | 15 | contract('BasicMultiToken', function ([_, wallet1, wallet2, wallet3, wallet4, wallet5]) { 16 | let abc; 17 | let xyz; 18 | let lmn; 19 | let multi; 20 | 21 | beforeEach(async function () { 22 | abc = await Token.new('ABC'); 23 | await abc.mint(_, 1000e6); 24 | await abc.mint(wallet1, 50e6); 25 | await abc.mint(wallet2, 50e6); 26 | 27 | xyz = await BadToken.new('BadToken', 'XYZ', 18); 28 | await xyz.mint(_, 500e6); 29 | await xyz.mint(wallet1, 50e6); 30 | await xyz.mint(wallet2, 50e6); 31 | 32 | lmn = await Token.new('LMN'); 33 | await lmn.mint(_, 100e6); 34 | }); 35 | 36 | it('should fail to create multitoken with wrong arguments', async function () { 37 | const mtkn = await BasicMultiToken.new(); 38 | await mtkn.init([abc.address], 'Multi', '1ABC', 18).should.be.rejectedWith(EVMRevert); 39 | await mtkn.init([xyz.address], 'Multi', '1XYZ', 18).should.be.rejectedWith(EVMRevert); 40 | await mtkn.init([abc.address, xyz.address], '', '1ABC', 18).should.be.rejectedWith(EVMRevert); 41 | await mtkn.init([abc.address, xyz.address], 'Multi', '', 18).should.be.rejectedWith(EVMRevert); 42 | await mtkn.init([abc.address, xyz.address], 'Multi', '1ABC', 0).should.be.rejectedWith(EVMRevert); 43 | }); 44 | 45 | describe('bundle', async function () { 46 | beforeEach(async function () { 47 | multi = await BasicMultiToken.new(); 48 | await multi.init([abc.address, xyz.address], 'Multi', '1ABC_1XYZ', 18); 49 | }); 50 | 51 | it('should not bundle first tokens with bundle method', async function () { 52 | await multi.bundle(_, 1).should.be.rejectedWith(EVMRevert); 53 | }); 54 | 55 | it('should bundle second tokens with bundle method', async function () { 56 | await abc.approve(multi.address, 1000e6); 57 | await xyz.approve(multi.address, 500e6); 58 | await multi.bundleFirstTokens(_, 1000, [1000e6, 500e6]); 59 | 60 | await abc.approve(multi.address, 10e6, { from: wallet1 }); 61 | await xyz.approve(multi.address, 5e6, { from: wallet1 }); 62 | await multi.bundle(wallet1, 10, { from: wallet1 }); 63 | }); 64 | 65 | it('should bundle first tokens with bundleFirstTokens method', async function () { 66 | await abc.approve(multi.address, 1000e6); 67 | await xyz.approve(multi.address, 500e6); 68 | await multi.bundleFirstTokens(_, 1000, [1000e6, 500e6]); 69 | }); 70 | 71 | it('should not bundle second tokens with bundleFirstTokens method', async function () { 72 | await abc.approve(multi.address, 1002e6); 73 | await xyz.approve(multi.address, 501e6); 74 | await multi.bundleFirstTokens(_, 1000, [1000e6, 500e6]); 75 | await multi.bundleFirstTokens(_, 1, [2e6, 1e6]).should.be.rejectedWith(EVMRevert); 76 | }); 77 | 78 | it('should not bundle invalid number of volumes', async function () { 79 | await abc.approve(multi.address, 1002e6); 80 | await xyz.approve(multi.address, 501e6); 81 | await multi.bundleFirstTokens(_, 1000, [1000e6, 500e6, 100e6]).should.be.rejectedWith(EVMRevert); 82 | await multi.bundleFirstTokens(_, 1, [2e6]).should.be.rejectedWith(EVMRevert); 83 | }); 84 | 85 | it('should handle wrong transferFrom of tokens', async function () { 86 | const _abc = await BrokenTransferFromToken.new('ABC'); 87 | await _abc.mint(_, 1000e6); 88 | const _xyz = await BrokenTransferFromToken.new('XYZ'); 89 | await _xyz.mint(_, 500e6); 90 | 91 | const brokenMulti = await BasicMultiToken.new(); 92 | await brokenMulti.init([_abc.address, _xyz.address], 'Multi', '1ABC_1XYZ', 18); 93 | await _abc.approve(brokenMulti.address, 1000e6); 94 | await _xyz.approve(brokenMulti.address, 500e6); 95 | await brokenMulti.bundleFirstTokens(_, 1000, [1000e6, 500e6]).should.be.rejectedWith(EVMRevert); 96 | }); 97 | }); 98 | 99 | describe('unbundle', async function () { 100 | beforeEach(async function () { 101 | multi = await BasicMultiToken.new(); 102 | await multi.init([abc.address, xyz.address], 'Multi', '1ABC_1XYZ', 18); 103 | await abc.approve(multi.address, 1000e6); 104 | await xyz.approve(multi.address, 500e6); 105 | await multi.bundleFirstTokens(_, 1000, [1000e6, 500e6]); 106 | }); 107 | 108 | it('should not unbundle when no tokens', async function () { 109 | await multi.unbundle(wallet1, 1, { from: wallet1 }).should.be.rejectedWith(EVMThrow); 110 | }); 111 | 112 | it('should not unbundle too many tokens', async function () { 113 | await multi.unbundle(_, 1001).should.be.rejectedWith(EVMThrow); 114 | }); 115 | 116 | it('should unbundle owned tokens', async function () { 117 | await multi.unbundle(_, 200); 118 | await multi.unbundle(_, 801).should.be.rejectedWith(EVMThrow); 119 | await multi.unbundle(_, 300); 120 | await multi.unbundle(_, 501).should.be.rejectedWith(EVMThrow); 121 | await multi.unbundle(_, 500); 122 | await multi.unbundle(_, 1).should.be.rejectedWith(EVMThrow); 123 | 124 | (await abc.balanceOf.call(multi.address)).should.be.bignumber.equal(0); 125 | (await xyz.balanceOf.call(multi.address)).should.be.bignumber.equal(0); 126 | }); 127 | 128 | it('should not be able to unbundle none tokens', async function () { 129 | await multi.unbundleSome(_, 100, []).should.be.rejectedWith(EVMRevert); 130 | }); 131 | 132 | it('should be able to unbundleSome in case of first tokens paused', async function () { 133 | await abc.pause(); 134 | await multi.unbundle(_, 500).should.be.rejectedWith(EVMRevert); 135 | 136 | const xyzBalance = await xyz.balanceOf.call(multi.address); 137 | await multi.unbundleSome(_, 500, [xyz.address]); 138 | (await multi.balanceOf.call(_)).should.be.bignumber.equal(500); 139 | (await xyz.balanceOf.call(_)).should.be.bignumber.equal(xyzBalance / 2); 140 | }); 141 | 142 | it('should be able to unbundleSome in case of last tokens paused', async function () { 143 | await xyz.pause(); 144 | await multi.unbundle(_, 500).should.be.rejectedWith(EVMRevert); 145 | 146 | const abcBalance = await abc.balanceOf.call(multi.address); 147 | await multi.unbundleSome(_, 500, [abc.address]); 148 | (await multi.balanceOf.call(_)).should.be.bignumber.equal(500); 149 | (await abc.balanceOf.call(_)).should.be.bignumber.equal(abcBalance / 2); 150 | }); 151 | 152 | it('should be able to receive airdrop while unbundle', async function () { 153 | await lmn.transfer(multi.address, 100e6); 154 | 155 | const lmnBalance = await lmn.balanceOf.call(multi.address); 156 | await multi.unbundleSome(_, 500, [abc.address, xyz.address, lmn.address]); 157 | (await multi.balanceOf.call(_)).should.be.bignumber.equal(500); 158 | (await lmn.balanceOf.call(_)).should.be.bignumber.equal(lmnBalance / 2); 159 | }); 160 | 161 | it('should handle wrong transfer of first token', async function () { 162 | const _abc = await BrokenTransferToken.new('ABC'); 163 | await _abc.mint(_, 1000e6); 164 | const _xyz = await Token.new('XYZ'); 165 | await _xyz.mint(_, 500e6); 166 | 167 | const brokenMulti = await BasicMultiToken.new(); 168 | await brokenMulti.init([_abc.address, _xyz.address], 'Multi', '1ABC_1XYZ', 18); 169 | await _abc.approve(brokenMulti.address, 1000e6); 170 | await _xyz.approve(brokenMulti.address, 500e6); 171 | await brokenMulti.bundleFirstTokens(_, 1000, [1000e6, 500e6]); 172 | 173 | await brokenMulti.unbundle(_, 100).should.be.rejectedWith(EVMRevert); 174 | await brokenMulti.unbundleSome(_, 100, [_abc.address]).should.be.rejectedWith(EVMRevert); 175 | await brokenMulti.unbundleSome(_, 100, [_xyz.address]).should.be.fulfilled; 176 | }); 177 | 178 | it('should handle wrong transfer of last token', async function () { 179 | const _abc = await Token.new('ABC'); 180 | await _abc.mint(_, 1000e6); 181 | const _xyz = await BrokenTransferToken.new('XYZ'); 182 | await _xyz.mint(_, 500e6); 183 | 184 | const brokenMulti = await BasicMultiToken.new(); 185 | await brokenMulti.init([_abc.address, _xyz.address], 'Multi', '1ABC_1XYZ', 18); 186 | await _abc.approve(brokenMulti.address, 1000e6); 187 | await _xyz.approve(brokenMulti.address, 500e6); 188 | await brokenMulti.bundleFirstTokens(_, 1000, [1000e6, 500e6]); 189 | 190 | await brokenMulti.unbundle(_, 100).should.be.rejectedWith(EVMRevert); 191 | await brokenMulti.unbundleSome(_, 100, [_xyz.address]).should.be.rejectedWith(EVMRevert); 192 | await brokenMulti.unbundleSome(_, 100, [_abc.address]).should.be.fulfilled; 193 | }); 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /test/MultiToken.js: -------------------------------------------------------------------------------- 1 | const EVMRevert = require('./helpers/EVMRevert'); 2 | const { assertRevert } = require('./helpers/assertRevert'); 3 | 4 | require('chai') 5 | .use(require('chai-as-promised')) 6 | .use(require('chai-bignumber')(web3.BigNumber)) 7 | .should(); 8 | 9 | const Token = artifacts.require('Token.sol'); 10 | const BadToken = artifacts.require('BadToken.sol'); 11 | const MultiToken = artifacts.require('MultiToken.sol'); 12 | 13 | contract('MultiToken', function ([_, wallet1, wallet2, wallet3, wallet4, wallet5]) { 14 | let abc; 15 | let xyz; 16 | let lmn; 17 | let multi; 18 | 19 | beforeEach(async function () { 20 | abc = await Token.new('ABC'); 21 | await abc.mint(_, 1000e6); 22 | await abc.mint(wallet1, 50e6); 23 | 24 | xyz = await BadToken.new('BadToken', 'XYZ', 18); 25 | await xyz.mint(_, 500e6); 26 | await xyz.mint(wallet2, 50e6); 27 | 28 | lmn = await Token.new('LMN'); 29 | await lmn.mint(_, 100e6); 30 | 31 | multi = await MultiToken.new(); 32 | }); 33 | 34 | it('should failure on wrong constructor arguments', async function () { 35 | assertRevert(function () { 36 | multi.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address], [1, 1, 1], 'Multi', '1ABC_1XYZ', 18, { from: _, gas: 8000000 }); 37 | }); 38 | assertRevert(function () { 39 | multi.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address], [1], 'Multi', '1ABC_1XYZ', 18, { from: _, gas: 8000000 }); 40 | }); 41 | assertRevert(function () { 42 | multi.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address], [1, 0], 'Multi', '1ABC_0XYZ', 18, { from: _, gas: 8000000 }); 43 | }); 44 | assertRevert(function () { 45 | multi.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address], [0, 1], 'Multi', '0ABC_1XYZ', 18, { from: _, gas: 8000000 }); 46 | }); 47 | }); 48 | 49 | describe('ERC228', async function () { 50 | it('should have correct tokensCount implementation', async function () { 51 | const multi2 = await MultiToken.new(); 52 | await multi2.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address], [1, 1], 'Multi', '1ABC_1XYZ', 18, { from: _, gas: 8000000 }); 53 | (await multi2.tokensCount.call()).should.be.bignumber.equal(2); 54 | 55 | const multi3 = await MultiToken.new(); 56 | await multi3.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address, lmn.address], [1, 1, 1], 'Multi', '1ABC_1XYZ', 18, { from: _, gas: 8000000 }); 57 | (await multi3.tokensCount.call()).should.be.bignumber.equal(3); 58 | }); 59 | 60 | it('should have correct tokens implementation', async function () { 61 | const multi2 = await MultiToken.new(); 62 | await multi2.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address], [1, 1], 'Multi', '1ABC_1XYZ', 18, { from: _, gas: 8000000 }); 63 | (await multi2.tokens.call(0)).should.be.equal(abc.address); 64 | (await multi2.tokens.call(1)).should.be.equal(xyz.address); 65 | 66 | const multi3 = await MultiToken.new(); 67 | await multi3.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address, lmn.address], [1, 1, 1], 'Multi', '1ABC_1XYZ', 18, { from: _, gas: 8000000 }); 68 | (await multi3.tokens.call(0)).should.be.equal(abc.address); 69 | (await multi3.tokens.call(1)).should.be.equal(xyz.address); 70 | (await multi3.tokens.call(2)).should.be.equal(lmn.address); 71 | }); 72 | 73 | it('should have correct getReturn implementation', async function () { 74 | await multi.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address], [1, 1], 'Multi', '1ABC_1XYZ', 18, { from: _, gas: 8000000 }); 75 | (await multi.getReturn.call(abc.address, xyz.address, 100)).should.be.bignumber.equal(0); 76 | 77 | await abc.approve(multi.address, 1000e6); 78 | await xyz.approve(multi.address, 500e6); 79 | await multi.bundleFirstTokens(_, 1000, [1000e6, 500e6]); 80 | 81 | (await multi.getReturn.call(abc.address, lmn.address, 100)).should.be.bignumber.equal(0); 82 | (await multi.getReturn.call(lmn.address, xyz.address, 100)).should.be.bignumber.equal(0); 83 | (await multi.getReturn.call(lmn.address, lmn.address, 100)).should.be.bignumber.equal(0); 84 | (await multi.getReturn.call(abc.address, xyz.address, 100)).should.be.bignumber.not.equal(0); 85 | 86 | (await multi.getReturn.call(abc.address, abc.address, 100)).should.be.bignumber.equal(0); 87 | (await multi.getReturn.call(xyz.address, xyz.address, 100)).should.be.bignumber.equal(0); 88 | }); 89 | 90 | it('should have correct change implementation for missing and same tokens', async function () { 91 | await multi.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address], [1, 1], 'Multi', '1ABC_1XYZ', 18, { from: _, gas: 8000000 }); 92 | 93 | await abc.approve(multi.address, 1000e6); 94 | await xyz.approve(multi.address, 500e6); 95 | await multi.bundleFirstTokens(_, 1000, [1000e6, 500e6]); 96 | 97 | await multi.change(abc.address, lmn.address, 100, 0).should.be.rejectedWith(EVMRevert); 98 | await multi.change(lmn.address, xyz.address, 100, 0).should.be.rejectedWith(EVMRevert); 99 | await multi.change(lmn.address, lmn.address, 100, 0).should.be.rejectedWith(EVMRevert); 100 | 101 | await multi.change(abc.address, abc.address, 100, 0).should.be.rejectedWith(EVMRevert); 102 | await multi.change(xyz.address, xyz.address, 100, 0).should.be.rejectedWith(EVMRevert); 103 | }); 104 | }); 105 | 106 | describe('exchange 1:1', async function () { 107 | beforeEach(async function () { 108 | await multi.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address], [1, 1], 'Multi', '1ABC_1XYZ', 18, { from: _, gas: 8000000 }); 109 | await abc.approve(multi.address, 1000e6); 110 | await xyz.approve(multi.address, 500e6); 111 | await multi.bundleFirstTokens(_, 1000, [1000e6, 500e6]); 112 | }); 113 | 114 | it('should have valid prices for exchange tokens', async function () { 115 | (await multi.getReturn.call(abc.address, xyz.address, 10e6)).should.be.bignumber.equal(4950495); 116 | (await multi.getReturn.call(abc.address, xyz.address, 20e6)).should.be.bignumber.equal(9803921); 117 | (await multi.getReturn.call(abc.address, xyz.address, 30e6)).should.be.bignumber.equal(14563106); 118 | (await multi.getReturn.call(abc.address, xyz.address, 40e6)).should.be.bignumber.equal(19230769); 119 | (await multi.getReturn.call(abc.address, xyz.address, 50e6)).should.be.bignumber.equal(23809523); 120 | (await multi.getReturn.call(abc.address, xyz.address, 60e6)).should.be.bignumber.equal(28301886); 121 | (await multi.getReturn.call(abc.address, xyz.address, 70e6)).should.be.bignumber.equal(32710280); 122 | (await multi.getReturn.call(abc.address, xyz.address, 80e6)).should.be.bignumber.equal(37037037); 123 | (await multi.getReturn.call(abc.address, xyz.address, 90e6)).should.be.bignumber.equal(41284403); 124 | (await multi.getReturn.call(abc.address, xyz.address, 100e6)).should.be.bignumber.equal(45454545); 125 | 126 | (await multi.getReturn.call(xyz.address, abc.address, 10e6)).should.be.bignumber.equal(19607843); 127 | (await multi.getReturn.call(xyz.address, abc.address, 20e6)).should.be.bignumber.equal(38461538); 128 | (await multi.getReturn.call(xyz.address, abc.address, 30e6)).should.be.bignumber.equal(56603773); 129 | (await multi.getReturn.call(xyz.address, abc.address, 40e6)).should.be.bignumber.equal(74074074); 130 | (await multi.getReturn.call(xyz.address, abc.address, 50e6)).should.be.bignumber.equal(90909090); 131 | (await multi.getReturn.call(xyz.address, abc.address, 60e6)).should.be.bignumber.equal(107142857); 132 | (await multi.getReturn.call(xyz.address, abc.address, 70e6)).should.be.bignumber.equal(122807017); 133 | (await multi.getReturn.call(xyz.address, abc.address, 80e6)).should.be.bignumber.equal(137931034); 134 | (await multi.getReturn.call(xyz.address, abc.address, 90e6)).should.be.bignumber.equal(152542372); 135 | (await multi.getReturn.call(xyz.address, abc.address, 100e6)).should.be.bignumber.equal(166666666); 136 | }); 137 | 138 | it('should be able to exchange tokens 0 => 1', async function () { 139 | (await xyz.balanceOf.call(wallet1)).should.be.bignumber.equal(0); 140 | await abc.approve(multi.address, 50e6, { from: wallet1 }); 141 | await multi.change(abc.address, xyz.address, 50e6, 23809523, { from: wallet1 }); 142 | (await xyz.balanceOf.call(wallet1)).should.be.bignumber.equal(23809523); 143 | }); 144 | 145 | it('should not be able to exchange due to high min return argument', async function () { 146 | (await xyz.balanceOf.call(wallet1)).should.be.bignumber.equal(0); 147 | await abc.approve(multi.address, 50e6, { from: wallet1 }); 148 | await multi.change(abc.address, xyz.address, 50e6, 23809523 + 1, { from: wallet1 }).should.be.rejectedWith(EVMRevert); 149 | }); 150 | 151 | it('should be able to exchange tokens 1 => 0', async function () { 152 | (await abc.balanceOf.call(wallet2)).should.be.bignumber.equal(0); 153 | await xyz.approve(multi.address, 50e6, { from: wallet2 }); 154 | await multi.change(xyz.address, abc.address, 50e6, 90909090, { from: wallet2 }); 155 | (await abc.balanceOf.call(wallet2)).should.be.bignumber.equal(90909090); 156 | }); 157 | 158 | it('should be able to buy tokens and sell back', async function () { 159 | (await xyz.balanceOf.call(wallet1)).should.be.bignumber.equal(0); 160 | await abc.approve(multi.address, 50e6, { from: wallet1 }); 161 | await multi.change(abc.address, xyz.address, 50e6, 23809523, { from: wallet1 }); 162 | 163 | (await abc.balanceOf.call(wallet1)).should.be.bignumber.equal(0); 164 | (await xyz.balanceOf.call(wallet1)).should.be.bignumber.equal(23809523); 165 | 166 | await xyz.approve(multi.address, 23809523, { from: wallet1 }); 167 | await multi.change(xyz.address, abc.address, 23809523, 49999998, { from: wallet1 }); 168 | 169 | (await abc.balanceOf.call(wallet1)).should.be.bignumber.equal(49999998); 170 | }); 171 | }); 172 | 173 | describe('exchange 2:1', async function () { 174 | beforeEach(async function () { 175 | await multi.contract.init['address[],uint256[],string,string,uint8']([abc.address, xyz.address], [2, 1], 'Multi', '2ABC_1XYZ', 18, { from: _, gas: 8000000 }); 176 | await abc.approve(multi.address, 1000e6); 177 | await xyz.approve(multi.address, 500e6); 178 | await multi.bundleFirstTokens(_, 1000, [1000e6, 500e6]); 179 | }); 180 | 181 | it('should have valid prices for exchange tokens', async function () { 182 | (await multi.getReturn.call(abc.address, xyz.address, 10e6)).should.be.bignumber.equal(9803921); 183 | (await multi.getReturn.call(abc.address, xyz.address, 20e6)).should.be.bignumber.equal(19230769); 184 | (await multi.getReturn.call(abc.address, xyz.address, 30e6)).should.be.bignumber.equal(28301886); 185 | (await multi.getReturn.call(abc.address, xyz.address, 40e6)).should.be.bignumber.equal(37037037); 186 | (await multi.getReturn.call(abc.address, xyz.address, 50e6)).should.be.bignumber.equal(45454545); 187 | (await multi.getReturn.call(abc.address, xyz.address, 60e6)).should.be.bignumber.equal(53571428); 188 | (await multi.getReturn.call(abc.address, xyz.address, 70e6)).should.be.bignumber.equal(61403508); 189 | (await multi.getReturn.call(abc.address, xyz.address, 80e6)).should.be.bignumber.equal(68965517); 190 | (await multi.getReturn.call(abc.address, xyz.address, 90e6)).should.be.bignumber.equal(76271186); 191 | (await multi.getReturn.call(abc.address, xyz.address, 100e6)).should.be.bignumber.equal(83333333); 192 | 193 | (await multi.getReturn.call(xyz.address, abc.address, 10e6)).should.be.bignumber.equal(9803921); 194 | (await multi.getReturn.call(xyz.address, abc.address, 20e6)).should.be.bignumber.equal(19230769); 195 | (await multi.getReturn.call(xyz.address, abc.address, 30e6)).should.be.bignumber.equal(28301886); 196 | (await multi.getReturn.call(xyz.address, abc.address, 40e6)).should.be.bignumber.equal(37037037); 197 | (await multi.getReturn.call(xyz.address, abc.address, 50e6)).should.be.bignumber.equal(45454545); 198 | (await multi.getReturn.call(xyz.address, abc.address, 60e6)).should.be.bignumber.equal(53571428); 199 | (await multi.getReturn.call(xyz.address, abc.address, 70e6)).should.be.bignumber.equal(61403508); 200 | (await multi.getReturn.call(xyz.address, abc.address, 80e6)).should.be.bignumber.equal(68965517); 201 | (await multi.getReturn.call(xyz.address, abc.address, 90e6)).should.be.bignumber.equal(76271186); 202 | (await multi.getReturn.call(xyz.address, abc.address, 100e6)).should.be.bignumber.equal(83333333); 203 | }); 204 | }); 205 | }); 206 | -------------------------------------------------------------------------------- /test/MultiTokenInfo.js: -------------------------------------------------------------------------------- 1 | require('chai') 2 | .use(require('chai-as-promised')) 3 | .use(require('chai-bignumber')(web3.BigNumber)) 4 | .should(); 5 | 6 | const Token = artifacts.require('Token.sol'); 7 | const BadToken = artifacts.require('BadToken.sol'); 8 | const BasicMultiToken = artifacts.require('BasicMultiToken.sol'); 9 | const MultiTokenInfo = artifacts.require('MultiTokenInfo.sol'); 10 | 11 | contract('MultiTokenInfo', function ([_, wallet1, wallet2, wallet3, wallet4, wallet5]) { 12 | let abc; 13 | let xyz; 14 | let lmn; 15 | let info; 16 | 17 | before(async function () { 18 | info = await MultiTokenInfo.new(); 19 | }); 20 | 21 | beforeEach(async function () { 22 | abc = await Token.new('ABC'); 23 | await abc.mint(_, 1000e6); 24 | await abc.mint(wallet1, 50e6); 25 | await abc.mint(wallet2, 50e6); 26 | 27 | xyz = await BadToken.new('BadToken', 'XYZ', 18); 28 | await xyz.mint(_, 500e6); 29 | await xyz.mint(wallet1, 50e6); 30 | await xyz.mint(wallet2, 50e6); 31 | 32 | lmn = await Token.new('LMN'); 33 | await lmn.mint(_, 100e6); 34 | }); 35 | 36 | it('should provide working method allTokens', async function () { 37 | const multi = await BasicMultiToken.new(); 38 | await multi.init([abc.address, xyz.address], 'Multi', '1ABC_1XYZ', 18); 39 | (await info.allTokens.call(multi.address)).should.be.deep.equal([ 40 | abc.address, 41 | xyz.address, 42 | ]); 43 | 44 | const multi2 = await BasicMultiToken.new(); 45 | await multi2.init([abc.address, xyz.address, lmn.address], 'Multi', '1ABC_1XYZ_1LMN', 18); 46 | (await info.allTokens.call(multi2.address)).should.be.deep.equal([ 47 | abc.address, 48 | xyz.address, 49 | lmn.address, 50 | ]); 51 | }); 52 | 53 | it('should provide working method allNames', async function () { 54 | const multi = await BasicMultiToken.new(); 55 | await multi.init([abc.address, xyz.address], 'Multi', '1ABC_1XYZ', 18); 56 | (await info.allNames.call(multi.address)).map(web3.toUtf8).should.be.deep.equal([ 57 | 'Token', 58 | 'BadToken', 59 | ]); 60 | 61 | const multi2 = await BasicMultiToken.new(); 62 | await multi2.init([abc.address, xyz.address, lmn.address], 'Multi', '1ABC_1XYZ_1LMN', 18); 63 | (await info.allNames.call(multi2.address)).map(web3.toUtf8).should.be.deep.equal([ 64 | 'Token', 65 | 'BadToken', 66 | 'Token', 67 | ]); 68 | }); 69 | 70 | it('should provide working method allSymbols', async function () { 71 | const multi = await BasicMultiToken.new(); 72 | await multi.init([abc.address, xyz.address], 'Multi', '1ABC_1XYZ', 18); 73 | (await info.allSymbols.call(multi.address)).map(web3.toUtf8).should.be.deep.equal([ 74 | 'ABC', 75 | 'XYZ', 76 | ]); 77 | 78 | const multi2 = await BasicMultiToken.new(); 79 | await multi2.init([abc.address, xyz.address, lmn.address], 'Multi', '1ABC_1XYZ_1LMN', 18); 80 | (await info.allSymbols.call(multi2.address)).map(web3.toUtf8).should.be.deep.equal([ 81 | 'ABC', 82 | 'XYZ', 83 | 'LMN', 84 | ]); 85 | }); 86 | 87 | it('should provide working method allBalances', async function () { 88 | const multi = await BasicMultiToken.new(); 89 | await multi.init([abc.address, xyz.address], 'Multi', '1ABC_1XYZ', 18); 90 | await abc.approve(multi.address, 1000e6); 91 | await xyz.approve(multi.address, 500e6); 92 | await multi.bundleFirstTokens(_, 1000, [1000e6, 500e6]); 93 | 94 | (await info.allBalances.call(multi.address)).map(bn => bn.toString()).should.be.deep.equal([ 95 | '1000000000', 96 | '500000000', 97 | ]); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/helpers/EVMRevert.js: -------------------------------------------------------------------------------- 1 | const EVMRevert = 'revert'; 2 | 3 | module.exports = { 4 | EVMRevert, 5 | }; 6 | -------------------------------------------------------------------------------- /test/helpers/EVMThrow.js: -------------------------------------------------------------------------------- 1 | const EVMThrow = 'invalid opcode'; 2 | 3 | module.exports = { 4 | EVMThrow, 5 | }; 6 | -------------------------------------------------------------------------------- /test/helpers/advanceToBlock.js: -------------------------------------------------------------------------------- 1 | function advanceBlock () { 2 | return new Promise((resolve, reject) => { 3 | web3.currentProvider.sendAsync({ 4 | jsonrpc: '2.0', 5 | method: 'evm_mine', 6 | id: Date.now(), 7 | }, (err, res) => { 8 | return err ? reject(err) : resolve(res); 9 | }); 10 | }); 11 | } 12 | 13 | // Advances the block number so that the last mined block is `number`. 14 | async function advanceToBlock (number) { 15 | if (web3.eth.blockNumber > number) { 16 | throw Error(`block number ${number} is in the past (current is ${web3.eth.blockNumber})`); 17 | } 18 | 19 | while (web3.eth.blockNumber < number) { 20 | await advanceBlock(); 21 | } 22 | } 23 | 24 | module.exports = { 25 | advanceBlock, 26 | advanceToBlock, 27 | }; 28 | -------------------------------------------------------------------------------- /test/helpers/assertJump.js: -------------------------------------------------------------------------------- 1 | const should = require('chai') 2 | .should(); 3 | 4 | async function assertJump (promise) { 5 | try { 6 | await promise; 7 | should.fail('Expected invalid opcode not received'); 8 | } catch (error) { 9 | error.message.should.include('invalid opcode', `Expected "invalid opcode", got ${error} instead`); 10 | } 11 | } 12 | 13 | module.exports = { 14 | assertJump, 15 | }; 16 | -------------------------------------------------------------------------------- /test/helpers/assertRevert.js: -------------------------------------------------------------------------------- 1 | const should = require('chai') 2 | .should(); 3 | 4 | async function assertRevert (foo) { 5 | try { 6 | foo(); 7 | } catch (error) { 8 | error.message.should.include('revert', `Expected "revert", got ${error} instead`); 9 | return; 10 | } 11 | should.fail('Expected revert not received'); 12 | } 13 | 14 | module.exports = { 15 | assertRevert, 16 | }; 17 | -------------------------------------------------------------------------------- /test/helpers/ether.js: -------------------------------------------------------------------------------- 1 | function ether (n) { 2 | return new web3.BigNumber(web3.toWei(n, 'ether')); 3 | } 4 | 5 | module.exports = { 6 | ether, 7 | }; 8 | -------------------------------------------------------------------------------- /test/helpers/expectEvent.js: -------------------------------------------------------------------------------- 1 | const should = require('chai').should(); 2 | 3 | function inLogs (logs, eventName, eventArgs = {}) { 4 | const event = logs.find(function (e) { 5 | if (e.event === eventName) { 6 | let matches = true; 7 | 8 | for (const [k, v] of Object.entries(eventArgs)) { 9 | if (e.args[k] !== v) { 10 | matches = false; 11 | } 12 | } 13 | 14 | if (matches) { 15 | return true; 16 | } 17 | } 18 | }); 19 | 20 | should.exist(event); 21 | 22 | return event; 23 | } 24 | 25 | async function inTransaction (tx, eventName, eventArgs = {}) { 26 | const { logs } = await tx; 27 | return inLogs(logs, eventName, eventArgs); 28 | } 29 | 30 | module.exports = { 31 | inLogs, 32 | inTransaction, 33 | }; 34 | -------------------------------------------------------------------------------- /test/helpers/expectThrow.js: -------------------------------------------------------------------------------- 1 | const should = require('chai') 2 | .should(); 3 | 4 | async function expectThrow (promise, message) { 5 | try { 6 | await promise; 7 | } catch (error) { 8 | // Message is an optional parameter here 9 | if (message) { 10 | error.message.should.include(message, 'Expected \'' + message + '\', got \'' + error + '\' instead'); 11 | return; 12 | } else { 13 | // TODO: Check jump destination to destinguish between a throw 14 | // and an actual invalid jump. 15 | // TODO: When we contract A calls contract B, and B throws, instead 16 | // of an 'invalid jump', we get an 'out of gas' error. How do 17 | // we distinguish this from an actual out of gas event? (The 18 | // ganache log actually show an 'invalid jump' event.) 19 | error.message.should.match(/[invalid opcode|out of gas|revert]/, 'Expected throw, got \'' + error + '\' instead'); 20 | return; 21 | } 22 | } 23 | should.fail('Expected throw not received'); 24 | } 25 | 26 | module.exports = { 27 | expectThrow, 28 | }; 29 | -------------------------------------------------------------------------------- /test/helpers/increaseTime.js: -------------------------------------------------------------------------------- 1 | const { latestTime } = require('./latestTime'); 2 | 3 | // Increases ganache time by the passed duration in seconds 4 | function increaseTime (duration) { 5 | const id = Date.now(); 6 | 7 | return new Promise((resolve, reject) => { 8 | web3.currentProvider.sendAsync({ 9 | jsonrpc: '2.0', 10 | method: 'evm_increaseTime', 11 | params: [duration], 12 | id: id, 13 | }, err1 => { 14 | if (err1) return reject(err1); 15 | 16 | web3.currentProvider.sendAsync({ 17 | jsonrpc: '2.0', 18 | method: 'evm_mine', 19 | id: id + 1, 20 | }, (err2, res) => { 21 | return err2 ? reject(err2) : resolve(res); 22 | }); 23 | }); 24 | }); 25 | } 26 | 27 | /** 28 | * Beware that due to the need of calling two separate ganache methods and rpc calls overhead 29 | * it's hard to increase time precisely to a target point so design your test to tolerate 30 | * small fluctuations from time to time. 31 | * 32 | * @param target time in seconds 33 | */ 34 | async function increaseTimeTo (target) { 35 | const now = (await latestTime()); 36 | 37 | if (target < now) throw Error(`Cannot increase current time(${now}) to a moment in the past(${target})`); 38 | const diff = target - now; 39 | return increaseTime(diff); 40 | } 41 | 42 | const duration = { 43 | seconds: function (val) { return val; }, 44 | minutes: function (val) { return val * this.seconds(60); }, 45 | hours: function (val) { return val * this.minutes(60); }, 46 | days: function (val) { return val * this.hours(24); }, 47 | weeks: function (val) { return val * this.days(7); }, 48 | years: function (val) { return val * this.days(365); }, 49 | }; 50 | 51 | module.exports = { 52 | increaseTime, 53 | increaseTimeTo, 54 | duration, 55 | }; 56 | -------------------------------------------------------------------------------- /test/helpers/latestTime.js: -------------------------------------------------------------------------------- 1 | const { ethGetBlock } = require('./web3'); 2 | 3 | // Returns the time of the last mined block in seconds 4 | async function latestTime () { 5 | const block = await ethGetBlock('latest'); 6 | return block.timestamp; 7 | } 8 | 9 | module.exports = { 10 | latestTime, 11 | }; 12 | -------------------------------------------------------------------------------- /test/helpers/makeInterfaceId.js: -------------------------------------------------------------------------------- 1 | const { soliditySha3 } = require('web3-utils'); 2 | 3 | const INTERFACE_ID_LENGTH = 4; 4 | 5 | function makeInterfaceId (interfaces = []) { 6 | const interfaceIdBuffer = interfaces 7 | .map(methodSignature => soliditySha3(methodSignature)) // keccak256 8 | .map(h => 9 | Buffer 10 | .from(h.substring(2), 'hex') 11 | .slice(0, 4) // bytes4() 12 | ) 13 | .reduce((memo, bytes) => { 14 | for (let i = 0; i < INTERFACE_ID_LENGTH; i++) { 15 | memo[i] = memo[i] ^ bytes[i]; // xor 16 | } 17 | return memo; 18 | }, Buffer.alloc(INTERFACE_ID_LENGTH)); 19 | 20 | return `0x${interfaceIdBuffer.toString('hex')}`; 21 | } 22 | 23 | module.exports = { 24 | makeInterfaceId, 25 | }; 26 | -------------------------------------------------------------------------------- /test/helpers/merkleTree.js: -------------------------------------------------------------------------------- 1 | const { sha3, bufferToHex } = require('ethereumjs-util'); 2 | 3 | class MerkleTree { 4 | constructor (elements) { 5 | // Filter empty strings and hash elements 6 | this.elements = elements.filter(el => el).map(el => sha3(el)); 7 | 8 | // Deduplicate elements 9 | this.elements = this.bufDedup(this.elements); 10 | // Sort elements 11 | this.elements.sort(Buffer.compare); 12 | 13 | // Create layers 14 | this.layers = this.getLayers(this.elements); 15 | } 16 | 17 | getLayers (elements) { 18 | if (elements.length === 0) { 19 | return [['']]; 20 | } 21 | 22 | const layers = []; 23 | layers.push(elements); 24 | 25 | // Get next layer until we reach the root 26 | while (layers[layers.length - 1].length > 1) { 27 | layers.push(this.getNextLayer(layers[layers.length - 1])); 28 | } 29 | 30 | return layers; 31 | } 32 | 33 | getNextLayer (elements) { 34 | return elements.reduce((layer, el, idx, arr) => { 35 | if (idx % 2 === 0) { 36 | // Hash the current element with its pair element 37 | layer.push(this.combinedHash(el, arr[idx + 1])); 38 | } 39 | 40 | return layer; 41 | }, []); 42 | } 43 | 44 | combinedHash (first, second) { 45 | if (!first) { return second; } 46 | if (!second) { return first; } 47 | 48 | return sha3(this.sortAndConcat(first, second)); 49 | } 50 | 51 | getRoot () { 52 | return this.layers[this.layers.length - 1][0]; 53 | } 54 | 55 | getHexRoot () { 56 | return bufferToHex(this.getRoot()); 57 | } 58 | 59 | getProof (el) { 60 | let idx = this.bufIndexOf(el, this.elements); 61 | 62 | if (idx === -1) { 63 | throw new Error('Element does not exist in Merkle tree'); 64 | } 65 | 66 | return this.layers.reduce((proof, layer) => { 67 | const pairElement = this.getPairElement(idx, layer); 68 | 69 | if (pairElement) { 70 | proof.push(pairElement); 71 | } 72 | 73 | idx = Math.floor(idx / 2); 74 | 75 | return proof; 76 | }, []); 77 | } 78 | 79 | getHexProof (el) { 80 | const proof = this.getProof(el); 81 | 82 | return this.bufArrToHexArr(proof); 83 | } 84 | 85 | getPairElement (idx, layer) { 86 | const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1; 87 | 88 | if (pairIdx < layer.length) { 89 | return layer[pairIdx]; 90 | } else { 91 | return null; 92 | } 93 | } 94 | 95 | bufIndexOf (el, arr) { 96 | let hash; 97 | 98 | // Convert element to 32 byte hash if it is not one already 99 | if (el.length !== 32 || !Buffer.isBuffer(el)) { 100 | hash = sha3(el); 101 | } else { 102 | hash = el; 103 | } 104 | 105 | for (let i = 0; i < arr.length; i++) { 106 | if (hash.equals(arr[i])) { 107 | return i; 108 | } 109 | } 110 | 111 | return -1; 112 | } 113 | 114 | bufDedup (elements) { 115 | return elements.filter((el, idx) => { 116 | return this.bufIndexOf(el, elements) === idx; 117 | }); 118 | } 119 | 120 | bufArrToHexArr (arr) { 121 | if (arr.some(el => !Buffer.isBuffer(el))) { 122 | throw new Error('Array is not an array of buffers'); 123 | } 124 | 125 | return arr.map(el => '0x' + el.toString('hex')); 126 | } 127 | 128 | sortAndConcat (...args) { 129 | return Buffer.concat([...args].sort(Buffer.compare)); 130 | } 131 | } 132 | 133 | module.exports = { 134 | MerkleTree, 135 | }; 136 | -------------------------------------------------------------------------------- /test/helpers/sendTransaction.js: -------------------------------------------------------------------------------- 1 | const ethjsABI = require('ethjs-abi'); 2 | 3 | function findMethod (abi, name, args) { 4 | for (let i = 0; i < abi.length; i++) { 5 | const methodArgs = abi[i].inputs.map(input => input.type).join(','); 6 | if ((abi[i].name === name) && (methodArgs === args)) { 7 | return abi[i]; 8 | } 9 | } 10 | } 11 | 12 | function sendTransaction (target, name, argsTypes, argsValues, opts) { 13 | const abiMethod = findMethod(target.abi, name, argsTypes); 14 | const encodedData = ethjsABI.encodeMethod(abiMethod, argsValues); 15 | return target.sendTransaction(Object.assign({ data: encodedData }, opts)); 16 | } 17 | 18 | module.exports = { 19 | findMethod, 20 | sendTransaction, 21 | }; 22 | -------------------------------------------------------------------------------- /test/helpers/sign.js: -------------------------------------------------------------------------------- 1 | const { sha3, soliditySha3 } = require('web3-utils'); 2 | 3 | const REAL_SIGNATURE_SIZE = 2 * 65; // 65 bytes in hexadecimal string legnth 4 | const PADDED_SIGNATURE_SIZE = 2 * 96; // 96 bytes in hexadecimal string length 5 | 6 | const DUMMY_SIGNATURE = `0x${web3.padLeft('', REAL_SIGNATURE_SIZE)}`; 7 | 8 | // messageHex = '0xdeadbeef' 9 | function toEthSignedMessageHash (messageHex) { 10 | const messageBuffer = Buffer.from(messageHex.substring(2), 'hex'); 11 | const prefix = Buffer.from(`\u0019Ethereum Signed Message:\n${messageBuffer.length}`); 12 | return sha3(Buffer.concat([prefix, messageBuffer])); 13 | } 14 | 15 | // signs message in node (ganache auto-applies "Ethereum Signed Message" prefix) 16 | // messageHex = '0xdeadbeef' 17 | const signMessage = (signer, messageHex = '0x') => { 18 | return web3.eth.sign(signer, messageHex); // actually personal_sign 19 | }; 20 | 21 | // @TODO - remove this when we migrate to web3-1.0.0 22 | const transformToFullName = function (json) { 23 | if (json.name.indexOf('(') !== -1) { 24 | return json.name; 25 | } 26 | 27 | const typeName = json.inputs.map(function (i) { return i.type; }).join(); 28 | return json.name + '(' + typeName + ')'; 29 | }; 30 | 31 | /** 32 | * Create a signer between a contract and a signer for a voucher of method, args, and redeemer 33 | * Note that `method` is the web3 method, not the truffle-contract method 34 | * Well truffle is terrible, but luckily (?) so is web3 < 1.0, so we get to make our own method id 35 | * fetcher because the method on the contract isn't actually the SolidityFunction object ಠ_ಠ 36 | * @param contract TruffleContract 37 | * @param signer address 38 | * @param redeemer address 39 | * @param methodName string 40 | * @param methodArgs any[] 41 | */ 42 | const getSignFor = (contract, signer) => (redeemer, methodName, methodArgs = []) => { 43 | const parts = [ 44 | contract.address, 45 | redeemer, 46 | ]; 47 | 48 | // if we have a method, add it to the parts that we're signing 49 | if (methodName) { 50 | if (methodArgs.length > 0) { 51 | parts.push( 52 | contract.contract[methodName].getData(...methodArgs.concat([DUMMY_SIGNATURE])).slice( 53 | 0, 54 | -1 * PADDED_SIGNATURE_SIZE 55 | ) 56 | ); 57 | } else { 58 | const abi = contract.abi.find(abi => abi.name === methodName); 59 | const name = transformToFullName(abi); 60 | const signature = sha3(name).slice(0, 10); 61 | parts.push(signature); 62 | } 63 | } 64 | 65 | // return the signature of the "Ethereum Signed Message" hash of the hash of `parts` 66 | const messageHex = soliditySha3(...parts); 67 | return signMessage(signer, messageHex); 68 | }; 69 | 70 | module.exports = { 71 | signMessage, 72 | toEthSignedMessageHash, 73 | getSignFor, 74 | }; 75 | -------------------------------------------------------------------------------- /test/helpers/transactionMined.js: -------------------------------------------------------------------------------- 1 | // From https://gist.github.com/xavierlepretre/88682e871f4ad07be4534ae560692ee6 2 | function transactionMined (txnHash, interval) { 3 | interval = interval || 500; 4 | const transactionReceiptAsync = function (txnHash, resolve, reject) { 5 | try { 6 | const receipt = web3.eth.getTransactionReceipt(txnHash); 7 | if (receipt === null) { 8 | setTimeout(function () { 9 | transactionReceiptAsync(txnHash, resolve, reject); 10 | }, interval); 11 | } else { 12 | resolve(receipt); 13 | } 14 | } catch (e) { 15 | reject(e); 16 | } 17 | }; 18 | 19 | if (Array.isArray(txnHash)) { 20 | return Promise.all(txnHash.map(hash => 21 | web3.eth.getTransactionReceiptMined(hash, interval) 22 | )); 23 | } else { 24 | return new Promise(function (resolve, reject) { 25 | transactionReceiptAsync(txnHash, resolve, reject); 26 | }); 27 | } 28 | } 29 | 30 | web3.eth.transactionMined = transactionMined; 31 | 32 | module.exports = { 33 | transactionMined, 34 | }; 35 | -------------------------------------------------------------------------------- /test/helpers/web3.js: -------------------------------------------------------------------------------- 1 | const pify = require('pify'); 2 | 3 | const ethAsync = pify(web3.eth); 4 | 5 | module.exports = { 6 | ethGetBalance: ethAsync.getBalance, 7 | ethSendTransaction: ethAsync.sendTransaction, 8 | ethGetBlock: ethAsync.getBlock, 9 | }; 10 | -------------------------------------------------------------------------------- /test/impl/BadToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 4 | 5 | 6 | contract BadToken { 7 | using SafeMath for uint256; 8 | 9 | bytes32 public name; // [!] bytes32 instead of string 10 | bytes32 public symbol; // [!] bytes32 instead of string 11 | uint256 public decimals; // [!] uint256 instead of uint8 12 | 13 | address public owner; 14 | bool public paused; 15 | uint256 public totalSupply; 16 | mapping(address => uint256) public balanceOf; 17 | mapping(address => mapping(address => uint256)) public allowance; 18 | 19 | event Transfer(address indexed from, address indexed to, uint256 value); 20 | event Approval(address indexed owner, address indexed spender, uint256 value); 21 | 22 | modifier onlyOwner { 23 | require(msg.sender == owner); 24 | _; 25 | } 26 | 27 | modifier notPaused { 28 | require(!paused); 29 | _; 30 | } 31 | 32 | constructor(bytes32 _name, bytes32 _symbol, uint256 _decimals) public { 33 | owner = msg.sender; 34 | name = _name; 35 | symbol = _symbol; 36 | decimals = _decimals; 37 | } 38 | 39 | function mint(address _to, uint256 _value) public onlyOwner { 40 | balanceOf[_to] = balanceOf[_to].add(_value); 41 | emit Transfer(address(0), _to, _value); 42 | } 43 | 44 | function pause() public onlyOwner { 45 | paused = true; 46 | } 47 | 48 | function unpause() public onlyOwner { 49 | paused = false; 50 | } 51 | 52 | // [!] Returns void instead of bool 53 | function transfer(address _to, uint256 _value) public notPaused { 54 | _transfer(msg.sender, _to, _value); 55 | } 56 | 57 | // [!] Returns void instead of bool 58 | function transferFrom(address _from, address _to, uint256 _value) public notPaused { 59 | allowance[_from][msg.sender] = allowance[_from][msg.sender].sub(_value); 60 | _transfer(_from, _to, _value); 61 | } 62 | 63 | // [!] Returns void instead of bool 64 | function approve(address _spender, uint256 _value) public notPaused { 65 | require((allowance[msg.sender][_spender] == 0) || (_value == 0)); 66 | allowance[msg.sender][_spender] = _value; 67 | emit Approval(msg.sender, _spender, _value); 68 | } 69 | 70 | function _transfer(address _from, address _to, uint256 _value) internal { 71 | balanceOf[_from] = balanceOf[_from].sub(_value); 72 | balanceOf[_to] = balanceOf[_to].add(_value); 73 | emit Transfer(_from, _to, _value); 74 | } 75 | } -------------------------------------------------------------------------------- /test/impl/BrokenTransferFromToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/PausableToken.sol"; 5 | import "openzeppelin-solidity/contracts/token/ERC20/DetailedERC20.sol"; 6 | 7 | 8 | contract BrokenTransferFromToken is MintableToken, PausableToken, DetailedERC20 { 9 | 10 | constructor(string _symbol) public 11 | DetailedERC20("BrokenTransferFromToken", _symbol, 18) 12 | { 13 | } 14 | 15 | function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { 16 | super.transferFrom(_from, _to, _value.mul(80).div(100)); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /test/impl/BrokenTransferToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/PausableToken.sol"; 5 | import "openzeppelin-solidity/contracts/token/ERC20/DetailedERC20.sol"; 6 | 7 | 8 | contract BrokenTransferToken is MintableToken, PausableToken, DetailedERC20 { 9 | 10 | constructor(string _symbol) public 11 | DetailedERC20("BrokenTransferToken", _symbol, 18) 12 | { 13 | } 14 | 15 | function transfer(address _to, uint256 _value) public returns (bool) { 16 | super.transfer(_to, _value.mul(80).div(100)); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /test/impl/Token.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/PausableToken.sol"; 5 | import "openzeppelin-solidity/contracts/token/ERC20/DetailedERC20.sol"; 6 | 7 | 8 | contract Token is MintableToken, PausableToken, DetailedERC20 { 9 | constructor(string _symbol) public 10 | DetailedERC20("Token", _symbol, 18) 11 | { 12 | } 13 | } -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | development: { 4 | host: "localhost", 5 | port: 9545, 6 | network_id: "*", 7 | gas: 8000000 8 | }, 9 | coverage: { 10 | host: "localhost", 11 | port: 8555, 12 | network_id: "*", 13 | gas: 0xfffffffffff 14 | }, 15 | profiler: { 16 | host: "localhost", 17 | port: 8555, 18 | network_id: "*", 19 | gas: 0xfffffffffff 20 | } 21 | }, 22 | solc: { 23 | optimizer: { 24 | enabled: true, 25 | runs: 200 26 | } 27 | } 28 | }; 29 | --------------------------------------------------------------------------------