├── .gitignore ├── docs └── images │ ├── permissions.png │ ├── upgradability.png │ └── component_diagram.png ├── migrations ├── 1_initial_migration.js ├── 2_deploy_contracts.js └── 3_deploy_test_tokens.js ├── LICENSE.txt ├── contracts ├── Migrations.sol ├── TestRegulatedToken.sol ├── RegulatorService.sol ├── ServiceRegistry.sol ├── TestRegulatorService.sol ├── RegulatedToken.sol └── TokenRegulatorService.sol ├── package.json ├── .circleci └── config.yml ├── test ├── helpers │ ├── MockRegulatorService.sol │ └── MockRegulatedToken.sol ├── TestRegulatedToken.js ├── helpers.js ├── ServiceRegistry.js ├── TestRegulatorService.js ├── RegulatedToken.js └── TokenRegulatorService.js ├── truffle.js ├── scripts └── mint_test_tokens.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | *.log 4 | -------------------------------------------------------------------------------- /docs/images/permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harborhq/r-token/HEAD/docs/images/permissions.png -------------------------------------------------------------------------------- /docs/images/upgradability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harborhq/r-token/HEAD/docs/images/upgradability.png -------------------------------------------------------------------------------- /docs/images/component_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harborhq/r-token/HEAD/docs/images/component_diagram.png -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Harbor Platform, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the “License”); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an “AS IS” BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 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) restricted public { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) restricted public { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "r-token", 3 | "version": "1.0.0", 4 | "description": "Proof of concept for RegulatedToken", 5 | "main": "truffle.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "truffle test", 11 | "test:ganache": "yarn ganache-cli -m \"$RTOKEN_DEVELOPMENT_MNEMONIC\" -e 100" 12 | }, 13 | "author": "Bob Remeika", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "ganache-cli": "^6.1.0", 17 | "truffle": "^4.1.5", 18 | "truffle-hdwallet-provider": "^0.0.3", 19 | "web3": "^1.0.0-beta.33" 20 | }, 21 | "dependencies": { 22 | "bignumber.js": "^6.0.0", 23 | "zeppelin-solidity": "^1.4.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | dependencies: 3 | override: 4 | - yarn 5 | jobs: 6 | build: 7 | docker: 8 | # specify the version you desire here 9 | - image: circleci/node:8 10 | working_directory: ~/repo 11 | steps: 12 | - checkout 13 | # Download and cache dependencies 14 | - restore_cache: 15 | keys: 16 | - v1-dependencies-{{ checksum "package.json" }} 17 | # fallback to using the latest cache if no exact match is found 18 | - v1-dependencies- 19 | - run: yarn install 20 | - save_cache: 21 | paths: 22 | - node_modules 23 | key: v1-dependencies-{{ checksum "package.json" }} 24 | - run: 25 | name: ganache-cli 26 | command: yarn test:ganache 27 | background: true 28 | - run: CI=true yarn test --maxWorkers=2 29 | -------------------------------------------------------------------------------- /test/helpers/MockRegulatorService.sol: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | pragma solidity ^0.4.18; 18 | 19 | import '../../contracts/RegulatorService.sol'; 20 | 21 | contract MockRegulatorService is RegulatorService { 22 | bool public success; 23 | uint8 public reason; 24 | 25 | function setCheckResult(bool _success, uint8 _reason) public { 26 | success = _success; 27 | reason = _reason; 28 | } 29 | 30 | function check(address, address, address, address, uint256) public returns (uint8) { 31 | return reason; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const Web3 = require('web3'); 18 | const web3 = new Web3(); 19 | const HDWalletProvider = require('truffle-hdwallet-provider'); 20 | 21 | const kovanMnemonic = process.env['RTOKEN_KOVAN_MNEMONIC']; 22 | const infuraKey = process.env['INFURA_KEY']; 23 | 24 | module.exports = { 25 | networks: { 26 | development: { 27 | host: "localhost", 28 | port: 8545, 29 | network_id: "*" // Match any network id 30 | }, 31 | kovan: { 32 | provider: new HDWalletProvider(kovanMnemonic, `https://kovan.infura.io/${infuraKey}`), 33 | network_id: '42' 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | var RegulatedToken = artifacts.require("./RegulatedToken.sol"), 18 | ServiceRegistry = artifacts.require("./ServiceRegistry.sol"), 19 | TokenRegulatorService = artifacts.require("./TokenRegulatorService.sol"); 20 | 21 | module.exports = async function(deployer) { 22 | 23 | deployer.deploy(TokenRegulatorService).then(async () => { 24 | const regulator = await TokenRegulatorService.deployed(); 25 | return deployer.deploy(ServiceRegistry, regulator.address); 26 | }).then(async () => { 27 | const registry = await ServiceRegistry.deployed(); 28 | return deployer.deploy(RegulatedToken, registry.address, "Example", "EXPL"); 29 | }).then(async () => { 30 | const token = await RegulatedToken.deployed(); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /test/helpers/MockRegulatedToken.sol: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | pragma solidity ^0.4.18; 18 | 19 | import '../../contracts/RegulatedToken.sol'; 20 | import '../../contracts/RegulatorService.sol'; 21 | 22 | contract MockRegulatedToken is RegulatedToken { 23 | RegulatorService public service; 24 | uint public decimals; 25 | 26 | // 0xffffffff is a test address for ServiceRegistry that is bypassed by our _service() implementation 27 | function MockRegulatedToken(address _service) public 28 | RegulatedToken(ServiceRegistry(0xffffffff), "MockToken", "MTKN") 29 | { 30 | service = RegulatorService(_service); 31 | } 32 | 33 | function setDecimals(uint _decimals) public { 34 | decimals = _decimals; 35 | } 36 | 37 | function _service() constant public returns (RegulatorService) { 38 | return service; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/TestRegulatedToken.sol: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | pragma solidity ^0.4.18; 18 | 19 | import './RegulatedToken.sol'; 20 | 21 | /** 22 | * TestRegulatedToken is a RegulatedToken meant for testing purposes. 23 | * Developers can mint an unlimited number of TestRegulatedTokens. 24 | * TestRegulatedToken is meant to be instantiated with a ServiceRegistry 25 | * that points to an instance of TestRegulatorService. 26 | */ 27 | contract TestRegulatedToken is RegulatedToken { 28 | function TestRegulatedToken(ServiceRegistry _registry, string _name, string _symbol) public 29 | RegulatedToken(_registry, _name, _symbol) 30 | { 31 | 32 | } 33 | 34 | /** 35 | * Override zeppelin.MintableToken.mint() without onlyOwner or canMint modifiers. 36 | */ 37 | function mint(address _to, uint256 _amount) public returns (bool) { 38 | totalSupply = totalSupply.add(_amount); 39 | balances[_to] = balances[_to].add(_amount); 40 | Mint(_to, _amount); 41 | Transfer(address(0), _to, _amount); 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /contracts/RegulatorService.sol: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | pragma solidity ^0.4.18; 18 | 19 | /// @notice Standard interface for `RegulatorService`s 20 | contract RegulatorService { 21 | 22 | /* 23 | * @notice This method *MUST* be called by `RegulatedToken`s during `transfer()` and `transferFrom()`. 24 | * The implementation *SHOULD* check whether or not a transfer can be approved. 25 | * 26 | * @dev This method *MAY* call back to the token contract specified by `_token` for 27 | * more information needed to enforce trade approval. 28 | * 29 | * @param _token The address of the token to be transfered 30 | * @param _spender The address of the spender of the token 31 | * @param _from The address of the sender account 32 | * @param _to The address of the receiver account 33 | * @param _amount The quantity of the token to trade 34 | * 35 | * @return uint8 The reason code: 0 means success. Non-zero values are left to the implementation 36 | * to assign meaning. 37 | */ 38 | function check(address _token, address _spender, address _from, address _to, uint256 _amount) public returns (uint8); 39 | } 40 | -------------------------------------------------------------------------------- /migrations/3_deploy_test_tokens.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const ServiceRegistry = artifacts.require("./ServiceRegistry.sol"); 18 | const TestRegulatorService = artifacts.require("./TestRegulatorService.sol"); 19 | const TestRegulatedToken = artifacts.require("./TestRegulatedToken.sol"); 20 | 21 | module.exports = async function(deployer) { 22 | const log = deployer.logger.log; 23 | try { 24 | await deployer.deploy(TestRegulatorService); 25 | const regulator = await TestRegulatorService.deployed(); 26 | 27 | await deployer.deploy(ServiceRegistry, regulator.address, {overwrite: false}); 28 | const registry = await ServiceRegistry.deployed(); 29 | 30 | const copperToken = await deployer.new(TestRegulatedToken, registry.address, "Copper Token", "CPPR") 31 | log('copperToken.address ' + copperToken.address); 32 | 33 | const silverToken = await deployer.new(TestRegulatedToken, registry.address, "Silver Token", "SLVR") 34 | log('silverToken.address ' + silverToken.address); 35 | 36 | const goldToken = await deployer.new(TestRegulatedToken, registry.address, "Gold Token", "GOLD") 37 | log('goldToken.address ' + goldToken.address); 38 | } 39 | catch (e) { 40 | log(e); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /contracts/ServiceRegistry.sol: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | pragma solidity ^0.4.18; 18 | 19 | import './RegulatorService.sol'; 20 | import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; 21 | 22 | /// @notice A service that points to a `RegulatorService` 23 | contract ServiceRegistry is Ownable { 24 | address public service; 25 | 26 | /** 27 | * @notice Triggered when service address is replaced 28 | */ 29 | event ReplaceService(address oldService, address newService); 30 | 31 | /** 32 | * @dev Validate contract address 33 | * Credit: https://github.com/Dexaran/ERC223-token-standard/blob/Recommended/ERC223_Token.sol#L107-L114 34 | * 35 | * @param _addr The address of a smart contract 36 | */ 37 | modifier withContract(address _addr) { 38 | uint length; 39 | assembly { length := extcodesize(_addr) } 40 | require(length > 0); 41 | _; 42 | } 43 | 44 | /** 45 | * @notice Constructor 46 | * 47 | * @param _service The address of the `RegulatorService` 48 | * 49 | */ 50 | function ServiceRegistry(address _service) public { 51 | service = _service; 52 | } 53 | 54 | /** 55 | * @notice Replaces the address pointer to the `RegulatorService` 56 | * 57 | * @dev This method is only callable by the contract's owner 58 | * 59 | * @param _service The address of the new `RegulatorService` 60 | */ 61 | function replaceService(address _service) onlyOwner withContract(_service) public { 62 | address oldService = service; 63 | service = _service; 64 | ReplaceService(oldService, service); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /contracts/TestRegulatorService.sol: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | pragma solidity ^0.4.18; 18 | 19 | import './RegulatorService.sol'; 20 | 21 | /** 22 | * TestRegulatorService is a RegulatorService meant for testing purposes. 23 | * It returns check() reason codes based on the amount of the transaction. 24 | */ 25 | contract TestRegulatorService is RegulatorService { 26 | uint8 constant private SUCCESS = 0; /* 0 <= AMOUNT < 10 */ 27 | uint8 constant private ELOCKED = 1; /* 10 <= AMOUNT < 20 */ 28 | uint8 constant private EDIVIS = 2; /* 20 <= AMOUNT < 30 */ 29 | uint8 constant private ESEND = 3; /* 30 <= AMOUNT < 40 */ 30 | uint8 constant private ERECV = 4; /* 40 <= AMOUNT < 50 */ 31 | 32 | /** 33 | * @notice Checks whether or not a trade should be approved by checking the trade amount 34 | * 35 | * @param _token The address of the token to be transfered 36 | * @param _spender The address of the spender of the token (unused in this implementation) 37 | * @param _from The address of the sender account 38 | * @param _to The address of the receiver account 39 | * @param _amount The quantity of the token to trade 40 | * 41 | * @return `true` if the trade should be approved and `false` if the trade should not be approved 42 | */ 43 | function check(address _token, address _spender, address _from, address _to, uint256 _amount) public returns (uint8) { 44 | if (_amount >= 10 && _amount < 20) { 45 | return ELOCKED; 46 | } 47 | else if (_amount >= 20 && _amount < 30) { 48 | return EDIVIS; 49 | } 50 | else if (_amount >= 30 && _amount < 40) { 51 | return ESEND; 52 | } 53 | else if (_amount >= 40 && _amount < 50) { 54 | return ERECV; 55 | } 56 | return SUCCESS; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/TestRegulatedToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const TestRegulatedToken = artifacts.require('./TestRegulatedToken.sol'); 18 | const ServiceRegistry = artifacts.require('./ServiceRegistry.sol'); 19 | const TestRegulatorService = artifacts.require('./TestRegulatorService.sol'); 20 | 21 | contract('TestRegulatedToken', async function(accounts) { 22 | let regulator; 23 | let token; 24 | const owner = accounts[0]; // Owner of the TestRegulatedToken 25 | const notOwner = accounts[1]; // Not the owner of the TestRegulatedToken 26 | const fromOwner = { from: owner }; 27 | const fromNotOwner = { from: notOwner }; 28 | 29 | beforeEach(async () => { 30 | regulator = await TestRegulatorService.new(fromOwner); 31 | 32 | const registry = await ServiceRegistry.new(regulator.address); 33 | 34 | token = await TestRegulatedToken.new(registry.address, 'Test', 'TEST'); 35 | }); 36 | 37 | describe('minting', () => { 38 | beforeEach(async () => { 39 | assert.equal((await token.owner.call()).valueOf(), owner); 40 | assert.equal((await token.balanceOf.call(owner)).valueOf(), 0); 41 | assert.equal((await token.balanceOf.call(notOwner)).valueOf(), 0); 42 | }); 43 | 44 | it('lets people other than the token owner mint', async () => { 45 | await token.mint(notOwner, 100, fromNotOwner); 46 | assert.equal((await token.balanceOf.call(notOwner)).valueOf(), 100); 47 | }); 48 | 49 | it('lets people mint after finishMinting()', async () => { 50 | await token.finishMinting(); 51 | // Owner can still mint 52 | await token.mint(owner, 100, fromOwner); 53 | assert.equal((await token.balanceOf.call(owner)).valueOf(), 100); 54 | // NotOwner can still mint 55 | await token.mint(notOwner, 200, fromNotOwner); 56 | assert.equal((await token.balanceOf.call(notOwner)).valueOf(), 200); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/expectThrow.js 18 | exports.expectThrow = async promise => { 19 | try { 20 | await promise; 21 | } catch (error) { 22 | // TODO: Check jump destination to destinguish between a throw 23 | // and an actual invalid jump. 24 | const invalidOpcode = error.message.search('invalid opcode') >= 0; 25 | // TODO: When we contract A calls contract B, and B throws, instead 26 | // of an 'invalid jump', we get an 'out of gas' error. How do 27 | // we distinguish this from an actual out of gas event? (The 28 | // testrpc log actually show an 'invalid jump' event.) 29 | const outOfGas = error.message.search('out of gas') >= 0; 30 | const revert = error.message.search('revert') >= 0; 31 | assert(invalidOpcode || outOfGas || revert, "Expected throw, got '" + error + "' instead"); 32 | return; 33 | } 34 | assert.fail('Expected throw not received'); 35 | }; 36 | 37 | exports.assertEvent = (event, args, assertEqual = assert.deepEqual, timeout = 3000) => { 38 | return new Promise((resolve, reject) => { 39 | const t = setTimeout(() => { 40 | reject(new Error('Timeout while waiting for event')); 41 | }, timeout); 42 | 43 | event.watch((error, response) => { 44 | try { 45 | assertEqual(response.args, args, 'Event argument mismatch'); 46 | resolve(response); 47 | } finally { 48 | clearTimeout(t); 49 | event.stopWatching(); 50 | } 51 | }); 52 | }); 53 | }; 54 | 55 | /** 56 | * @return true if the regulator will allow the trade. 57 | */ 58 | exports.checkResult = async (regulator, tokenAddress, spender, sender, receiver, amount) => { 59 | const reason = await regulator.check.call(tokenAddress, spender, sender, receiver, amount); 60 | return reason == 0; 61 | }; 62 | 63 | /** 64 | * Assert all addresses have the given balance for the given token. 65 | * @param token The token to check balances for. 66 | * @param balances Object where keys are addresses and values are the expected number of tokens for that address. 67 | */ 68 | exports.assertBalances = async (token, balances) => { 69 | for (let addr in balances) { 70 | const expectedBal = balances[addr]; 71 | const actualBal = await token.balanceOf.call(addr); 72 | assert.equal(actualBal.toNumber(), expectedBal); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /test/ServiceRegistry.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const helpers = require('./helpers'); 18 | const RegulatedToken = artifacts.require('./RegulatedToken.sol'); 19 | const ServiceRegistry = artifacts.require('./ServiceRegistry.sol'); 20 | const MockRegulatorService = artifacts.require('../test/helpers/MockRegulatorService.sol'); 21 | 22 | contract('ServiceRegistry', async accounts => { 23 | let owner; 24 | let newOwner; 25 | let hacker; 26 | let participant; 27 | 28 | let service; 29 | let registry; 30 | 31 | beforeEach(async () => { 32 | owner = accounts[0]; 33 | newOwner = accounts[1]; 34 | hacker = accounts[2]; 35 | participant = accounts[3]; 36 | 37 | service = await MockRegulatorService.new({ from: owner }); 38 | registry = await ServiceRegistry.new(service.address, { from: owner }); 39 | }); 40 | 41 | describe('ownership', () => { 42 | it('allows ownership transfer', async () => { 43 | await helpers.expectThrow(registry.transferOwnership(newOwner, { from: hacker })); 44 | await registry.transferOwnership(newOwner, { from: owner }); 45 | 46 | await helpers.expectThrow(registry.transferOwnership(hacker, { from: owner })); 47 | await registry.transferOwnership(hacker, { from: newOwner }); 48 | }); 49 | }); 50 | 51 | describe('replaceService', () => { 52 | let newService; 53 | 54 | beforeEach(async () => { 55 | assert.equal(await registry.owner(), owner); 56 | assert.equal(await registry.service(), service.address); 57 | 58 | newService = await MockRegulatorService.new({ from: owner }); 59 | }); 60 | 61 | it('should allow the owner to replace the service with a contract', async () => { 62 | const event = registry.ReplaceService(); 63 | 64 | await registry.replaceService(newService.address); 65 | assert.equal(await registry.service(), newService.address); 66 | 67 | await helpers.assertEvent(event, { 68 | oldService: service.address, 69 | newService: newService.address, 70 | }); 71 | }); 72 | 73 | it('should NOT allow an invalid address', async () => { 74 | await helpers.expectThrow(registry.replaceService(participant)); 75 | await helpers.expectThrow(registry.replaceService(0)); 76 | assert.equal(await registry.service(), service.address); 77 | }); 78 | 79 | it('should NOT allow anybody except for the owner to replace the service', async () => { 80 | await helpers.expectThrow(registry.replaceService(newService.address, { from: hacker })); 81 | assert.equal(await registry.service(), service.address); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/TestRegulatorService.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const TestRegulatedToken = artifacts.require('./TestRegulatedToken.sol'); 18 | const ServiceRegistry = artifacts.require('./ServiceRegistry.sol'); 19 | const TestRegulatorService = artifacts.require('./TestRegulatorService.sol'); 20 | const checkResult = require('./helpers').checkResult; 21 | const helperAssertBalances = require('./helpers').assertBalances; 22 | 23 | 24 | const SUCCESS = 0; /* 0 <= AMOUNT < 10 */ 25 | const ELOCKED = 1; /* 10 <= AMOUNT < 20 */ 26 | const EDIVIS = 2; /* 20 <= AMOUNT < 30 */ 27 | const ESEND = 3; /* 30 <= AMOUNT < 40 */ 28 | const ERECV = 4; /* 40 <= AMOUNT < 50 */ 29 | 30 | contract('TestRegulatorService', async accounts => { 31 | const owner = accounts[0]; 32 | const spender = owner; 33 | const receiver = accounts[1]; 34 | const fromOwner = { from: owner }; 35 | 36 | let regulator; 37 | let token; 38 | let assertBalances; 39 | 40 | beforeEach(async () => { 41 | regulator = await TestRegulatorService.new({ from: owner }); 42 | const registry = await ServiceRegistry.new(regulator.address); 43 | token = await TestRegulatedToken.new(registry.address, 'Test', 'TEST'); 44 | }); 45 | 46 | describe('check', () => { 47 | it('returns SUCCESS when amount is between zero and ten', async () => { 48 | assert.equal(await regulator.check.call(token.address, spender, owner, receiver, 1), SUCCESS); 49 | }); 50 | 51 | it('returns ELOCKED when amount is between ten and twenty', async () => { 52 | assert.equal(await regulator.check.call(token.address, spender, owner, receiver, 11), ELOCKED); 53 | }); 54 | 55 | it('returns EDIVIS when amount is between twenty and thirty', async () => { 56 | assert.equal(await regulator.check.call(token.address, spender, owner, receiver, 21), EDIVIS); 57 | }); 58 | 59 | it('returns ESEND when amount is between thirty and forty', async () => { 60 | assert.equal(await regulator.check.call(token.address, spender, owner, receiver, 31), ESEND); 61 | }); 62 | 63 | it('returns ERECV when amount is between forty and fifty', async () => { 64 | assert.equal(await regulator.check.call(token.address, spender, owner, receiver, 41), ERECV); 65 | }); 66 | }); 67 | 68 | describe('TestRegulatedToken.transfer() with TestRegulatorService.check()', () => { 69 | beforeEach(async () => { 70 | assertBalances = async (ownerBal, receiverBal) => { 71 | await helperAssertBalances(token, { [owner]: ownerBal, [receiver]: receiverBal }); 72 | } 73 | 74 | await token.mint(owner, 100); 75 | await token.finishMinting(); 76 | 77 | await assertBalances(100, 0); 78 | }); 79 | 80 | describe('when the transfer is simulated to be NOT APPROVED by the regulator(by entering a transfer amount from 10-50)', () => { 81 | beforeEach(async () => { 82 | assert.isFalse(await checkResult(regulator, token.address, owner, owner, receiver, 15)); 83 | }); 84 | 85 | it('does NOT transfer tokens', async () => { 86 | await token.transfer(receiver, 15, fromOwner); 87 | await assertBalances(100, 0); 88 | }); 89 | }); 90 | 91 | describe('when the transfer is simulated to be APPROVED by the regulator(by entering a transfer amount from 0-10)', () => { 92 | beforeEach(async () => { 93 | assert.isTrue(await checkResult(regulator, token.address, owner, owner, receiver, 5)); 94 | }); 95 | 96 | it('transfers tokens', async () => { 97 | await token.transfer(receiver, 5, fromOwner); 98 | await assertBalances(95, 5); 99 | }); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /contracts/RegulatedToken.sol: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | pragma solidity ^0.4.18; 18 | 19 | import 'zeppelin-solidity/contracts/token/DetailedERC20.sol'; 20 | import 'zeppelin-solidity/contracts/token/MintableToken.sol'; 21 | import './ServiceRegistry.sol'; 22 | import './RegulatorService.sol'; 23 | 24 | /// @notice An ERC-20 token that has the ability to check for trade validity 25 | contract RegulatedToken is DetailedERC20, MintableToken { 26 | 27 | /** 28 | * @notice R-Token decimals setting (used when constructing DetailedERC20) 29 | */ 30 | uint8 constant public RTOKEN_DECIMALS = 18; 31 | 32 | /** 33 | * @notice Triggered when regulator checks pass or fail 34 | */ 35 | event CheckStatus(uint8 reason, address indexed spender, address indexed from, address indexed to, uint256 value); 36 | 37 | /** 38 | * @notice Address of the `ServiceRegistry` that has the location of the 39 | * `RegulatorService` contract responsible for checking trade 40 | * permissions. 41 | */ 42 | ServiceRegistry public registry; 43 | 44 | /** 45 | * @notice Constructor 46 | * 47 | * @param _registry Address of `ServiceRegistry` contract 48 | * @param _name Name of the token: See DetailedERC20 49 | * @param _symbol Symbol of the token: See DetailedERC20 50 | */ 51 | function RegulatedToken(ServiceRegistry _registry, string _name, string _symbol) public 52 | DetailedERC20(_name, _symbol, RTOKEN_DECIMALS) 53 | { 54 | require(_registry != address(0)); 55 | 56 | registry = _registry; 57 | } 58 | 59 | /** 60 | * @notice ERC-20 overridden function that include logic to check for trade validity. 61 | * 62 | * @param _to The address of the receiver 63 | * @param _value The number of tokens to transfer 64 | * 65 | * @return `true` if successful and `false` if unsuccessful 66 | */ 67 | function transfer(address _to, uint256 _value) public returns (bool) { 68 | if (_check(msg.sender, _to, _value)) { 69 | return super.transfer(_to, _value); 70 | } else { 71 | return false; 72 | } 73 | } 74 | 75 | /** 76 | * @notice ERC-20 overridden function that include logic to check for trade validity. 77 | * 78 | * @param _from The address of the sender 79 | * @param _to The address of the receiver 80 | * @param _value The number of tokens to transfer 81 | * 82 | * @return `true` if successful and `false` if unsuccessful 83 | */ 84 | function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { 85 | if (_check(_from, _to, _value)) { 86 | return super.transferFrom(_from, _to, _value); 87 | } else { 88 | return false; 89 | } 90 | } 91 | 92 | /** 93 | * @notice Performs the regulator check 94 | * 95 | * @dev This method raises a CheckStatus event indicating success or failure of the check 96 | * 97 | * @param _from The address of the sender 98 | * @param _to The address of the receiver 99 | * @param _value The number of tokens to transfer 100 | * 101 | * @return `true` if the check was successful and `false` if unsuccessful 102 | */ 103 | function _check(address _from, address _to, uint256 _value) private returns (bool) { 104 | var reason = _service().check(this, msg.sender, _from, _to, _value); 105 | 106 | CheckStatus(reason, msg.sender, _from, _to, _value); 107 | 108 | return reason == 0; 109 | } 110 | 111 | /** 112 | * @notice Retreives the address of the `RegulatorService` that manages this token. 113 | * 114 | * @dev This function *MUST NOT* memoize the `RegulatorService` address. This would 115 | * break the ability to upgrade the `RegulatorService`. 116 | * 117 | * @return The `RegulatorService` that manages this token. 118 | */ 119 | function _service() constant public returns (RegulatorService) { 120 | return RegulatorService(registry.service()); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /scripts/mint_test_tokens.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /******************************************************************************* 18 | * What: 19 | * Mints TestRegulatorTokens to specified wallet address. 20 | * 21 | * Usage: 22 | * node scripts/mint_test_tokens.js env numberOfTokens tokenSymbol toWalletAddress 23 | * 24 | * Example: 25 | * node scripts/mint_test_tokens.js development 100 CPPR 0xd4afd5525ada1efa8ae661be1a0b373eb9e68498 26 | ******************************************************************************/ 27 | 28 | console.log('STARTING'); 29 | const Web3 = require('web3'); 30 | const TestRegulatedTokenJson = require('../build/contracts/TestRegulatedToken'); 31 | const BigNumber = require('bignumber.js'); 32 | const HDWalletProvider = require('truffle-hdwallet-provider'); 33 | 34 | /******************************************************************************* 35 | * Input validation & Init 36 | ******************************************************************************/ 37 | const ensure = (condition, msg) => { 38 | if (!condition) { 39 | console.log('ERROR:', msg); 40 | process.exit(1); 41 | } 42 | }; 43 | 44 | const env = process.argv[2]; 45 | console.log('env:', env); 46 | ensure(env, 'No env'); 47 | 48 | const numberOfTokens = process.argv[3]; 49 | console.log('numberOfTokens:', numberOfTokens); 50 | ensure(numberOfTokens, 'No numberOfTokens'); 51 | 52 | const tokenSymbol = process.argv[4]; 53 | console.log('tokenSymbol:', tokenSymbol); 54 | ensure(tokenSymbol, 'No tokenSymbol'); 55 | 56 | const mintToWallet = process.argv[5]; 57 | console.log('mintToWallet:', mintToWallet); 58 | ensure(mintToWallet, 'No mintToWallet'); 59 | 60 | const configs = { 61 | development: { 62 | tokenAddresses: { 63 | 'CPPR': '0x8ef59034b52138fbcb1d36607fb5dd665fc7bc1c', 64 | 'SLVR': '0x6d966540214a7a55f61244e346d0ae6033ace4f8', 65 | 'GOLD': '0x3b6b17477763ccb313a55e87fd104c37e3e01f33' 66 | } 67 | }, 68 | kovan: { 69 | tokenAddresses: { 70 | 'CPPR': '0x5cdcc989560693e845060d271399216d8ab00c25', 71 | 'SLVR': '0x9843430d81290c90701bd655a7f19222b1de1f5f', 72 | 'GOLD': '0x88c3f6e2ddd8e65ec548197f2670623c5d876459' 73 | } 74 | } 75 | }; 76 | 77 | const config = configs[env]; 78 | ensure(config, 'No config'); 79 | 80 | const tokenAddress = config.tokenAddresses[tokenSymbol]; 81 | ensure(tokenAddress, 'No tokenAddress'); 82 | 83 | let provider; 84 | if (env === 'kovan') { 85 | const kovanMnemonic = process.env['RTOKEN_KOVAN_MNEMONIC']; 86 | ensure(kovanMnemonic, 'No kovanMnemonic'); 87 | const infuraKey = process.env['INFURA_KEY']; 88 | ensure(infuraKey, 'No infuraKey'); 89 | provider = new HDWalletProvider(kovanMnemonic, `https://kovan.infura.io/${infuraKey}`); 90 | } else { 91 | provider = new Web3.providers.HttpProvider('http://localhost:8545'); 92 | } 93 | const web3 = new Web3(provider); 94 | 95 | /******************************************************************************* 96 | * Actually Mint 97 | ******************************************************************************/ 98 | ;(async () => { 99 | try { 100 | const myAddress = (await web3.eth.getAccounts())[0]; 101 | console.log('myAddress:', myAddress); 102 | 103 | const ethBalance = await web3.eth.getBalance(myAddress); 104 | console.log('myAddress.eth.balance:', web3.utils.fromWei(ethBalance, 'ether')); 105 | 106 | var token = await new web3.eth.Contract(TestRegulatedTokenJson.abi, tokenAddress, {from: myAddress, gas: 5000000}); 107 | 108 | const toMintAmount = web3.utils.toWei(new BigNumber(numberOfTokens).toString(), 'ether'); 109 | console.log(`mintToWallet.${tokenSymbol}.toMintAmount:`, toMintAmount); 110 | 111 | const transaction = await token.methods.mint(mintToWallet, toMintAmount).send(); 112 | console.log('transaction:', transaction.status, transaction.transactionHash); 113 | 114 | const tokenBalance = await token.methods.balanceOf(mintToWallet).call({from: myAddress}) 115 | console.log(`mintToWallet.${tokenSymbol}.balance:`, tokenBalance); 116 | console.log('DONE'); 117 | } 118 | catch (e) { 119 | console.log(e); 120 | } 121 | })(); 122 | -------------------------------------------------------------------------------- /test/RegulatedToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const BigNumber = require('bignumber.js'); 18 | 19 | const helpers = require('./helpers'); 20 | const RegulatedToken = artifacts.require('./RegulatedToken.sol'); 21 | const ServiceRegistry = artifacts.require('./ServiceRegistry.sol'); 22 | const MockRegulatorService = artifacts.require('../test/helpers/MockRegulatorService.sol'); 23 | 24 | contract('RegulatedToken', async function(accounts) { 25 | let regulator; 26 | let token; 27 | let assertBalances; 28 | 29 | const owner = accounts[0]; 30 | const receiver = accounts[1]; 31 | 32 | const fromOwner = { from: owner }; 33 | const fromReceiver = { from: receiver }; 34 | 35 | beforeEach(async () => { 36 | regulator = await MockRegulatorService.new({ from: owner }); 37 | 38 | const registry = await ServiceRegistry.new(regulator.address); 39 | 40 | token = await RegulatedToken.new(registry.address, 'Test', 'TEST'); 41 | 42 | assertBalances = async (ownerBal, receiverBal) => { 43 | await helpers.assertBalances(token, { [owner]: ownerBal, [receiver]: receiverBal }); 44 | } 45 | 46 | await token.mint(owner, 100); 47 | await token.finishMinting(); 48 | 49 | await assertBalances(100, 0); 50 | }); 51 | 52 | const assertCheckStatusEvent = async (event, params) => { 53 | const p = Object.assign({}, params, { 54 | reason: new BigNumber(params.reason), 55 | value: new BigNumber(params.value), 56 | }); 57 | 58 | return helpers.assertEvent(event, p, (expected, actual) => { 59 | assert.equal(expected.reason.valueOf(), actual.reason.valueOf()); 60 | assert.equal(expected.spender, actual.spender); 61 | assert.equal(expected.from, actual.from); 62 | assert.equal(expected.to, actual.to); 63 | assert.equal(expected.value.valueOf(), actual.value.valueOf()); 64 | }); 65 | }; 66 | 67 | describe('constructor', () => { 68 | it('requires a non-zero registry argument', async () => { 69 | await helpers.expectThrow(RegulatedToken.new(0, 'TEST', 'Test')); 70 | }); 71 | }); 72 | 73 | describe('transfer', () => { 74 | describe('when the transfer is NOT approved by the regulator', () => { 75 | beforeEach(async () => { 76 | await regulator.setCheckResult(false, 255); 77 | assert.isFalse(await helpers.checkResult(regulator, token.address, owner, owner, receiver, 0)); 78 | await assertBalances(100, 0); 79 | }); 80 | 81 | it('returns false', async () => { 82 | assert.isFalse(await token.transfer.call(receiver, 100, fromOwner)); 83 | await assertBalances(100, 0); 84 | }); 85 | 86 | it('triggers a CheckStatus event and does NOT transfer funds', async () => { 87 | const event = token.CheckStatus(); 88 | const value = 25; 89 | 90 | await token.transfer(receiver, value, fromOwner); 91 | await assertBalances(100, 0); 92 | await assertCheckStatusEvent(event, { 93 | reason: 255, 94 | spender: owner, 95 | from: owner, 96 | to: receiver, 97 | value, 98 | }); 99 | }); 100 | }); 101 | 102 | describe('when the transfer is approved by the regulator', () => { 103 | beforeEach(async () => { 104 | await regulator.setCheckResult(true, 0); 105 | assert.isTrue(await helpers.checkResult(regulator, token.address, owner, owner, receiver, 0)); 106 | await assertBalances(100, 0); 107 | }); 108 | 109 | it('returns true', async () => { 110 | assert.isTrue(await token.transfer.call(receiver, 100, fromOwner)); 111 | // note: calls don't modify state 112 | await assertBalances(100, 0); 113 | }); 114 | 115 | it('triggers a CheckStatus event and transfers funds', async () => { 116 | const event = token.CheckStatus(); 117 | const value = 25; 118 | 119 | await token.transfer(receiver, value, fromOwner); 120 | await assertBalances(75, value); 121 | await assertCheckStatusEvent(event, { 122 | reason: 0, 123 | spender: owner, 124 | from: owner, 125 | to: receiver, 126 | value, 127 | }); 128 | }); 129 | }); 130 | }); 131 | 132 | describe('transferFrom', () => { 133 | describe('when the transfer is NOT approved by the regulator', () => { 134 | beforeEach(async () => { 135 | await regulator.setCheckResult(false, 255); 136 | 137 | assert.isFalse(await helpers.checkResult(regulator, token.address, owner, owner, receiver, 0)); 138 | await token.approve(receiver, 25, fromOwner); 139 | await assertBalances(100, 0); 140 | }); 141 | 142 | it('returns false', async () => { 143 | assert.isFalse(await token.transferFrom.call(owner, receiver, 20, fromReceiver)); 144 | await assertBalances(100, 0); 145 | }); 146 | 147 | it('triggers a CheckStatus event and does NOT transfer funds', async () => { 148 | const event = token.CheckStatus(); 149 | const value = 25; 150 | 151 | await token.transferFrom(owner, receiver, value, fromOwner); 152 | 153 | await assertBalances(100, 0); 154 | await assertCheckStatusEvent(event, { 155 | reason: 255, 156 | spender: owner, 157 | from: owner, 158 | to: receiver, 159 | value, 160 | }); 161 | }); 162 | }); 163 | 164 | describe('when the transfer is approved by the regulator', () => { 165 | beforeEach(async () => { 166 | await regulator.setCheckResult(true, 0); 167 | 168 | assert.isTrue(await helpers.checkResult(regulator, token.address, owner, owner, receiver, 0)); 169 | await token.approve(receiver, 25, fromOwner); 170 | await assertBalances(100, 0); 171 | }); 172 | 173 | it('returns true', async () => { 174 | assert.isTrue(await token.transferFrom.call(owner, receiver, 25, fromReceiver)); 175 | // note: calls don't modify state 176 | await assertBalances(100, 0); 177 | }); 178 | 179 | it('triggers a CheckStatus event and transfers funds', async () => { 180 | const event = token.CheckStatus(); 181 | const value = 20; 182 | 183 | await token.transferFrom(owner, receiver, 20, fromReceiver); 184 | await assertBalances(80, 20); 185 | await assertCheckStatusEvent(event, { 186 | reason: 0, 187 | spender: receiver, 188 | from: owner, 189 | to: receiver, 190 | value, 191 | }); 192 | 193 | await token.transferFrom(owner, receiver, 5, fromReceiver); 194 | await assertBalances(75, 25); 195 | }); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Regulated Token System (R-Token) 2 | 3 | Smart Contracts for applying regulatory compliance to tokenized securities issuance and trading 4 | 5 | ## Feedback 6 | 7 | We would love your feedback. Have questions, want to contribute, or have general comments? Ping the team and join the conversation at https://gitter.im/harborhq. 8 | 9 | ## Description 10 | 11 | R-Token is a permissioned token on the Ethereum blockchain, enabling token transfers to occur if and only if they are approved by an on-chain Regulator Service. The Regulator Service can be configured to meet relevant securities regulations, Know Your Customer (KYC) policies, Anti-Money Laundering (AML) requirements, tax laws, and more. Implemented with the correct configurations, the R-Token standard makes compliant transfers possible, both on exchanges and person to person, in ICOs and secondary trades, and across jurisdictions. R-Token enables ERC-20 tokens to be used for regulated securities. 12 | 13 | ## How It Works 14 | 15 | R-Token implements ERC-20 methods `transfer()` and `transferFrom()` with an additional check to determine whether or not a transfer should be allowed to proceed. The implementation of `check()` can take many forms, but a default whitelist approach is implemented by `TokenRegulatorService`. Token and participant-level permissions, when used in different combinations, can be used to satisfy multiple regulatory exemptions. The `ServiceRegistry` is included as a mechanism to facilitate upgrading the R-Token check logic as rules change over time. 16 | 17 | ## Components 18 | 19 | * RegulatedToken 20 | * Permissioned ERC-20 smart contract representing ownership of securities 21 | * Compatible with existing wallets and exchanges that support the ERC-20 token standard 22 | * Overrides the existing ERC-20 transfer method to check with an on-chain Regulator Service for trade approval 23 | * RegulatorService 24 | * Contains the permissions necessary for regulatory compliance 25 | * Relies on off-chain trade approver to set and update permissions 26 | * ServiceRegistry 27 | * Accounts for regulatory requirement changes over time 28 | * Routes the R-Token to the correct version of the Regulator Service 29 | 30 | 31 |

32 | 33 |

34 | 35 | ## Features 36 | 37 | Upgradable, token-level trade permission and participant-level trade permissions. 38 | 39 | * Configurable without code modification and need for more security auditing 40 | * Upgradable so an owner/admin can change business logic as rules evolve over time 41 | * An owner/admin can lock/unlock trading for a period of time 42 | * An owner/admin can whitelist/blacklist partial token transfers 43 | * An owner/admin can qualify/disqualify a trade participant from sending tokens 44 | * An owner/admin can qualify/disqualify a trade participant from receiving tokens 45 | 46 | ### Upgradable 47 | 48 | The `ServiceRegistry` is used to point many `RegulatedToken` smart contracts to a single `RegulatorService`. This setup is recommended so that rules and logic implemented by the `RegulatorService` can be upgraded by changing a single `RegulatorService` address held by the `ServiceRegistry`. 49 | 50 |

51 | 52 |

53 | 54 | 55 | When `RegulatorService` logic needs to be updated, the migration path resembles a process like this: 56 | 57 | 1. Deploy new RegulatorService (V2) 58 | 2. Copy required state from Regulator Service V1 to RegulatorService V2 59 | 3. Call `replaceService()` on `ServiceRegistry` with address pointing to RegulatorService V2 60 | 61 | ### Token/Participant Level Permissions 62 | 63 | In the `TokenRegulatorService` implementation of the `RegulatorService` interface, there are token level permissions and participant level permissions. These permissions should be updated by an off-chain process like shown below: 64 | 65 |

66 | 67 |

68 | 69 | Token-level permissions include: 70 | 71 | * `locked` - controls locking and unlocking of all token trades for a particular token 72 | * `partialAmounts` - allows or disallows transfers of partial token amounts 73 | 74 | Participant-level permissions include: 75 | 76 | * `PERM_SEND` - permission for a participant to send a token to another account 77 | * `PERM_RECV` - permission for a participant to receive a token from another account 78 | 79 | ## Administrative Roles / Contract Ownership 80 | 81 | Administrative privileges on R-Token smart contracts are divided into two roles: `Owner` and `Admin`. We will continue to decentralize administration in future versions. 82 | 83 | The privileges for each role are defined below: 84 | 85 | | | RegulatedToken | ServiceRegistry | RegulatorService | 86 | |:-----------|:--------------- |:-------------------------------- |:----------------------------------------------------------------- | 87 | | Owner | Can Mint | Transfer Owner / Replace Service | Update Token-Level Settings / Transfer Ownership / Transfer Admin | 88 | | Admin | N/A | N/A | Update Participant-Level Settings | 89 | 90 | # Developers 91 | The R-Token project was created with [Truffle](http://truffleframework.com/) so all truffle commands will work. 92 | 93 | ## Setup 94 | 1. Install dependencies. `yarn install` 95 | 1. Pick a 12 word mnemonic and put this in your bash profile so you can launch ganache-cli with a consistent seed. 96 | ``` 97 | export RTOKEN_DEVELOPMENT_MNEMONIC="YOUR_MNEMONIC_HERE" 98 | ``` 99 | 1. Run ganache-cli so truffle commands will work. 100 | ``` 101 | yarn test:ganache 102 | ``` 103 | 104 | ## Run Unit Tests 105 | ``` 106 | truffle test 107 | ``` 108 | 109 | ## Migrate contracts 110 | ``` 111 | truffle migrate 112 | ``` 113 | 114 | ## TestRegulatedTokens 115 | Normal R-Tokens should only be minted under carefully regulated conditions, such as an ICO. Normal R-Tokens also restrict `ERC20.transfer()` and `ERC20.transferFrom()` to ensure that trades comply with government regulations or other business needs. 116 | 117 | ### TestRegulatedTokens can be minted by developers whenever they want. 118 | Kovan has three test tokens already deployed to it: 119 | * Copper - CPPR - 0x5cdcc989560693e845060d271399216d8ab00c25 120 | * Silver - SLVR - 0x9843430d81290c90701bd655a7f19222b1de1f5f 121 | * Gold - GOLD - 0x88c3f6e2ddd8e65ec548197f2670623c5d876459 122 | 123 | Mint `100` `CPPR` tokens to address `0xd4afd5525ada1efa8ae661be1a0b373eb9e68498` on your local `development` environment(localhost:8545). 124 | ``` 125 | node scripts/mint_test_tokens.js development 100 CPPR 0xd4afd5525ada1efa8ae661be1a0b373eb9e68498 126 | ``` 127 | 128 | Mint `100` `GOLD` tokens to address `0xd4afd5525ada1efa8ae661be1a0b373eb9e68498` on `kovan`. 129 | ``` 130 | node scripts/mint_test_tokens.js kovan 100 GOLD 0xd4afd5525ada1efa8ae661be1a0b373eb9e68498 131 | ``` 132 | 133 | ### Developers can control if TestRegulatedToken transfers succeed or fail by adjusting the transaction amount. 134 | * Approved: 0 >= amount < 10 135 | * Error, Token is locked: 10 >= amount < 20 136 | * Error, Token is not divisible: 20 >= amount < 30 137 | * Error, Sender cannot send: 30 >= amount < 40 138 | * Error, Receiver cannot receive: 40 >= amount < 50 139 | ``` 140 | testToken.transfer(to, 5); // Approved 141 | testToken.transfer(to, 15); // Rejected, Token is locked 142 | testToken.transferFrom(from, to, 5); // Approved 143 | testToken.transferFrom(from, to, 15); // Rejected, Token is locked 144 | ``` 145 | -------------------------------------------------------------------------------- /contracts/TokenRegulatorService.sol: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | pragma solidity ^0.4.18; 18 | 19 | import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; 20 | import 'zeppelin-solidity/contracts/token/DetailedERC20.sol'; 21 | import './RegulatedToken.sol'; 22 | import './RegulatorService.sol'; 23 | 24 | /** 25 | * @title On-chain RegulatorService implementation for approving trades 26 | * @author Bob Remeika 27 | */ 28 | contract TokenRegulatorService is RegulatorService, Ownable { 29 | /** 30 | * @dev Throws if called by any account other than the admin 31 | */ 32 | modifier onlyAdmins() { 33 | require(msg.sender == admin || msg.sender == owner); 34 | _; 35 | } 36 | 37 | /// @dev Settings that affect token trading at a global level 38 | struct Settings { 39 | 40 | /** 41 | * @dev Toggle for locking/unlocking trades at a token level. 42 | * The default behavior of the zero memory state for locking will be unlocked. 43 | */ 44 | bool locked; 45 | 46 | /** 47 | * @dev Toggle for allowing/disallowing fractional token trades at a token level. 48 | * The default state when this contract is created `false` (or no partial 49 | * transfers allowed). 50 | */ 51 | bool partialTransfers; 52 | } 53 | 54 | // @dev Check success code 55 | uint8 constant private CHECK_SUCCESS = 0; 56 | 57 | // @dev Check error reason: Token is locked 58 | uint8 constant private CHECK_ELOCKED = 1; 59 | 60 | // @dev Check error reason: Token can not trade partial amounts 61 | uint8 constant private CHECK_EDIVIS = 2; 62 | 63 | // @dev Check error reason: Sender is not allowed to send the token 64 | uint8 constant private CHECK_ESEND = 3; 65 | 66 | // @dev Check error reason: Receiver is not allowed to receive the token 67 | uint8 constant private CHECK_ERECV = 4; 68 | 69 | /// @dev Permission bits for allowing a participant to send tokens 70 | uint8 constant private PERM_SEND = 0x1; 71 | 72 | /// @dev Permission bits for allowing a participant to receive tokens 73 | uint8 constant private PERM_RECEIVE = 0x2; 74 | 75 | // @dev Address of the administrator 76 | address public admin; 77 | 78 | /// @notice Permissions that allow/disallow token trades on a per token level 79 | mapping(address => Settings) private settings; 80 | 81 | /// @dev Permissions that allow/disallow token trades on a per participant basis. 82 | /// The format for key based access is `participants[tokenAddress][participantAddress]` 83 | /// which returns the permission bits of a participant for a particular token. 84 | mapping(address => mapping(address => uint8)) private participants; 85 | 86 | /// @dev Event raised when a token's locked setting is set 87 | event LogLockSet(address indexed token, bool locked); 88 | 89 | /// @dev Event raised when a token's partial transfer setting is set 90 | event LogPartialTransferSet(address indexed token, bool enabled); 91 | 92 | /// @dev Event raised when a participant permissions are set for a token 93 | event LogPermissionSet(address indexed token, address indexed participant, uint8 permission); 94 | 95 | /// @dev Event raised when the admin address changes 96 | event LogTransferAdmin(address indexed oldAdmin, address indexed newAdmin); 97 | 98 | function TokenRegulatorService() public { 99 | admin = msg.sender; 100 | } 101 | 102 | /** 103 | * @notice Locks the ability to trade a token 104 | * 105 | * @dev This method can only be called by this contract's owner 106 | * 107 | * @param _token The address of the token to lock 108 | */ 109 | function setLocked(address _token, bool _locked) onlyOwner public { 110 | settings[_token].locked = _locked; 111 | 112 | LogLockSet(_token, _locked); 113 | } 114 | 115 | /** 116 | * @notice Allows the ability to trade a fraction of a token 117 | * 118 | * @dev This method can only be called by this contract's owner 119 | * 120 | * @param _token The address of the token to allow partial transfers 121 | */ 122 | function setPartialTransfers(address _token, bool _enabled) onlyOwner public { 123 | settings[_token].partialTransfers = _enabled; 124 | 125 | LogPartialTransferSet(_token, _enabled); 126 | } 127 | 128 | /** 129 | * @notice Sets the trade permissions for a participant on a token 130 | * 131 | * @dev The `_permission` bits overwrite the previous trade permissions and can 132 | * only be called by the contract's owner. `_permissions` can be bitwise 133 | * `|`'d together to allow for more than one permission bit to be set. 134 | * 135 | * @param _token The address of the token 136 | * @param _participant The address of the trade participant 137 | * @param _permission Permission bits to be set 138 | */ 139 | function setPermission(address _token, address _participant, uint8 _permission) onlyAdmins public { 140 | participants[_token][_participant] = _permission; 141 | 142 | LogPermissionSet(_token, _participant, _permission); 143 | } 144 | 145 | /** 146 | * @dev Allows the owner to transfer admin controls to newAdmin. 147 | * 148 | * @param newAdmin The address to transfer admin rights to. 149 | */ 150 | function transferAdmin(address newAdmin) onlyOwner public { 151 | require(newAdmin != address(0)); 152 | 153 | address oldAdmin = admin; 154 | admin = newAdmin; 155 | 156 | LogTransferAdmin(oldAdmin, newAdmin); 157 | } 158 | 159 | /** 160 | * @notice Checks whether or not a trade should be approved 161 | * 162 | * @dev This method calls back to the token contract specified by `_token` for 163 | * information needed to enforce trade approval if needed 164 | * 165 | * @param _token The address of the token to be transfered 166 | * @param _spender The address of the spender of the token (unused in this implementation) 167 | * @param _from The address of the sender account 168 | * @param _to The address of the receiver account 169 | * @param _amount The quantity of the token to trade 170 | * 171 | * @return `true` if the trade should be approved and `false` if the trade should not be approved 172 | */ 173 | function check(address _token, address _spender, address _from, address _to, uint256 _amount) public returns (uint8) { 174 | if (settings[_token].locked) { 175 | return CHECK_ELOCKED; 176 | } 177 | 178 | if (participants[_token][_from] & PERM_SEND == 0) { 179 | return CHECK_ESEND; 180 | } 181 | 182 | if (participants[_token][_to] & PERM_RECEIVE == 0) { 183 | return CHECK_ERECV; 184 | } 185 | 186 | if (!settings[_token].partialTransfers && _amount % _wholeToken(_token) != 0) { 187 | return CHECK_EDIVIS; 188 | } 189 | 190 | return CHECK_SUCCESS; 191 | } 192 | 193 | /** 194 | * @notice Retrieves the whole token value from a token that this `RegulatorService` manages 195 | * 196 | * @param _token The token address of the managed token 197 | * 198 | * @return The uint256 value that represents a single whole token 199 | */ 200 | function _wholeToken(address _token) view private returns (uint256) { 201 | return uint256(10)**DetailedERC20(_token).decimals(); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /test/TokenRegulatorService.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright (c) 2017 Harbor Platform, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the “License”); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an “AS IS” BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const helpers = require('./helpers'); 18 | const MockRegulatedToken = artifacts.require('../test/helpers/MockRegulatedToken.sol'); 19 | const TokenRegulatorService = artifacts.require('./TokenRegulatorService.sol'); 20 | 21 | const PERM_NONE = 0x0; 22 | const PERM_SEND = 0x1; 23 | const PERM_RECEIVE = 0x2; 24 | const PERM_TRANSFER = PERM_SEND | PERM_RECEIVE; 25 | 26 | const ENONE = 0; 27 | const ELOCKED = 1; 28 | const EDIVIS = 2; 29 | const ESEND = 3; 30 | const ERECV = 4; 31 | 32 | contract('TokenRegulatorService', async accounts => { 33 | let owner; 34 | let spender; 35 | let account; 36 | let token; 37 | let service; 38 | 39 | beforeEach(async () => { 40 | owner = accounts[0]; 41 | spender = owner; 42 | admin = accounts[1]; 43 | account = accounts[2]; 44 | other = accounts[3]; 45 | 46 | service = await TokenRegulatorService.new({ from: owner }); 47 | token = await MockRegulatedToken.new(service.address); 48 | }); 49 | 50 | const onlyOwner = (method, producer) => { 51 | it(method + ' requires owner permissions', async () => { 52 | const [serviceToTest, ...args] = producer(); 53 | 54 | const acct = accounts[accounts.length - 1]; 55 | 56 | assert.isTrue(!!acct); 57 | assert.isTrue(acct != accounts[0]); 58 | 59 | await helpers.expectThrow(serviceToTest[method](...args, { from: acct })); 60 | }); 61 | }; 62 | 63 | const assertResult = (ret, success, reason) => { 64 | assert.equal(ret, reason, 'Assert reason'); 65 | }; 66 | 67 | describe('permissions', () => { 68 | onlyOwner('setLocked', () => { 69 | return [service, token.address, true]; 70 | }); 71 | onlyOwner('setPartialTransfers', () => { 72 | return [service, token.address, true]; 73 | }); 74 | onlyOwner('setPermission', () => { 75 | return [service, token.address, account, 0]; 76 | }); 77 | onlyOwner('transferAdmin', () => { 78 | return [service, account]; 79 | }); 80 | 81 | describe('setPermission', () => { 82 | beforeEach(async () => { 83 | await service.transferAdmin(admin); 84 | }); 85 | 86 | it('allows admin to invoke', async () => { 87 | await service.setPermission.call(0, account, 0, { from: admin }); 88 | await helpers.expectThrow(service.setPermission.call(0, account, 0, { from: other })); 89 | }); 90 | }); 91 | 92 | describe('default roles', () => { 93 | it('defaults the owner as the creator of the contract', async () => { 94 | const currentOwner = await service.owner(); 95 | assert.equal(owner, currentOwner); 96 | }); 97 | 98 | it('defaults the admin as the creator of the contract', async () => { 99 | const currentAdmin = await service.admin(); 100 | assert.equal(owner, currentAdmin); 101 | }); 102 | }); 103 | }); 104 | 105 | describe('locking', () => { 106 | beforeEach(async () => { 107 | await service.setPermission(token.address, owner, PERM_TRANSFER); 108 | await service.setPermission(token.address, account, PERM_TRANSFER); 109 | }); 110 | 111 | it('toggles the ability to trade', async () => { 112 | await service.setLocked(token.address, true); 113 | assertResult(await service.check.call(token.address, spender, owner, account, 0), false, ELOCKED); 114 | await service.setLocked(token.address, false); 115 | assertResult(await service.check.call(token.address, spender, owner, account, 0), true, ENONE); 116 | }); 117 | 118 | it('logs an event', async () => { 119 | await service.setLocked(token.address, false); 120 | 121 | await helpers.assertEvent(service.LogLockSet(), { 122 | token: token.address, 123 | locked: false, 124 | }); 125 | }); 126 | }); 127 | 128 | describe('partial trades', () => { 129 | beforeEach(async () => { 130 | await service.setLocked(token.address, false); 131 | await service.setPermission(token.address, owner, PERM_TRANSFER); 132 | await service.setPermission(token.address, account, PERM_TRANSFER); 133 | 134 | const decimals = 4; 135 | const expectedTotalSupply = 2000 * 10 ** decimals; 136 | 137 | await token.setDecimals(decimals); 138 | await token.mint(owner, expectedTotalSupply); 139 | 140 | assert.equal(expectedTotalSupply, await token.totalSupply.call()); 141 | 142 | assertResult(await service.check.call(token.address, spender, owner, account, 10001111), false, EDIVIS); 143 | }); 144 | 145 | it('logs an event', async () => { 146 | await service.setPartialTransfers(token.address, true); 147 | 148 | await helpers.assertEvent(service.LogPartialTransferSet(), { 149 | token: token.address, 150 | enabled: true, 151 | }); 152 | }); 153 | 154 | describe('when partial trades are allowed', async () => { 155 | it('allows fractional trades', async () => { 156 | await service.setPartialTransfers(token.address, true); 157 | assertResult(await service.check.call(token.address, spender, owner, account, 10001111), true, ENONE); 158 | assertResult(await service.check.call(token.address, spender, owner, account, 10000000), true, ENONE); 159 | }); 160 | }); 161 | 162 | describe('when partial trades are NOT allowed', async () => { 163 | it('does NOT allow fractional trades', async () => { 164 | await service.setPartialTransfers(token.address, false); 165 | assertResult(await service.check.call(token.address, spender, owner, account, 10000000), true, ENONE); 166 | assertResult(await service.check.call(token.address, spender, owner, account, 10001111), false, EDIVIS); 167 | }); 168 | }); 169 | }); 170 | 171 | describe('transferAdmin()', () => { 172 | describe('when the new admin is valid', () => { 173 | beforeEach(async () => { 174 | assert.equal(await service.admin(), owner); 175 | }); 176 | 177 | it('sets the new admin', async () => { 178 | await service.transferAdmin(admin); 179 | assert.equal(await service.admin(), admin); 180 | }); 181 | 182 | it('logs an event', async () => { 183 | await service.transferAdmin(admin); 184 | 185 | await helpers.assertEvent(service.LogTransferAdmin(), { 186 | oldAdmin: owner, 187 | newAdmin: admin, 188 | }); 189 | }); 190 | }); 191 | 192 | describe('when the new admin is NOT valid', () => { 193 | it('throws', async () => { 194 | await helpers.expectThrow(service.transferAdmin(0)); 195 | assert.equal(await service.admin(), owner); 196 | }); 197 | }); 198 | }); 199 | 200 | describe('transfer permissions', () => { 201 | beforeEach(async () => { 202 | await service.setLocked(token.address, false); 203 | }); 204 | 205 | it('logs an event', async () => { 206 | await service.setPermission(token.address, account, PERM_SEND); 207 | 208 | const properties = { 209 | token: token.address, 210 | participant: account, 211 | permission: PERM_SEND, 212 | }; 213 | 214 | await helpers.assertEvent(service.LogPermissionSet(), properties, (expected, actual) => { 215 | assert.equal(expected.token, actual.token); 216 | assert.equal(expected.participant, actual.participant); 217 | assert.equal(expected.permission.valueOf(), actual.permission.valueOf()); 218 | }); 219 | }); 220 | 221 | describe('when granular permissions are used', () => { 222 | it('requires a sender to have send permissions', async () => { 223 | await service.setPermission(token.address, owner, PERM_SEND); 224 | await service.setPermission(token.address, account, PERM_RECEIVE); 225 | 226 | assertResult(await service.check.call(token.address, spender, owner, account, 0), true, ENONE); 227 | 228 | await service.setPermission(token.address, owner, PERM_RECEIVE); 229 | await service.setPermission(token.address, account, PERM_RECEIVE); 230 | 231 | assertResult(await service.check.call(token.address, spender, owner, account, 0), false, ESEND); 232 | }); 233 | 234 | it('requires a receiver to have receive permissions', async () => { 235 | await service.setPermission(token.address, owner, PERM_SEND); 236 | await service.setPermission(token.address, account, PERM_RECEIVE); 237 | 238 | assertResult(await service.check.call(token.address, spender, owner, account, 0), true, ENONE); 239 | 240 | await service.setPermission(token.address, owner, PERM_TRANSFER); 241 | await service.setPermission(token.address, account, PERM_SEND); 242 | 243 | assertResult(await service.check.call(token.address, spender, owner, account, 0), false, ERECV); 244 | }); 245 | }); 246 | 247 | describe('when a participant does not exist', () => { 248 | beforeEach(async () => { 249 | await service.setPermission(token.address, owner, PERM_TRANSFER); 250 | await service.setPermission(token.address, account, PERM_TRANSFER); 251 | assertResult(await service.check.call(token.address, spender, owner, account, 0), true, ENONE); 252 | }); 253 | 254 | it('denies trades', async () => { 255 | assertResult(await service.check.call(token.address, spender, owner, '0x0', 0), false, ERECV); 256 | assertResult(await service.check.call(token.address, spender, '0x0', owner, 0), false, ESEND); 257 | }); 258 | }); 259 | 260 | describe('when both participants are eligible', () => { 261 | beforeEach(async () => { 262 | await service.setPermission(token.address, owner, PERM_NONE); 263 | await service.setPermission(token.address, account, PERM_NONE); 264 | assertResult(await service.check.call(token.address, spender, owner, account, 0), false, ESEND); 265 | }); 266 | 267 | it('allows trades', async () => { 268 | await service.setPermission(token.address, owner, PERM_TRANSFER); 269 | await service.setPermission(token.address, account, PERM_TRANSFER); 270 | assertResult(await service.check.call(token.address, spender, owner, account, 0), true, ENONE); 271 | }); 272 | }); 273 | 274 | describe('when one participant is ineligible', () => { 275 | beforeEach(async () => { 276 | await service.setPermission(token.address, owner, PERM_TRANSFER); 277 | await service.setPermission(token.address, account, PERM_TRANSFER); 278 | assertResult(await service.check.call(token.address, spender, owner, account, 0), true, ENONE); 279 | }); 280 | 281 | it('prevents trades', async () => { 282 | await service.setPermission(token.address, owner, PERM_TRANSFER); 283 | await service.setPermission(token.address, account, PERM_NONE); 284 | 285 | assertResult(await service.check.call(token.address, spender, owner, account, 0), false, ERECV); 286 | 287 | await service.setPermission(token.address, owner, PERM_NONE); 288 | await service.setPermission(token.address, account, PERM_TRANSFER); 289 | 290 | assertResult(await service.check.call(token.address, spender, owner, account, 0), false, ESEND); 291 | }); 292 | }); 293 | 294 | describe('when no participants are eligible', () => { 295 | beforeEach(async () => { 296 | await service.setPermission(token.address, owner, PERM_TRANSFER); 297 | await service.setPermission(token.address, account, PERM_TRANSFER); 298 | assertResult(await service.check.call(token.address, spender, owner, account, 0), true, ENONE); 299 | }); 300 | 301 | it('prevents trades', async () => { 302 | await service.setPermission(token.address, owner, PERM_NONE); 303 | await service.setPermission(token.address, account, PERM_NONE); 304 | assertResult(await service.check.call(token.address, spender, owner, account, 0), false, ESEND); 305 | }); 306 | }); 307 | }); 308 | }); 309 | --------------------------------------------------------------------------------