├── .soliumignore ├── CODEOWNERS ├── .gitattributes ├── test ├── constants.js ├── helpers │ └── assertRevert.js ├── utils.js ├── TestBsktToken.sol ├── BsktToken.test.js └── E2E.test.js ├── .gitmodules ├── config-examples ├── secrets.ci.json ├── secrets.example.json └── deploy-config.example.js ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── .soliumrc.json ├── .solcover.js ├── contracts ├── TokenA.sol ├── TokenB.sol ├── TokenC.sol ├── Migrations.sol ├── BsktToken.sol └── MultiSigWallet.sol ├── scripts ├── ganache-cli.sh ├── generate-arguments.js ├── generate-bytecode.js └── compile-contract.js ├── .circleci └── config.yml ├── .gitignore ├── truffle.js ├── README.md └── package.json /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @dmdque @drixta 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /test/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ETW_DECIMALS: 18 3 | }; 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "underlying-token-contracts"] 2 | path = underlying-token-contracts 3 | url = git@github.com:cryptofinlabs/underlying-token-contracts.git 4 | -------------------------------------------------------------------------------- /config-examples/secrets.ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "deploy": { 3 | "coinbase": { 4 | "address": "0x8bbe02653aac65ed51bed07dc3f23136d6de7b2c" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config-examples/secrets.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "deploy": { 3 | "coinbase": { 4 | "address": "" 5 | } 6 | }, 7 | "mnemonic": "", 8 | "infura_token": "" 9 | } 10 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer, network) { 4 | if (network == "development") { 5 | deployer.deploy(Migrations); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /test/helpers/assertRevert.js: -------------------------------------------------------------------------------- 1 | // From zeppelin-solidity@v1.5.0:test/helpers/assertRevert.js 2 | module.exports = function (error) { 3 | assert.isAbove(error.message.search('revert'), -1, 'Error containing "revert" must be returned'); 4 | }; 5 | -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:recommended", 3 | "plugins": [ 4 | "security" 5 | ], 6 | "rules": { 7 | "quotes": [ 8 | "error", 9 | "double" 10 | ], 11 | "indentation": [ 12 | "error", 13 | 4 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const {ETW_DECIMALS} = require('./constants'); 2 | /** 3 | * Convert 1 Bskt value to base unit, similar to converting ETH to Wei 4 | */ 5 | function toBU(amount) { 6 | return amount * (10 ** ETW_DECIMALS); 7 | } 8 | 9 | module.exports = { 10 | toBU 11 | }; 12 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | accounts: 10, 3 | norpc: true, 4 | testCommand: "truffle test --network coverage ./test/BsktToken.test.js", 5 | copyPackages: ["zeppelin-solidity"], 6 | skipFiles: ["Migrations.sol", "TokenA.sol", "TokenB.sol", "TokenC.sol", "MultiSigWallet.sol"] 7 | }; 8 | -------------------------------------------------------------------------------- /contracts/TokenA.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.21; 2 | 3 | import "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; 4 | 5 | contract TokenA is StandardToken { 6 | string public name = "TokenA"; 7 | string public symbol = "TA"; 8 | uint8 public decimals = 2; 9 | uint constant public INITIAL_SUPPLY = 13000; 10 | 11 | function TokenA() public { 12 | totalSupply_ = INITIAL_SUPPLY; 13 | balances[msg.sender] = INITIAL_SUPPLY; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/TokenB.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.21; 2 | 3 | import "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; 4 | 5 | contract TokenB is StandardToken { 6 | string public name = "TokenB"; 7 | string public symbol = "TB"; 8 | uint8 public decimals = 2; 9 | uint constant public INITIAL_SUPPLY = 13000; 10 | 11 | function TokenB() public { 12 | totalSupply_ = INITIAL_SUPPLY; 13 | balances[msg.sender] = INITIAL_SUPPLY; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/TokenC.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.21; 2 | 3 | import "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol"; 4 | 5 | contract TokenC is StandardToken { 6 | string public name = "TokenC"; 7 | string public symbol = "TC"; 8 | uint8 public decimals = 2; 9 | uint constant public INITIAL_SUPPLY = 13000; 10 | 11 | function TokenC() public { 12 | totalSupply_ = INITIAL_SUPPLY; 13 | balances[msg.sender] = INITIAL_SUPPLY; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /config-examples/deploy-config.example.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "mainnet": { 3 | "tokenAddresses": [], 4 | "tokenQuantities": [], 5 | "creationUnit": "", 6 | "name": "", 7 | "symbol": "", 8 | "multisig": { 9 | "owners": [], 10 | "required": 3 11 | }, 12 | }, 13 | "development": { 14 | "tokenAddresses": [], 15 | "tokenQuantities": [], 16 | "creationUnit": "", 17 | "name": "", 18 | "symbol": "", 19 | "multisig": { 20 | "owners": [], 21 | "required": 3 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.17; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() public { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/ganache-cli.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Exit script as soon as a command fails. 4 | set -o errexit 5 | 6 | if [ "$SOLIDITY_COVERAGE" = true ]; then 7 | testrpc_port=8555 8 | else 9 | testrpc_port=8545 10 | fi 11 | 12 | testrpc_running() { 13 | nc -z localhost "$testrpc_port" 14 | } 15 | 16 | start_testrpc() { 17 | if [ "$SOLIDITY_COVERAGE" = true ]; then 18 | node_modules/.bin/testrpc-sc -e 10000 -i 16 --gasLimit 0xfffffffffff --port "$testrpc_port" > /dev/null & 19 | else 20 | node_modules/.bin/ganache-cli -e 10000 -i 15 --gasLimit 50000000 > /dev/null & 21 | fi 22 | 23 | testrpc_pid=$! 24 | } 25 | 26 | if testrpc_running; then 27 | echo "Using existing testrpc instance at port $testrpc_port" 28 | else 29 | echo "Starting our own testrpc instance at port $testrpc_port" 30 | start_testrpc 31 | fi 32 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/node:7.10 10 | 11 | working_directory: ~/repo 12 | 13 | steps: 14 | - checkout 15 | - run: mkdir -p config && mv config-examples/secrets.ci.json config/secrets.json 16 | 17 | # Download and cache dependencies 18 | - restore_cache: 19 | keys: 20 | - v1-dependencies-{{ checksum "package.json" }} 21 | # fallback to using the latest cache if no exact match is found 22 | - v1-dependencies- 23 | 24 | - run: 25 | name: bootstrap 26 | command: npm run bootstrap 27 | 28 | - save_cache: 29 | paths: 30 | - node_modules 31 | key: v1-dependencies-{{ checksum "package.json" }} 32 | 33 | # run tests! 34 | - run: 35 | name: test 36 | command: npm run test:js:gas && npm run test:sol && npm run cover 37 | -------------------------------------------------------------------------------- /scripts/generate-arguments.js: -------------------------------------------------------------------------------- 1 | // Script to generate constructor argument list from config files to speed up development with Remix. 2 | const path = require('path'); 3 | 4 | if (require.main === module) { 5 | let configFile; 6 | process.argv.forEach(function (val, index) { 7 | if (val == '--network') { 8 | network = process.argv[index + 1]; 9 | } 10 | if (val == '--config') { 11 | configFile = process.argv[index + 1]; 12 | configFile = path.resolve(configFile); 13 | } 14 | }); 15 | config = require(configFile); 16 | let arglist = ''; 17 | arglist += '['; 18 | arglist += config[network].tokenAddresses.map(function(e) { 19 | return '"' + e + '"'; 20 | }) 21 | arglist += '],['; 22 | arglist += config[network].tokenQuantities.map(function(e) { 23 | return '"' + e + '"'; 24 | }) 25 | arglist += '],'; 26 | arglist += '"' + config[network].creationUnit + '"'; 27 | arglist += ','; 28 | arglist += '"' + config[network].name + '"'; 29 | arglist += ','; 30 | arglist += '"' + config[network].symbol + '"'; 31 | 32 | console.log(arglist); 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .idea 3 | 4 | ### Node template 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (https://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules/ 35 | 36 | # Typescript v1 declaration files 37 | typings/ 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional eslint cache 43 | .eslintcache 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | # Output of 'npm pack' 49 | *.tgz 50 | 51 | # Yarn 52 | .yarn-integrity 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # coverage files 58 | allFiredEvents 59 | coverage.json 60 | coverageEnv/ 61 | scTopics 62 | 63 | package-lock.json 64 | 65 | secrets.json 66 | temp/ 67 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('truffle-hdwallet-provider'); 2 | 3 | const secrets = require('./config/secrets.json') 4 | 5 | const mochaGasSettings = { 6 | reporter: 'eth-gas-reporter', 7 | reporterOptions : { 8 | currency: 'USD', 9 | gasPrice: 21 10 | } 11 | }; 12 | 13 | const mocha = process.env.GAS_REPORTER ? mochaGasSettings : {}; 14 | 15 | module.exports = { 16 | networks: { 17 | development: { 18 | host: 'localhost', 19 | port: 7545, 20 | network_id: '5777' 21 | }, 22 | ropsten: { 23 | provider: function() { 24 | return new HDWalletProvider(secrets.mnemonic, `https://ropsten.infura.io/${secrets.infura_token}`) 25 | }, 26 | network_id: 3, 27 | gasPrice: 9, 28 | gas: 4000000, 29 | from: '0x8bbe02653aac65ed51bed07dc3f23136d6de7b2c' 30 | }, 31 | rinkeby: { 32 | provider: function() { 33 | return new HDWalletProvider(secrets.mnemonic, `https://rinkeby.infura.io/${secrets.infura_token}`) 34 | }, 35 | network_id: 4 36 | }, 37 | mainnet: { 38 | provider: function() { 39 | return new HDWalletProvider(secrets.mnemonic, `https://mainnet.infura.io/${secrets.infura_token}`) 40 | }, 41 | network_id: 1, 42 | gasPrice: 4000000000, 43 | gas: 4000000, 44 | from: '0x8bbe02653aac65ed51bed07dc3f23136d6de7b2c' 45 | }, 46 | coverage: { 47 | host: 'localhost', 48 | network_id: '*', 49 | port: 8555, 50 | gas: 0xffffffffff, 51 | gasPrice: 0x01 52 | }, 53 | }, 54 | mocha 55 | }; 56 | -------------------------------------------------------------------------------- /scripts/generate-bytecode.js: -------------------------------------------------------------------------------- 1 | const Web3 = require("web3"); 2 | 3 | const config = require("../config/deploy-config.js"); 4 | const bsktInfo = require("../build/contracts/BsktToken.json"); 5 | 6 | const web3 = new Web3(); 7 | 8 | /// @notice Generates bytecode with encoded constructor arguments 9 | /// @param network {String} Network to generate bytecode for 10 | /// @return string 11 | // Generates bytecode from build folder 12 | function generateBytecode(network) { 13 | let args; 14 | if (["development", "ropsten", "mainnet"].includes(network)) { 15 | args = config[network]; 16 | } else { 17 | return; 18 | } 19 | 20 | const abi = bsktInfo.abi; 21 | const bytecode = bsktInfo.bytecode; 22 | const tokenAddresses = args.tokenAddresses; 23 | const tokenQuantities = args.tokenQuantities; 24 | const creationUnit = args.creationUnit; 25 | const bsktContract = web3.eth.contract(abi); 26 | 27 | const constructorBytecode = bsktContract.new.getData(tokenAddresses, tokenQuantities, creationUnit, {data: bytecode}); 28 | return constructorBytecode; 29 | }; 30 | 31 | module.exports = generateBytecode; 32 | 33 | // TODO: replace with more robust flags solution 34 | // Command line 35 | if (require.main === module) { 36 | let network; 37 | process.argv.forEach(function (val, index) { 38 | if (val == "--network") { 39 | network = process.argv[index + 1] 40 | } 41 | }); 42 | if (!network) { 43 | console.log("Network undefined"); 44 | return; 45 | } 46 | if (!["development", "ropsten", "mainnet",].includes(network)) { 47 | console.log("Invalid network"); 48 | return; 49 | } 50 | const bytecode = generateBytecode(network); 51 | console.log(bytecode); 52 | } 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bskt 2 | 3 | Bskt is a generic smart contract that creates decentralized token portfolios. Bskt facilitates the bundling and unbundling of a collection of ERC20 tokens in exchange for a new ERC20 token. Owners of the new token have a direct claim on the underlying tokens. Unlike traditional funds, custody is held by the smart contract. 4 | 5 | These new tokens can be created by anyone who surrenders the underlying tokens and redeemed by anyone who owns issued tokens. Bskt allows investors to diversify their exposure to tokens in the Ethereum ecosystem without adding custody risk. 6 | 7 | See the [whitepaper](https://github.com/cryptofinlabs/bskt-whitepaper) for more details. 8 | 9 | ## Usage 10 | The steps for creating and redeeming Bskts are outlined here. A dapp which abstracts most of this away will be available here. 11 | 12 | ### Create 13 | - Determine how many Bskt tokens to create 14 | - Acquire the underlying tokens tokens (usually bought on exchanges) 15 | - Call the ERC20 `approve` function for each underlying token, allowing the Bskt to access the appropriate amount of each token 16 | - Call the Bskt's create function 17 | - The Bskt contract uses the ERC20 `transferFrom` function which then mints Bskt tokens for the creator 18 | 19 | ### Redeem 20 | - Call the Bskt's redeem function 21 | - The Bskt contract burns the tokens and uses the ERC20 `transfer` function to transfer underlying tokens to the redeemer 22 | 23 | ## Documentation 24 | The main functions provided in this contract are detailed below. 25 | 26 | #### `BsktToken(address[] addresses, uint256[] quantities, uint256 _creationUnit, string _name, string _symbol)` 27 | Initializes contract with a list of ERC20 token addresses and corresponding minimum number of units required for a creation unit. 28 | 29 | #### `create(uint256 baseUnits)` 30 | Creates Bskt tokens in exchange for underlying tokens. Before calling, underlying tokens must be approved to be moved by the Bskt Token contract. The number of approved tokens required depends on baseUnits. The `baseUnits` must be a multiple of the `creationUnit`. 31 | 32 | #### `redeem(uint256 baseUnits, address[] tokensToSkip)` 33 | Redeems Bskt tokens in return for underlying tokens. The `baseUnits` must be a multiple of the `creationUnit`. 34 | 35 | ## Development 36 | 37 | ### Set up 38 | - Run: 39 | ``` 40 | # git clone 41 | npm run bootstrap 42 | ``` 43 | - Create a `config/secrets.json` file. The `config-examples/secrets.example.json` file is provided for reference. 44 | 45 | ### Test 46 | npm run test:js 47 | npm run test:sol 48 | npm run test:e2e 49 | 50 | ### Deploy (beta) 51 | - Create a `config/deploy-config.js` file. The `config-examples/deploy-config.example.js` file is provided for reference. 52 | truffle migrate --network 53 | 54 | ### Contact 55 | For questions, contact us hi@cryptofin.io 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bskt", 3 | "version": "1.0.1", 4 | "description": "Bskt is a generic smart contract that creates decentralized token portfolios. Bskt facilitates the bundling and unbundling of a collection of ERC20 tokens in exchange for a new ERC20 token.", 5 | "directories": { 6 | "example": "examples", 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "bootstrap": "rm -rf node_modules && npm cache clean --force && if [ ! -f \"config/secrets.json\" ]; then mkdir -p config && cp config-examples/secrets.example.json config/secrets.json; fi && if [ ! -f \"config/deploy-config.js\" ]; then mkdir -p config && cp config-examples/deploy-config.example.js config/deploy-config.js; fi && npm i", 11 | "lint": "solium --dir ./contracts", 12 | "test:prep": "npm run lint && npm run ganache-cli:dev", 13 | "test:e2eprep": "npm run update-module && node scripts/compile-contract.js", 14 | "test:js": "npm run test:prep && truffle test --network rpc ./test/BsktToken.test.js", 15 | "test:sol": "npm run test:prep && truffle test --network rpc ./test/TestBsktToken.sol", 16 | "test:e2e": "npm run test:e2eprep && npm run test:prep && TEST_ENV=e2e truffle test --network rpc ./test/E2E.test.js", 17 | "test:js:gas": "GAS_REPORTER=true npm run test:js", 18 | "test": "npm run test:prep && npm run test:js && npm run test:sol", 19 | "ganache-cli:dev": "scripts/ganache-cli.sh", 20 | "ganache-cli:coverage": "SOLIDITY_COVERAGE=true scripts/ganache-cli.sh", 21 | "cover": "npm run ganache-cli:coverage && ./node_modules/.bin/solidity-coverage", 22 | "update-module": "git submodule update --init && git submodule foreach git pull origin master" 23 | }, 24 | "keywords": [], 25 | "author": "", 26 | "license": "ISC", 27 | "devDependencies": { 28 | "bluebird": "^3.5.1", 29 | "checksum": "^0.1.1", 30 | "eth-gas-reporter": "^0.1.1", 31 | "ether-pudding": "^3.2.0", 32 | "ganache-cli": "^6.0.3", 33 | "glob": "^7.1.2", 34 | "jsonfile": "^4.0.0", 35 | "mkdirp": "^0.5.1", 36 | "npm-install-version": "^6.0.2", 37 | "solc": "^0.4.19", 38 | "solidity-coverage": "^0.4.9", 39 | "solium": "^1.1.3", 40 | "truffle": "=4.1.6", 41 | "truffle-artifactor": "^3.0.3", 42 | "truffle-config": "^1.0.4", 43 | "truffle-contract": "^3.0.3", 44 | "truffle-hdwallet-provider": "0.0.3", 45 | "web3": "0.20.4", 46 | "yarn": "^1.5.1" 47 | }, 48 | "dependencies": { 49 | "colors": "^1.1.2", 50 | "npm": "^5.7.1", 51 | "zeppelin-solidity": "git@github.com:OpenZeppelin/zeppelin-solidity.git#a7e91856f3e275668b4a4c55cbd14864aa61b100" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "git+https://github.com/cryptofinlabs/bskt.git" 56 | }, 57 | "bugs": { 58 | "url": "https://github.com/cryptofinlabs/bskt/issues" 59 | }, 60 | "homepage": "https://github.com/cryptofinlabs/bskt#readme" 61 | } 62 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | // Note: When deploying to mainnet, it tends to timeout due to long confirmation times 2 | const Web3 = require('web3'); 3 | 4 | const truffleConfig = require('../truffle.js') 5 | const deployConfig = require('../config/deploy-config.js') 6 | const secrets = require('../config/secrets.json') 7 | 8 | let MultiSigWallet = artifacts.require('MultiSigWallet'); 9 | let BsktToken = artifacts.require('BsktToken'); 10 | let TokenA = artifacts.require('TokenA'); 11 | let TokenB = artifacts.require('TokenB'); 12 | 13 | let web3 = new Web3(); 14 | 15 | /// @notice Loads configuration based on network 16 | /// @param network {string} 17 | function loadConfig(network) { 18 | let networkConfig = truffleConfig.networks[network]; 19 | let provider = networkConfig.provider ? networkConfig.provider() : new Web3.providers.HttpProvider(`http://${networkConfig.host}:${networkConfig.port}`); 20 | web3.setProvider(provider); 21 | } 22 | 23 | /// @notice Deploys multisig wallet, then the BsktToken, then sets the owner of 24 | /// the Bskt to the multisig to the specified network 25 | /// @param deployer {Object} Deployer object from Truffle 26 | /// @param network {string} String representing network from Truffle 27 | /// @param accounts {Array} Accounts array from Truffle 28 | /// @param callback {function} Callback function to invoke after deployment is 29 | /// done 30 | function deploy(deployer, network, accounts, callback) { 31 | let tokenA; 32 | let tokenB; 33 | let multisig; 34 | let bsktToken; 35 | 36 | const deployFrom = secrets.deploy.coinbase; 37 | const owners = deployConfig[network].multisig.owners; 38 | const required = deployConfig[network].multisig.required; 39 | const tokenAddresses = deployConfig[network].tokenAddresses; 40 | const tokenQuantities = deployConfig[network].tokenQuantities; 41 | const creationUnit = deployConfig[network].creationUnit; 42 | const name = deployConfig[network].name; 43 | const symbol = deployConfig[network].symbol; 44 | 45 | deployer.then(function() { 46 | console.log(owners); 47 | return MultiSigWallet.new(owners, required); 48 | }).then(function(_multisig) { 49 | multisig = _multisig; 50 | console.log('multisig address', multisig.address); 51 | return BsktToken.new(tokenAddresses, tokenQuantities, creationUnit, name, symbol); 52 | }).then(function(_bsktToken) { 53 | bsktToken = _bsktToken; 54 | return bsktToken.transferOwnership(multisig.address); 55 | }).then(function() { 56 | callback({tokenA, tokenB, multisig, bsktToken}); 57 | }); 58 | }; 59 | 60 | /// @notice Deploys contracts to network 61 | module.exports = function(deployer, network, accounts) { 62 | loadConfig(network); 63 | 64 | if (network == 'development') { 65 | deploy(deployer, network, accounts, function(deployed) { 66 | deployed.bsktToken.owner.call().then(function(e) { 67 | console.log('Owner address', e); 68 | console.log('MultiSigWallet address', deployed.multisig.address); 69 | console.log('Done'); 70 | }); 71 | }); 72 | } else if (network == 'ropsten') { 73 | deploy(deployer, network, accounts, function(deployed) { 74 | console.log('Done'); 75 | }); 76 | } else if (network == 'mainnet') { 77 | deploy(deployer, network, accounts, function(deployed) { 78 | console.log('Done'); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /scripts/compile-contract.js: -------------------------------------------------------------------------------- 1 | /** 2 | * How it works: npm run test:e2e 3 | - The test will pull the latest master of the "underlying-token-contracts" repo 4 | - It will compile all the tokens and add to build/contracts 5 | - It will look up what args are used for each contract and use them deploy the token contract to local ganache 6 | - The test will then initialize the Bskt token with each of the underlying tokens 7 | */ 8 | 9 | const P = require('bluebird'); 10 | const fs = require('fs'); 11 | const solc = require('solc'); 12 | const glob = require('glob'); 13 | const mkdirp = require('mkdirp'); 14 | const checksum = require('checksum'); 15 | const jsonfile = require('jsonfile'); 16 | const colors = require('colors'); 17 | const Artifactor = require('truffle-artifactor'); 18 | 19 | const checksumLocation = './temp/underlying-token-contracts/checksum.json'; 20 | const mkdirpPromise = P.promisify(mkdirp); 21 | 22 | let checksumStore; 23 | 24 | glob("**/underlying-token-contracts/*/*.sol", {}, async function (er, files) { 25 | if (!files.length) { 26 | console.log('Cannot find any underlying token contract files, make sure the submodule is imported'); 27 | process.exit(1); 28 | } 29 | if (!fs.existsSync(checksumLocation)){ 30 | await mkdirpPromise('./temp/underlying-token-contracts'); 31 | jsonfile.writeFileSync(checksumLocation, {}, {spaces: 2}); 32 | } 33 | 34 | checksumStore = require(`../${checksumLocation}`); 35 | 36 | for (let i=0; i 0); 60 | _; 61 | } 62 | 63 | /// @notice Initializes contract with a list of ERC20 token addresses and 64 | /// corresponding minimum number of units required for a creation unit 65 | /// @param addresses Addresses of the underlying ERC20 token contracts 66 | /// @param quantities Number of token base units required per creation unit 67 | /// @param _creationUnit Number of base units per creation unit 68 | function BsktToken( 69 | address[] addresses, 70 | uint256[] quantities, 71 | uint256 _creationUnit, 72 | string _name, 73 | string _symbol 74 | ) DetailedERC20(_name, _symbol, 18) public { 75 | require(addresses.length > 0); 76 | require(addresses.length == quantities.length); 77 | require(_creationUnit >= 1); 78 | 79 | for (uint256 i = 0; i < addresses.length; i++) { 80 | tokens.push(TokenInfo({ 81 | addr: addresses[i], 82 | quantity: quantities[i] 83 | })); 84 | } 85 | 86 | creationUnit = _creationUnit; 87 | name = _name; 88 | symbol = _symbol; 89 | } 90 | 91 | /// @notice Creates Bskt tokens in exchange for underlying tokens. Before 92 | /// calling, underlying tokens must be approved to be moved by the Bskt 93 | /// contract. The number of approved tokens required depends on baseUnits. 94 | /// @dev If any underlying tokens' `transferFrom` fails (eg. the token is 95 | /// frozen), create will no longer work. At this point a token upgrade will 96 | /// be necessary. 97 | /// @param baseUnits Number of base units to create. Must be a multiple of 98 | /// creationUnit. 99 | function create(uint256 baseUnits) 100 | external 101 | whenNotPaused() 102 | requireNonZero(baseUnits) 103 | requireMultiple(baseUnits) 104 | { 105 | // Check overflow 106 | require((totalSupply_ + baseUnits) > totalSupply_); 107 | 108 | for (uint256 i = 0; i < tokens.length; i++) { 109 | TokenInfo memory token = tokens[i]; 110 | ERC20 erc20 = ERC20(token.addr); 111 | uint256 amount = baseUnits.div(creationUnit).mul(token.quantity); 112 | require(erc20.transferFrom(msg.sender, address(this), amount)); 113 | } 114 | 115 | mint(msg.sender, baseUnits); 116 | emit Create(msg.sender, baseUnits); 117 | } 118 | 119 | /// @notice Redeems Bskt tokens in exchange for underlying tokens 120 | /// @param baseUnits Number of base units to redeem. Must be a multiple of 121 | /// creationUnit. 122 | /// @param tokensToSkip Underlying token addresses to skip redemption for. 123 | /// Intended to be used to skip frozen or broken tokens which would prevent 124 | /// all underlying tokens from being withdrawn due to a revert. Skipped 125 | /// tokens are left in the Bskt contract and are unclaimable. 126 | function redeem(uint256 baseUnits, address[] tokensToSkip) 127 | external 128 | requireNonZero(baseUnits) 129 | requireMultiple(baseUnits) 130 | { 131 | require(baseUnits <= totalSupply_); 132 | require(baseUnits <= balances[msg.sender]); 133 | require(tokensToSkip.length <= tokens.length); 134 | // Total supply check not required since a user would have to have 135 | // balance greater than the total supply 136 | 137 | // Burn before to prevent re-entrancy 138 | burn(msg.sender, baseUnits); 139 | 140 | for (uint256 i = 0; i < tokens.length; i++) { 141 | TokenInfo memory token = tokens[i]; 142 | ERC20 erc20 = ERC20(token.addr); 143 | uint256 index; 144 | bool ok; 145 | (index, ok) = tokensToSkip.index(token.addr); 146 | if (ok) { 147 | continue; 148 | } 149 | uint256 amount = baseUnits.div(creationUnit).mul(token.quantity); 150 | require(erc20.transfer(msg.sender, amount)); 151 | } 152 | emit Redeem(msg.sender, baseUnits, tokensToSkip); 153 | } 154 | 155 | /// @return addresses Underlying token addresses 156 | function tokenAddresses() external view returns (address[]){ 157 | address[] memory addresses = new address[](tokens.length); 158 | for (uint256 i = 0; i < tokens.length; i++) { 159 | addresses[i] = tokens[i].addr; 160 | } 161 | return addresses; 162 | } 163 | 164 | /// @return quantities Number of token base units required per creation unit 165 | function tokenQuantities() external view returns (uint256[]){ 166 | uint256[] memory quantities = new uint256[](tokens.length); 167 | for (uint256 i = 0; i < tokens.length; i++) { 168 | quantities[i] = tokens[i].quantity; 169 | } 170 | return quantities; 171 | } 172 | 173 | // @dev Mints new Bskt tokens 174 | // @param to Address to mint to 175 | // @param amount Amount to mint 176 | // @return ok Whether the operation was successful 177 | function mint(address to, uint256 amount) internal returns (bool) { 178 | totalSupply_ = totalSupply_.add(amount); 179 | balances[to] = balances[to].add(amount); 180 | emit Transfer(address(0), to, amount); 181 | return true; 182 | } 183 | 184 | // @dev Burns Bskt tokens 185 | // @param from Address to burn from 186 | // @param amount Amount to burn 187 | // @return ok Whether the operation was successful 188 | function burn(address from, uint256 amount) internal returns (bool) { 189 | totalSupply_ = totalSupply_.sub(amount); 190 | balances[from] = balances[from].sub(amount); 191 | emit Transfer(from, address(0), amount); 192 | return true; 193 | } 194 | 195 | // @notice Look up token quantity and whether token exists 196 | // @param token Token address to look up 197 | // @return (quantity, ok) Units of underlying token, and whether the 198 | // token was found 199 | function getQuantity(address token) internal view returns (uint256, bool) { 200 | for (uint256 i = 0; i < tokens.length; i++) { 201 | if (tokens[i].addr == token) { 202 | return (tokens[i].quantity, true); 203 | } 204 | } 205 | return (0, false); 206 | } 207 | 208 | /// @notice Owner: Withdraw excess funds which don't belong to Bskt token 209 | /// holders 210 | /// @param token ERC20 token address to withdraw 211 | function withdrawExcessToken(address token) 212 | external 213 | onlyOwner 214 | nonReentrant 215 | { 216 | ERC20 erc20 = ERC20(token); 217 | uint256 withdrawAmount; 218 | uint256 amountOwned = erc20.balanceOf(address(this)); 219 | uint256 quantity; 220 | bool ok; 221 | (quantity, ok) = getQuantity(token); 222 | if (ok) { 223 | withdrawAmount = amountOwned.sub( 224 | totalSupply_.div(creationUnit).mul(quantity) 225 | ); 226 | } else { 227 | withdrawAmount = amountOwned; 228 | } 229 | require(erc20.transfer(owner, withdrawAmount)); 230 | } 231 | 232 | /// @dev Prevent Bskt tokens from being sent to the Bskt contract 233 | /// @param _to The address to transfer tokens to 234 | /// @param _value the amount of tokens to be transferred 235 | function transfer(address _to, uint256 _value) public returns (bool) { 236 | require(_to != address(this)); 237 | return super.transfer(_to, _value); 238 | } 239 | 240 | /// @dev Prevent Bskt tokens from being sent to the Bskt contract 241 | /// @param _from The address to transfer tokens from 242 | /// @param _to The address to transfer to 243 | /// @param _value The amount of tokens to be transferred 244 | function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { 245 | require(_to != address(this)); 246 | return super.transferFrom(_from, _to, _value); 247 | } 248 | 249 | } 250 | -------------------------------------------------------------------------------- /contracts/MultiSigWallet.sol: -------------------------------------------------------------------------------- 1 | /* solium-disable */ 2 | pragma solidity ^0.4.18; 3 | 4 | 5 | /// @title Multisignature wallet - Allows multiple parties to agree on transactions before execution. 6 | /// @author Stefan George - 7 | contract MultiSigWallet { 8 | 9 | /* 10 | * Events 11 | */ 12 | event Confirmation(address indexed sender, uint indexed transactionId); 13 | event Revocation(address indexed sender, uint indexed transactionId); 14 | event Submission(uint indexed transactionId); 15 | event Execution(uint indexed transactionId); 16 | event ExecutionFailure(uint indexed transactionId); 17 | event Deposit(address indexed sender, uint value); 18 | event OwnerAddition(address indexed owner); 19 | event OwnerRemoval(address indexed owner); 20 | event RequirementChange(uint required); 21 | 22 | /* 23 | * Constants 24 | */ 25 | uint constant public MAX_OWNER_COUNT = 50; 26 | 27 | /* 28 | * Storage 29 | */ 30 | mapping (uint => Transaction) public transactions; 31 | mapping (uint => mapping (address => bool)) public confirmations; 32 | mapping (address => bool) public isOwner; 33 | address[] public owners; 34 | uint public required; 35 | uint public transactionCount; 36 | 37 | struct Transaction { 38 | address destination; 39 | uint value; 40 | bytes data; 41 | bool executed; 42 | } 43 | 44 | /* 45 | * Modifiers 46 | */ 47 | modifier onlyWallet() { 48 | require(msg.sender == address(this)); 49 | _; 50 | } 51 | 52 | modifier ownerDoesNotExist(address owner) { 53 | require(!isOwner[owner]); 54 | _; 55 | } 56 | 57 | modifier ownerExists(address owner) { 58 | require(isOwner[owner]); 59 | _; 60 | } 61 | 62 | modifier transactionExists(uint transactionId) { 63 | require(transactions[transactionId].destination != 0); 64 | _; 65 | } 66 | 67 | modifier confirmed(uint transactionId, address owner) { 68 | require(confirmations[transactionId][owner]); 69 | _; 70 | } 71 | 72 | modifier notConfirmed(uint transactionId, address owner) { 73 | require(!confirmations[transactionId][owner]); 74 | _; 75 | } 76 | 77 | modifier notExecuted(uint transactionId) { 78 | require(!transactions[transactionId].executed); 79 | _; 80 | } 81 | 82 | modifier notNull(address _address) { 83 | require(_address != 0); 84 | _; 85 | } 86 | 87 | modifier validRequirement(uint ownerCount, uint _required) { 88 | require(ownerCount <= MAX_OWNER_COUNT 89 | && _required <= ownerCount 90 | && _required != 0 91 | && ownerCount != 0); 92 | _; 93 | } 94 | 95 | /// @dev Fallback function allows to deposit ether. 96 | function() 97 | payable 98 | { 99 | if (msg.value > 0) 100 | Deposit(msg.sender, msg.value); 101 | } 102 | 103 | /* 104 | * Public functions 105 | */ 106 | /// @dev Contract constructor sets initial owners and required number of confirmations. 107 | /// @param _owners List of initial owners. 108 | /// @param _required Number of required confirmations. 109 | function MultiSigWallet(address[] _owners, uint _required) 110 | public 111 | validRequirement(_owners.length, _required) 112 | { 113 | for (uint i=0; i<_owners.length; i++) { 114 | require(!isOwner[_owners[i]] && _owners[i] != 0); 115 | isOwner[_owners[i]] = true; 116 | } 117 | owners = _owners; 118 | required = _required; 119 | } 120 | 121 | /// @dev Allows to add a new owner. Transaction has to be sent by wallet. 122 | /// @param owner Address of new owner. 123 | function addOwner(address owner) 124 | public 125 | onlyWallet 126 | ownerDoesNotExist(owner) 127 | notNull(owner) 128 | validRequirement(owners.length + 1, required) 129 | { 130 | isOwner[owner] = true; 131 | owners.push(owner); 132 | OwnerAddition(owner); 133 | } 134 | 135 | /// @dev Allows to remove an owner. Transaction has to be sent by wallet. 136 | /// @param owner Address of owner. 137 | function removeOwner(address owner) 138 | public 139 | onlyWallet 140 | ownerExists(owner) 141 | { 142 | isOwner[owner] = false; 143 | for (uint i=0; i owners.length) 150 | changeRequirement(owners.length); 151 | OwnerRemoval(owner); 152 | } 153 | 154 | /// @dev Allows to replace an owner with a new owner. Transaction has to be sent by wallet. 155 | /// @param owner Address of owner to be replaced. 156 | /// @param newOwner Address of new owner. 157 | function replaceOwner(address owner, address newOwner) 158 | public 159 | onlyWallet 160 | ownerExists(owner) 161 | ownerDoesNotExist(newOwner) 162 | { 163 | for (uint i=0; i TokenA); 278 | tokenCountList = Array.from({length: 20}, () => 2); 279 | const result = await setupBsktToken(owner, bskt20Buyer, tokenList, tokenCountList, 1); 280 | bskt20Token = result.bsktToken; 281 | tokenInstances = result.underlyingTokensInstance; 282 | }); 283 | 284 | conditionalIt('should create bskt tokens with 20 tokens for buyer', async function test() { 285 | const txReceipt = await bskt20Token.create(100, {from: bskt20Buyer}); 286 | //assert.equal(txReceipt.logs.length, 22, 'logs should be created'); 287 | 288 | const buyerBalance = await bskt20Token.balanceOf.call(bskt20Buyer); 289 | assert.equal(buyerBalance.toNumber(), 100, 'should have correct buyer balance'); 290 | }); 291 | 292 | conditionalIt('should not send any underlying tokens if tokens transfer fails mid way of creation', async function test() { 293 | const fifthToken = tokenInstances[4]; 294 | await fifthToken.approve(bskt20Token.address, 0, {from: bskt20Buyer}); 295 | const allowanceAmount = await fifthToken.allowance.call(bskt20Buyer, bskt20Token.address); 296 | 297 | assert.equal(allowanceAmount, 0, 'invalid allowance amount'); 298 | try { 299 | await bskt20Token.create(200, {from: bskt20Buyer}); 300 | } catch(e) { 301 | const buyerBalance = await bskt20Token.balanceOf.call(bskt20Buyer); 302 | assert.equal(buyerBalance.toNumber(), 0, 'should have no bskt token'); 303 | for (let i = 0; i < tokenInstances.length; i++) { 304 | const buyerBalance = await tokenInstances[i].balanceOf(bskt20Buyer); 305 | const contractBalance = await tokenInstances[i].balanceOf(bskt20Token.address); 306 | assert.equal(buyerBalance.toNumber(), 200, 'should have the original token amount'); 307 | assert.equal(contractBalance.toNumber(), 0, 'should have no underlying token'); 308 | } 309 | } 310 | }); 311 | 312 | context('Locked funds recovery', function () { 313 | 314 | // TODO: what happens if token address isn't an ERC20 and we try to cast? 315 | 316 | // Note that the bskt20Token owner also holds all the underlying tokens 317 | conditionalIt('should recover tokens sent to contract', async function test() { 318 | const token = tokenInstances[0]; 319 | const ownerBalanceStart = await token.balanceOf(owner); 320 | 321 | await token.transfer(bskt20Token.address, 10); 322 | const ownerBalanceMid = await token.balanceOf(owner); 323 | await bskt20Token.withdrawExcessToken(token.address); 324 | 325 | const ownerBalanceEnd = await token.balanceOf(owner); 326 | 327 | assert.equal(ownerBalanceStart.toNumber() - ownerBalanceMid.toNumber(), 10); 328 | assert.equal(ownerBalanceEnd.toNumber() - ownerBalanceMid.toNumber(), 10); 329 | assert.equal(ownerBalanceStart.toNumber(), ownerBalanceEnd.toNumber()); 330 | }); 331 | 332 | conditionalIt('should not withdraw tokens for non-owner', async function test() { 333 | const token = tokenInstances[0]; 334 | const buyer1BalanceStart = await token.balanceOf(buyer1); 335 | 336 | await token.transfer(bskt20Token.address, 10); 337 | try { 338 | const tx = await bskt20Token.withdrawExcessToken(token.address, {from: buyer1}); 339 | assert.fail(false, true, 'contract address input should not be correct'); 340 | } catch(e) { 341 | assertRevert(e); 342 | } 343 | 344 | const buyer1BalanceEnd = await token.balanceOf(buyer1); 345 | 346 | assert.equal(buyer1BalanceStart.toNumber(), buyer1BalanceEnd.toNumber()); 347 | }); 348 | 349 | conditionalIt('should recover exactly excess tokens sent to contract for bskt token', async () => { 350 | const token = tokenInstances[0]; 351 | 352 | await bskt20Token.create(100, {from: bskt20Buyer}); 353 | await token.transfer(bskt20Token.address, 1000); // Excess tokens 354 | const bsktTokenBalanceWithExcess = await token.balanceOf(bskt20Token.address); 355 | await bskt20Token.withdrawExcessToken(token.address, {from: owner}); 356 | const bsktTokenBalanceAfterWithdraw = await token.balanceOf(bskt20Token.address); 357 | 358 | assert.equal(bsktTokenBalanceWithExcess.toNumber() - bsktTokenBalanceAfterWithdraw.toNumber(), 1000); 359 | assert.equal(bsktTokenBalanceAfterWithdraw, 100 * tokenCountList[0]); 360 | }); 361 | 362 | conditionalIt('should recover all excess tokens sent to contract for non-bskt token', async () => { 363 | const otherToken = await TokenB.new({from: buyer1}); 364 | await bskt20Token.create(100, {from: bskt20Buyer}); 365 | await otherToken.transfer(bskt20Token.address, 1000, {from: buyer1}); // Excess tokens 366 | await bskt20Token.withdrawExcessToken(otherToken.address); 367 | const ownerTokenBalance = await otherToken.balanceOf(owner); 368 | 369 | assert.equal(ownerTokenBalance, 1000); 370 | }); 371 | 372 | }); 373 | 374 | // TODO: initialization tests 375 | 376 | // TODO: realistic creationUnit test 377 | 378 | }); 379 | 380 | context('With unique underlying tokens', function () { 381 | let bskt, tokenInstances, tokenCountList, tokenA, tokenB, tokenC; 382 | 383 | beforeEach(async function () { 384 | const tokenList = [TokenA, TokenB, TokenC]; 385 | tokenCountList = [1, 2, 3]; 386 | const result = await setupBsktToken(owner, buyer1, tokenList, tokenCountList, 1); 387 | bskt = result.bsktToken; 388 | [tokenA, tokenB, tokenC] = result.underlyingTokensInstance; 389 | }); 390 | 391 | conditionalIt('should skip redeem for specified tokens', async function () { 392 | await bskt.create(10, {from: buyer1}); 393 | await bskt.redeem(10, [tokenB.address, tokenC.address], {from: buyer1}); 394 | 395 | let bsktTokenABalance = await tokenA.balanceOf(bskt.address); 396 | let bsktTokenBBalance = await tokenB.balanceOf(bskt.address); 397 | let bsktTokenCBalance = await tokenC.balanceOf(bskt.address); 398 | 399 | assert.equal(bsktTokenABalance.toNumber(), 0, 'contract TokenA balance should be 0'); 400 | assert.equal(bsktTokenBBalance.toNumber(), 10 * tokenCountList[1], 'contract TokenB balance should not have been redeemed'); 401 | assert.equal(bsktTokenCBalance.toNumber(), 10 * tokenCountList[2], 'contract TokenC balance should not have been redeemed'); 402 | }); 403 | 404 | conditionalIt('should skip redeem for specified tokens and owner withdraws them', async function () { 405 | let ownerTokenBBalanceStart = await tokenB.balanceOf(owner); 406 | let ownerTokenCBalanceStart = await tokenC.balanceOf(owner); 407 | 408 | await bskt.create(100, {from: buyer1}); 409 | await bskt.redeem(100, [tokenB.address, tokenC.address], {from: buyer1}); 410 | 411 | await bskt.withdrawExcessToken(tokenB.address, {from: owner}); 412 | await bskt.withdrawExcessToken(tokenC.address, {from: owner}); 413 | 414 | let bsktTokenBBalance = await tokenB.balanceOf(bskt.address); 415 | let bsktTokenCBalance = await tokenC.balanceOf(bskt.address); 416 | let ownerTokenBBalanceEnd = await tokenB.balanceOf(owner); 417 | let ownerTokenCBalanceEnd = await tokenC.balanceOf(owner); 418 | 419 | assert.equal(bsktTokenBBalance.toNumber(), 0, 'TokenB balance should be 0'); 420 | assert.equal(bsktTokenCBalance.toNumber(), 0, 'TokenC balance should be 0'); 421 | assert.equal(ownerTokenBBalanceEnd.toNumber() - ownerTokenBBalanceStart.toNumber(), 100 * tokenCountList[1], 'owner should have withdrawn the excess TokenBs'); 422 | assert.equal(ownerTokenCBalanceEnd.toNumber() - ownerTokenCBalanceStart.toNumber(), 100 * tokenCountList[2], 'owner should have withdrawn the excess TokenCs'); 423 | }); 424 | 425 | }); 426 | 427 | it('should not be able to initialize with more than 255', async function() { 428 | const tokenList = Array.from({length: 256}, () => TokenA); 429 | const tokenCountList = Array.from({length: 256}, () => 2); 430 | try { 431 | await BsktToken.new(tokenList, tokenCountList, 2, 'Basket', 'BSK'); 432 | assert.fail(false, 'should not be able to deploy with more than 255 tokens'); 433 | } catch(e) { 434 | // test may be failing because of gas before it fails because of length 435 | assert.isOk(e, 'some error'); 436 | } 437 | }); 438 | 439 | }); 440 | 441 | /**- 442 | * Setup Bskt token with underlying token assets 443 | * @param owner: owner address 444 | * @param buyer: buyer address 445 | * @param underlyingTokens: token list 446 | * @param tokenCountList: list of count of each token in the bskt contract 447 | * @param creationUnit: creationUnit for bskt token 448 | * @return {bsktTokenInstance, [tokenInstance]} 449 | */ 450 | async function setupBsktToken(owner, buyer, underlyingTokens, tokenCountList, creationUnit) { 451 | const underlyingTokensPromise = underlyingTokens.map(token => token.new({from: owner})); 452 | const underlyingTokensInstance = await P.all(underlyingTokensPromise); 453 | 454 | const bsktToken = await BsktToken.new( 455 | underlyingTokensInstance.map(token => token.address), 456 | tokenCountList, 457 | creationUnit, 458 | 'Basket', 459 | 'BSK', 460 | {from: owner} 461 | ); 462 | 463 | const CREATION_UNIT_MULTIPLE = NUM_UNITS / creationUnit; 464 | // Can't use await properly in a forEach loop 465 | for (let i = 0; i < underlyingTokensInstance.length; i++) { 466 | await underlyingTokensInstance[i].transfer(buyer, CREATION_UNIT_MULTIPLE * tokenCountList[i]); 467 | await underlyingTokensInstance[i].approve( 468 | bsktToken.address, 469 | CREATION_UNIT_MULTIPLE * tokenCountList[i], 470 | {from: buyer} 471 | ); 472 | } 473 | 474 | return { 475 | bsktToken, 476 | underlyingTokensInstance 477 | }; 478 | } 479 | -------------------------------------------------------------------------------- /test/E2E.test.js: -------------------------------------------------------------------------------- 1 | const P = require('bluebird'); 2 | const glob = require('glob'); 3 | 4 | const TokenA = artifacts.require('TokenA'); 5 | const BsktToken = artifacts.require('BsktToken'); 6 | 7 | const {ETW_DECIMALS} = require('./constants'); 8 | const {toBU} = require('./utils'); 9 | 10 | const provider = new web3.providers.HttpProvider("http://localhost:7545"); 11 | const globPromise = P.promisify(glob); 12 | 13 | const CREATION_UNIT = 10 ** 16; 14 | const CREATION_UNITS_PER_BSKT = (10 ** ETW_DECIMALS) / CREATION_UNIT; 15 | 16 | let mockToken; 17 | 18 | function conditionalIt(title, test) { 19 | let shouldSkip = true; 20 | if (process.env.TEST_ENV === 'e2e') { 21 | shouldSkip = false; 22 | } 23 | return shouldSkip ? it.skip(title, test) : it(title, test); 24 | } 25 | 26 | /** 27 | * Test balances and total supply for bskt contract, buyer and owner during creation process 28 | */ 29 | async function testCreationTokenState(bsktToken, underlyingToken, owner, buyer, contractName) { 30 | const initialContractBalance = await underlyingToken.balanceOf(bsktToken.address); 31 | const initialBuyerBalance = await underlyingToken.balanceOf(buyer); 32 | const initialTotalSupply = await bsktToken.totalSupply(); 33 | assert.equal(initialContractBalance.toNumber(), 0, `contract should have no ${contractName}`); 34 | assert.equal(initialBuyerBalance.toNumber(), 100 * CREATION_UNITS_PER_BSKT, `buyer should have some ${contractName} token`); 35 | assert.equal(initialTotalSupply.toNumber(), 0, `Bskt contract should have 0 total supply after creation`); 36 | 37 | const txReceipt = await bsktToken.create(toBU(1), {from: buyer}); 38 | 39 | const postCreateContractBalance = await underlyingToken.balanceOf(bsktToken.address); 40 | const postCreateBuyerBalance = await underlyingToken.balanceOf(buyer); 41 | const postCreateTotalSupply = await bsktToken.totalSupply(); 42 | assert.equal(postCreateContractBalance.toNumber(), 100 * CREATION_UNITS_PER_BSKT, `contract should have 100 ${contractName}`); 43 | assert.equal(postCreateBuyerBalance.toNumber(), 0, `buyer should have no ${contractName} left`); 44 | assert.equal(postCreateTotalSupply.toNumber(), toBU(1), `Bskt contract should have correct supply after creation`); 45 | 46 | const bsktTokenBuyerBalance = await bsktToken.balanceOf(buyer); 47 | assert.equal(bsktTokenBuyerBalance.toNumber(), toBU(1), 'buyer should have correct Bskt token balance'); 48 | } 49 | 50 | contract('E2E token testing', function([owner, buyer]) { 51 | let bsktToken, underlyingTokensInstance; 52 | 53 | context('Bskt with individual tokens', function() { 54 | before(async function () { 55 | mockToken = await TokenA.new({from: owner}); 56 | }); 57 | conditionalIt('should initialize Bskt with ZRXToken successfully', async function() { 58 | const contractName = 'ZRXToken'; 59 | const result = await initializeSingleContract(contractName, owner, buyer); 60 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 61 | }); 62 | conditionalIt('should initialize Bskt with AElfToken successfully', async function() { 63 | const contractName = 'AElfToken'; 64 | const result = await initializeSingleContract(contractName, owner, buyer); 65 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 66 | }); 67 | conditionalIt('should initialize Bskt with AEToken successfully', async function() { 68 | const contractName = 'AEToken'; 69 | const result = await initializeSingleContract(contractName, owner, buyer); 70 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 71 | }); 72 | conditionalIt('should initialize Bskt with RepToken successfully', async function() { 73 | const contractName = 'RepToken'; 74 | const result = await initializeSingleContract(contractName, owner, buyer); 75 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 76 | }); 77 | conditionalIt('should initialize Bskt with BAToken successfully', async function() { 78 | const contractName = 'BAToken'; 79 | const result = await initializeSingleContract(contractName, owner, buyer); 80 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 81 | }); 82 | conditionalIt('should initialize Bskt with BNB successfully', async function() { 83 | const contractName = 'BNB'; 84 | const result = await initializeSingleContract(contractName, owner, buyer); 85 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 86 | }); 87 | conditionalIt('should initialize Bskt with Dragon successfully', async function() { 88 | const contractName = 'Dragon'; 89 | const result = await initializeSingleContract(contractName, owner, buyer); 90 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 91 | }); 92 | conditionalIt('should initialize Bskt with EOS successfully', async function() { 93 | const contractName = 'EOS'; 94 | const result = await initializeSingleContract(contractName, owner, buyer); 95 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 96 | }); 97 | conditionalIt('should initialize Bskt with IcxToken successfully', async function() { 98 | const contractName = 'IcxToken'; 99 | const result = await initializeSingleContract(contractName, owner, buyer); 100 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 101 | }); 102 | conditionalIt('should initialize Bskt with IOSToken successfully', async function() { 103 | const contractName = 'IOSToken'; 104 | const result = await initializeSingleContract(contractName, owner, buyer); 105 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 106 | }); 107 | conditionalIt('should initialize Bskt with KyberNetworkCrystal successfully', async function() { 108 | const contractName = 'KyberNetworkCrystal'; 109 | const result = await initializeSingleContract(contractName, owner, buyer); 110 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 111 | }); 112 | conditionalIt('should initialize Bskt with OMGToken successfully', async function() { 113 | const contractName = 'OMGToken'; 114 | const result = await initializeSingleContract(contractName, owner, buyer); 115 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 116 | }); 117 | conditionalIt('should initialize Bskt with Populous successfully', async function() { 118 | const contractName = 'Populous'; 119 | const result = await initializeSingleContract(contractName, owner, buyer); 120 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 121 | }); 122 | conditionalIt('should initialize Bskt with PowerLedger successfully', async function() { 123 | const contractName = 'PowerLedger'; 124 | const result = await initializeSingleContract(contractName, owner, buyer); 125 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 126 | }); 127 | conditionalIt('should initialize Bskt with QASHToken successfully', async function() { 128 | const contractName = 'QASHToken'; 129 | const result = await initializeSingleContract(contractName, owner, buyer); 130 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 131 | }); 132 | conditionalIt('should initialize Bskt with QTUM successfully', async function() { 133 | const contractName = 'HumanStandardToken'; 134 | const result = await initializeSingleContract(contractName, owner, buyer); 135 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 136 | }); 137 | conditionalIt('should initialize Bskt with SNT successfully', async function() { 138 | const contractName = 'SNT'; 139 | const result = await initializeSingleContract(contractName, owner, buyer); 140 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 141 | }); 142 | conditionalIt('should initialize Bskt with TronToken successfully', async function() { 143 | const contractName = 'TronToken'; 144 | const result = await initializeSingleContract(contractName, owner, buyer); 145 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 146 | }); 147 | conditionalIt('should initialize Bskt with VEN successfully', async function() { 148 | const contractName = 'VEN'; 149 | const result = await initializeSingleContract(contractName, owner, buyer); 150 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 151 | }); 152 | conditionalIt('should initialize Bskt with WaltonToken successfully', async function() { 153 | const contractName = 'WaltonToken'; 154 | const result = await initializeSingleContract(contractName, owner, buyer); 155 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 156 | }); 157 | conditionalIt('should initialize Bskt with ZilliqaToken successfully', async function() { 158 | const contractName = 'ZilliqaToken'; 159 | const result = await initializeSingleContract(contractName, owner, buyer); 160 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 161 | }); 162 | conditionalIt('should initialize Bskt with DigixDao successfully', async function() { 163 | const contractName = 'DigixDao'; 164 | const result = await initializeSingleContract(contractName, owner, buyer); 165 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 166 | }); 167 | conditionalIt('should initialize Bskt with BytomToken successfully', async function() { 168 | const contractName = 'BytomToken'; 169 | const result = await initializeSingleContract(contractName, owner, buyer); 170 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 171 | }); 172 | conditionalIt('should initialize Bskt with Revain successfully', async function() { 173 | const contractName = 'Revain'; 174 | const result = await initializeSingleContract(contractName, owner, buyer); 175 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 176 | }); 177 | conditionalIt('should initialize Bskt with Bibox Token successfully', async function() { 178 | const contractName = 'BIXToken'; 179 | const result = await initializeSingleContract(contractName, owner, buyer); 180 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 181 | }); 182 | conditionalIt('should initialize ETF with ADXToken successfully', async function() { 183 | const contractName = 'ADXToken'; 184 | const result = await initializeSingleContract(contractName, owner, buyer); 185 | await testCreationTokenState(result.bsktToken, result.tokenInstances[0], owner, buyer, contractName); 186 | }); 187 | }); 188 | 189 | context('Bskt with 20 token portfolio', async function() { 190 | const tokensInfo = [ 191 | { 192 | name: 'EOS', 193 | units: 15459501974133 194 | }, 195 | { 196 | name: 'TronToken', 197 | units: 1459 198 | }, 199 | { 200 | name: 'VEN', 201 | units: 10550407423850 202 | }, 203 | { 204 | name: 'IcxToken', 205 | units: 8567415065480 206 | }, 207 | { 208 | name: 'Populous', 209 | units: 82 210 | }, 211 | { 212 | name: 'OMGToken', 213 | units: 2265069167986 214 | }, 215 | { 216 | name: 'BNB', 217 | units: 2197844246343 218 | }, 219 | { 220 | name: 'DigixDao', 221 | units: 44 222 | }, 223 | { 224 | name: 'ZRXToken', 225 | units: 11419786553677 226 | }, 227 | { 228 | name: 'RepToken', 229 | units: 244170152636 230 | }, 231 | { 232 | name: 'WaltonToken', 233 | units: 552672567904 234 | }, 235 | { 236 | name: 'BytomToken', 237 | units: 2191 238 | }, 239 | { 240 | name: 'SNT', 241 | units: 77035430289520 242 | }, 243 | { 244 | name: 'IOStoken', 245 | units: 150811070934861 246 | }, 247 | { 248 | name: 'ZilliqaToken', 249 | units: 144531945 250 | }, 251 | { 252 | name: 'KyberNetworkCrystal', 253 | units: 2977382061046 254 | }, 255 | { 256 | name: 'BAToken', 257 | units: 22197314735194 258 | }, 259 | { 260 | name: 'AElfToken', 261 | units: 5549330107047 262 | }, 263 | { 264 | name: 'QASHToken', 265 | units: 8 266 | }, 267 | { 268 | name: 'ADXToken', 269 | units: 2 270 | } 271 | ]; 272 | 273 | before(async function () { 274 | mockToken = await TokenA.new({from: owner}); 275 | }); 276 | conditionalIt('should create then redeem Bskt with all tokens', async function() { 277 | // Prepare tokens 278 | const contractNames = tokensInfo.map(token => token.name); 279 | const quantity = tokensInfo.map(token => token.units); 280 | const result = await initializePortfolioContract( 281 | contractNames, 282 | quantity, 283 | owner, 284 | buyer 285 | ); 286 | const bsktToken = result.bsktToken; 287 | const underlyingTokens = result.tokenInstances; 288 | 289 | // Test initial values 290 | for (let i = 0; i < underlyingTokens.length; i++) { 291 | const initialContractBalance = await underlyingTokens[i].balanceOf(bsktToken.address); 292 | const initialBuyerBalance = await underlyingTokens[i].balanceOf(buyer); 293 | assert.equal(initialContractBalance.toNumber(), 0, `contract should have no ${contractNames[i]}`); 294 | assert.equal(initialBuyerBalance.toNumber(), quantity[i] * CREATION_UNITS_PER_BSKT, `buyer should have some ${contractNames[i]} token`); 295 | } 296 | 297 | const initialTotalSupply = await bsktToken.totalSupply(); 298 | assert.equal(initialTotalSupply.toNumber(), 0, `Bskt contract should have 0 total supply after creation`); 299 | 300 | // Create Bskt 301 | const createReceipt = await bsktToken.create(web3.toWei(1, 'ether'), {from: buyer}); 302 | // Test post-creation values 303 | for (let i = 0; i < underlyingTokens.length; i++) { 304 | const postCreateContractBalance = await underlyingTokens[i].balanceOf(bsktToken.address); 305 | const postCreateBuyerBalance = await underlyingTokens[i].balanceOf(buyer); 306 | assert.equal(postCreateContractBalance.toNumber(), quantity[i] * CREATION_UNITS_PER_BSKT, `contract should have correct amount of ${contractNames[i]} in 1 Bskt`); 307 | assert.equal(postCreateBuyerBalance.toNumber(), 0, `buyer should have no ${contractNames[i]} left`); 308 | } 309 | const postCreateTotalSupply = await bsktToken.totalSupply(); 310 | const bsktTokenBuyerBalancePostCreate = await bsktToken.balanceOf(buyer); 311 | assert.equal(postCreateTotalSupply.toNumber(), toBU(1), `Bskt contract should have correct supply after creation`); 312 | assert.equal(bsktTokenBuyerBalancePostCreate.toNumber(), toBU(1), 'buyer should have correct Bskt token balance'); 313 | 314 | // Redeem Bskt 315 | const redeemReceipt = await bsktToken.redeem(toBU(1), [], {from: buyer}); 316 | 317 | // Test post-redemption values 318 | for (let i = 0; i < underlyingTokens.length; i++) { 319 | const postRedeemContractBalance = await underlyingTokens[i].balanceOf(bsktToken.address); 320 | const postRedeemBuyerBalance = await underlyingTokens[i].balanceOf(buyer); 321 | assert.equal(postRedeemContractBalance.toNumber(), 0, `contract should have no ${contractNames[i]} left`); 322 | assert.equal(postRedeemBuyerBalance.toNumber(), quantity[i] * CREATION_UNITS_PER_BSKT, `buyer should have the same ${contractNames[i]} balance initially`); 323 | } 324 | const postRedeemTotalSupply = await bsktToken.totalSupply(); 325 | const bsktTokenBuyerBalancePostRedeem = await bsktToken.balanceOf(buyer); 326 | 327 | assert.equal(postRedeemTotalSupply.toNumber(), 0, `Bskt contract should have no token after redemption`); 328 | assert.equal(bsktTokenBuyerBalancePostRedeem.toNumber(), 0, 'buyer should have no Bskt token'); 329 | }); 330 | }); 331 | }); 332 | 333 | 334 | async function initializePortfolioContract(contractNames, quantity, owner, buyer) { 335 | try { 336 | const deployPromises = contractNames.map((contractName) => { 337 | return deployUnderlyingToken(owner, contractName); 338 | }); 339 | const instances = await P.all(deployPromises); 340 | const result = await setupBsktToken(owner, buyer, instances, quantity, contractNames); 341 | return result; 342 | } catch(e) { 343 | assert.ifError(e); 344 | } 345 | } 346 | 347 | async function initializeSingleContract(contractName, owner, buyer) { 348 | try { 349 | const instance = await deployUnderlyingToken(owner, contractName); 350 | const result = await setupBsktToken(owner, buyer, [instance], null, [contractName]); 351 | return result; 352 | } catch(e) { 353 | assert.ifError(e); 354 | } 355 | } 356 | 357 | async function deployUnderlyingToken(owner, contractName) { 358 | const contract = artifacts.require(contractName); 359 | const argsFile = await globPromise(`**/underlying-token-contracts/*/${contractName}.json`, {}); 360 | const contractArgs = require(`../${argsFile[0]}`); 361 | const argsValue = contractArgs.values.slice(); 362 | argsValue.push({from: owner}); 363 | contract.setProvider(provider); 364 | try { 365 | const preprocessedArgs = overrideConstructorArgs(contractName, argsValue, mockToken, owner); 366 | const instance = await contract.new.apply(contract, preprocessedArgs); 367 | return instance; 368 | } catch(e) { 369 | console.log(e); 370 | } 371 | } 372 | 373 | /** 374 | * Setup Bskt token with an underlying token instance 375 | * @param {address} owner Address of contract owner 376 | * @param {address} buyer Address of buyer 377 | * @param {[ContractInstance]} tokenInstances Instances of deployed underlying token contracts 378 | * @param {[string]} tokenCountList Count of Each token per Bskt 379 | * @param {[string]} contractNames Name of deployed contract 380 | * @return {} Bskt token instance and deployed underlying token instance 381 | */ 382 | async function setupBsktToken(owner, buyer, tokenInstances, tokenCountList, contractNames) { 383 | if (!tokenCountList) { 384 | tokenCountList = Array.from({length: tokenInstances.length}, () => 100); 385 | } 386 | const bsktToken = await BsktToken.new( 387 | tokenInstances.map(token => token.address), 388 | tokenCountList, 389 | CREATION_UNIT, 390 | 'Basket', 391 | 'BSK', 392 | {from: owner} 393 | ); 394 | 395 | await prepUnderlyingTokensForTransfer(owner, buyer, tokenInstances, tokenCountList, contractNames, bsktToken); 396 | 397 | return { 398 | bsktToken, 399 | tokenInstances 400 | }; 401 | } 402 | 403 | /** 404 | * Enable buyer transfering tokens to bskt contract 405 | * @param {address} owner Owner's address 406 | * @param {address} buyer Buyer's address 407 | * @param {[ContractInstance]} tokenInstances Deployed token contract instances 408 | * @param {[string]} contractNames Token Contract Name 409 | * @param {ContractInstance} bsktToken Deployed bskt token contract instance 410 | */ 411 | async function prepUnderlyingTokensForTransfer(owner, buyer, tokenInstances, tokenCountList, contractNames, bsktToken) { 412 | // cannot do await properly in a forEach loop 413 | for (let i = 0; i < tokenInstances.length; i++) { 414 | try { 415 | const tokenUnit = tokenCountList[i]; 416 | const unitsToTransfer = tokenUnit * CREATION_UNITS_PER_BSKT; 417 | const contractName = contractNames[i]; 418 | 419 | switch(contractName) { 420 | case 'AElfToken': 421 | await tokenInstances[i].approveMintTokens(buyer, unitsToTransfer); 422 | await tokenInstances[i].mintTokens(buyer); 423 | break; 424 | case 'AEToken': 425 | await tokenInstances[i].prefill([buyer], [unitsToTransfer], {from: owner}); 426 | await tokenInstances[i].launch({from: owner}); 427 | break; 428 | case 'IcxToken': 429 | await tokenInstances[i].enableTokenTransfer(); 430 | await tokenInstances[i].transfer(buyer, unitsToTransfer); 431 | break; 432 | case 'VEN': 433 | await tokenInstances[i].mint(buyer, unitsToTransfer, false, 44444); 434 | await tokenInstances[i].seal(); 435 | break; 436 | case 'ZilliqaToken': 437 | await tokenInstances[i].pause(false, false); 438 | await tokenInstances[i].transfer(buyer, unitsToTransfer); 439 | break; 440 | case 'Populous': 441 | await tokenInstances[i].setReleaseAgent(owner); 442 | await tokenInstances[i].releaseTokenTransfer(); 443 | await tokenInstances[i].transfer(buyer, unitsToTransfer); 444 | break; 445 | case 'RepToken': 446 | await tokenInstances[i].unpause({from: owner}); 447 | await tokenInstances[i].transfer(buyer, unitsToTransfer); 448 | break; 449 | case 'EOS': 450 | await tokenInstances[i].mint(unitsToTransfer, {from: buyer}); 451 | break; 452 | case 'OMGToken': 453 | await tokenInstances[i].mint(buyer, unitsToTransfer); 454 | break; 455 | case 'SNT': 456 | await tokenInstances[i].generateTokens(buyer, unitsToTransfer); 457 | break; 458 | case 'DigixDao': 459 | await tokenInstances[i].mint(buyer, unitsToTransfer); 460 | break; 461 | default: 462 | await tokenInstances[i].transfer(buyer, unitsToTransfer); 463 | break; 464 | } 465 | await tokenInstances[i].approve( 466 | bsktToken.address, 467 | unitsToTransfer, 468 | {from: buyer} 469 | ); 470 | } catch(e) { 471 | console.log(`Error with ${contractNames[i]}:`.red); 472 | console.log(e); 473 | throw(e); 474 | } 475 | } 476 | } 477 | 478 | /** 479 | * Custom logic to modify contract contrustor arguments for smoother initialization 480 | */ 481 | function overrideConstructorArgs(contractName, args, mockToken, owner) { 482 | switch(contractName) { 483 | case 'RepToken': 484 | args[0] = mockToken.address; 485 | args[1] = -1; 486 | args[2] = owner; 487 | return args; 488 | case 'IcxToken': 489 | args = [-1, mockToken.address]; // -1 is MAX_UINT 490 | break; 491 | case 'BAToken': 492 | args = ["0xac2fa512db158f44f5ee2fa5766ea7c282763cdb", owner, "3798640", "3963480"]; 493 | break; 494 | case 'DigixDao': 495 | case'BIXToken': 496 | case 'TronToken': 497 | args = [owner]; 498 | break; 499 | } 500 | return args; 501 | } 502 | --------------------------------------------------------------------------------