├── .DS_Store ├── .babelrc ├── .gitignore ├── .solcover.js ├── LICENSE.md ├── README.md ├── contracts ├── HasRegistry.sol ├── Migrations.sol ├── ProvisionalRegistry.sol ├── Registry.sol ├── RegistryToken.sol └── mocks │ ├── ERC20TokenMock.sol │ ├── ForceEtherMock.sol │ ├── RegistryMock.sol │ └── RegistryTokenMock.sol ├── migrations └── 1_initial_migration.js ├── package-lock.json ├── package.json ├── test ├── HasRegistry.test.js ├── ProvisionalRegistry.test.js ├── Registry.js ├── Registry.test.js └── helpers │ ├── assertBalance.js │ ├── assertRevert.js │ ├── bytes32.js │ └── writeAttributeFor.js └── truffle.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trusttoken/registry/23a8059f136333e1900d4343c84ffd2a040a2e39/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "stage-2", "stage-3"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | 4 | # Generated by solidity-coverage? 5 | scTopics 6 | allFiredEvents 7 | coverage 8 | coverage.json 9 | coverageEnv 10 | 11 | # VIM 12 | *.swp 13 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: require('ganache-cli'), 3 | skipFiles: ['Migrations.sol', 'mocks'], 4 | providerOptions: { 5 | "hardfork": "istanbul" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 TrustToken 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README.md 2 | 3 | This repository contains contracts used for ensuring regulatory 4 | compliance of cryptocurrencies. It takes inspiration from the 5 | [Transaction Permission Layer project](https://github.com/TPL-protocol/tpl-contracts). 6 | Here is a high-level overview of the contracts; for more detail 7 | see the relevant .sol files. 8 | 9 | ## The Registry 10 | 11 | ### Registry.sol 12 | 13 | Stores arbitrary attributes for users, such as whether they have 14 | passed a KYC/AML check. 15 | Allows the owner to set all attributes, and also allows the owner 16 | to choose other users that can set specific attributes. 17 | 18 | ### HasRegistry.sol 19 | 20 | Extended by other contracts that want to have a registry that is 21 | replacable by the owner. 22 | 23 | ## Testing 24 | 25 | To run the tests and generate a code coverage report: 26 | ```bash 27 | npm install 28 | npm test 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /contracts/HasRegistry.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.13; 2 | 3 | import "./Registry.sol"; 4 | import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; 5 | 6 | // Superclass for contracts that have a registry that can be set by their owners 7 | contract HasRegistry is Ownable { 8 | Registry public registry; 9 | 10 | event SetRegistry(address indexed registry); 11 | 12 | function setRegistry(Registry _registry) onlyOwner public { 13 | registry = _registry; 14 | emit SetRegistry(address(registry)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.13; 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 | constructor() public { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/ProvisionalRegistry.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.13; 2 | 3 | import "./Registry.sol"; 4 | 5 | contract ProvisionalRegistry is Registry { 6 | bytes32 constant IS_BLACKLISTED = "isBlacklisted"; 7 | bytes32 constant IS_DEPOSIT_ADDRESS = "isDepositAddress"; 8 | bytes32 constant IS_REGISTERED_CONTRACT = "isRegisteredContract"; 9 | bytes32 constant CAN_BURN = "canBurn"; 10 | 11 | function requireCanTransfer(address _from, address _to) public view returns (address, bool) { 12 | require (attributes[_from][IS_BLACKLISTED].value == 0, "blacklisted"); 13 | uint256 depositAddressValue = attributes[address(uint256(_to) >> 20)][IS_DEPOSIT_ADDRESS].value; 14 | if (depositAddressValue != 0) { 15 | _to = address(depositAddressValue); 16 | } 17 | require (attributes[_to][IS_BLACKLISTED].value == 0, "blacklisted"); 18 | return (_to, attributes[_to][IS_REGISTERED_CONTRACT].value != 0); 19 | } 20 | 21 | function requireCanTransferFrom(address _sender, address _from, address _to) public view returns (address, bool) { 22 | require (attributes[_sender][IS_BLACKLISTED].value == 0, "blacklisted"); 23 | return requireCanTransfer(_from, _to); 24 | } 25 | 26 | function requireCanMint(address _to) public view returns (address, bool) { 27 | require (attributes[_to][IS_BLACKLISTED].value == 0, "blacklisted"); 28 | uint256 depositAddressValue = attributes[address(uint256(_to) >> 20)][IS_DEPOSIT_ADDRESS].value; 29 | if (depositAddressValue != 0) { 30 | _to = address(depositAddressValue); 31 | } 32 | return (_to, attributes[_to][IS_REGISTERED_CONTRACT].value != 0); 33 | } 34 | 35 | function requireCanBurn(address _from) public view { 36 | require (attributes[_from][CAN_BURN].value != 0); 37 | require (attributes[_from][IS_BLACKLISTED].value == 0); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/Registry.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.13; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; 4 | 5 | interface RegistryClone { 6 | function syncAttributeValue(address _who, bytes32 _attribute, uint256 _value) external; 7 | } 8 | 9 | contract Registry { 10 | struct AttributeData { 11 | uint256 value; 12 | bytes32 notes; 13 | address adminAddr; 14 | uint256 timestamp; 15 | } 16 | 17 | // never remove any storage variables 18 | address public owner; 19 | address public pendingOwner; 20 | bool initialized; 21 | 22 | // Stores arbitrary attributes for users. An example use case is an IERC20 23 | // token that requires its users to go through a KYC/AML check - in this case 24 | // a validator can set an account's "hasPassedKYC/AML" attribute to 1 to indicate 25 | // that account can use the token. This mapping stores that value (1, in the 26 | // example) as well as which validator last set the value and at what time, 27 | // so that e.g. the check can be renewed at appropriate intervals. 28 | mapping(address => mapping(bytes32 => AttributeData)) attributes; 29 | // The logic governing who is allowed to set what attributes is abstracted as 30 | // this accessManager, so that it may be replaced by the owner as needed 31 | bytes32 constant WRITE_PERMISSION = keccak256("canWriteTo-"); 32 | mapping(bytes32 => RegistryClone[]) subscribers; 33 | 34 | event OwnershipTransferred( 35 | address indexed previousOwner, 36 | address indexed newOwner 37 | ); 38 | event SetAttribute(address indexed who, bytes32 attribute, uint256 value, bytes32 notes, address indexed adminAddr); 39 | event SetManager(address indexed oldManager, address indexed newManager); 40 | event StartSubscription(bytes32 indexed attribute, RegistryClone indexed subscriber); 41 | event StopSubscription(bytes32 indexed attribute, RegistryClone indexed subscriber); 42 | 43 | // Allows a write if either a) the writer is that Registry's owner, or 44 | // b) the writer is writing to attribute foo and that writer already has 45 | // the canWriteTo-foo attribute set (in that same Registry) 46 | function confirmWrite(bytes32 _attribute, address _admin) internal view returns (bool) { 47 | return (_admin == owner || hasAttribute(_admin, keccak256(abi.encodePacked(WRITE_PERMISSION ^ _attribute)))); 48 | } 49 | 50 | // Writes are allowed only if the accessManager approves 51 | function setAttribute(address _who, bytes32 _attribute, uint256 _value, bytes32 _notes) public { 52 | require(confirmWrite(_attribute, msg.sender)); 53 | attributes[_who][_attribute] = AttributeData(_value, _notes, msg.sender, block.timestamp); 54 | emit SetAttribute(_who, _attribute, _value, _notes, msg.sender); 55 | 56 | RegistryClone[] storage targets = subscribers[_attribute]; 57 | uint256 index = targets.length; 58 | while (index --> 0) { 59 | targets[index].syncAttributeValue(_who, _attribute, _value); 60 | } 61 | } 62 | 63 | function subscribe(bytes32 _attribute, RegistryClone _syncer) external onlyOwner { 64 | subscribers[_attribute].push(_syncer); 65 | emit StartSubscription(_attribute, _syncer); 66 | } 67 | 68 | function unsubscribe(bytes32 _attribute, uint256 _index) external onlyOwner { 69 | uint256 length = subscribers[_attribute].length; 70 | require(_index < length); 71 | emit StopSubscription(_attribute, subscribers[_attribute][_index]); 72 | subscribers[_attribute][_index] = subscribers[_attribute][length - 1]; 73 | subscribers[_attribute].length = length - 1; 74 | } 75 | 76 | function subscriberCount(bytes32 _attribute) public view returns (uint256) { 77 | return subscribers[_attribute].length; 78 | } 79 | 80 | function setAttributeValue(address _who, bytes32 _attribute, uint256 _value) public { 81 | require(confirmWrite(_attribute, msg.sender)); 82 | attributes[_who][_attribute] = AttributeData(_value, "", msg.sender, block.timestamp); 83 | emit SetAttribute(_who, _attribute, _value, "", msg.sender); 84 | RegistryClone[] storage targets = subscribers[_attribute]; 85 | uint256 index = targets.length; 86 | while (index --> 0) { 87 | targets[index].syncAttributeValue(_who, _attribute, _value); 88 | } 89 | } 90 | 91 | // Returns true if the uint256 value stored for this attribute is non-zero 92 | function hasAttribute(address _who, bytes32 _attribute) public view returns (bool) { 93 | return attributes[_who][_attribute].value != 0; 94 | } 95 | 96 | 97 | // Returns the exact value of the attribute, as well as its metadata 98 | function getAttribute(address _who, bytes32 _attribute) public view returns (uint256, bytes32, address, uint256) { 99 | AttributeData memory data = attributes[_who][_attribute]; 100 | return (data.value, data.notes, data.adminAddr, data.timestamp); 101 | } 102 | 103 | function getAttributeValue(address _who, bytes32 _attribute) public view returns (uint256) { 104 | return attributes[_who][_attribute].value; 105 | } 106 | 107 | function getAttributeAdminAddr(address _who, bytes32 _attribute) public view returns (address) { 108 | return attributes[_who][_attribute].adminAddr; 109 | } 110 | 111 | function getAttributeTimestamp(address _who, bytes32 _attribute) public view returns (uint256) { 112 | return attributes[_who][_attribute].timestamp; 113 | } 114 | 115 | function syncAttribute(bytes32 _attribute, uint256 _startIndex, address[] calldata _addresses) external { 116 | RegistryClone[] storage targets = subscribers[_attribute]; 117 | uint256 index = targets.length; 118 | while (index --> _startIndex) { 119 | RegistryClone target = targets[index]; 120 | for (uint256 i = _addresses.length; i --> 0; ) { 121 | address who = _addresses[i]; 122 | target.syncAttributeValue(who, _attribute, attributes[who][_attribute].value); 123 | } 124 | } 125 | } 126 | 127 | function reclaimEther(address payable _to) external onlyOwner { 128 | _to.transfer(address(this).balance); 129 | } 130 | 131 | function reclaimToken(IERC20 token, address _to) external onlyOwner { 132 | uint256 balance = token.balanceOf(address(this)); 133 | token.transfer(_to, balance); 134 | } 135 | 136 | /** 137 | * @dev Throws if called by any account other than the owner. 138 | */ 139 | modifier onlyOwner() { 140 | require(msg.sender == owner, "only Owner"); 141 | _; 142 | } 143 | 144 | /** 145 | * @dev Modifier throws if called by any account other than the pendingOwner. 146 | */ 147 | modifier onlyPendingOwner() { 148 | require(msg.sender == pendingOwner); 149 | _; 150 | } 151 | 152 | /** 153 | * @dev Allows the current owner to set the pendingOwner address. 154 | * @param newOwner The address to transfer ownership to. 155 | */ 156 | function transferOwnership(address newOwner) public onlyOwner { 157 | pendingOwner = newOwner; 158 | } 159 | 160 | /** 161 | * @dev Allows the pendingOwner address to finalize the transfer. 162 | */ 163 | function claimOwnership() public onlyPendingOwner { 164 | emit OwnershipTransferred(owner, pendingOwner); 165 | owner = pendingOwner; 166 | pendingOwner = address(0); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /contracts/RegistryToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.13; 2 | 3 | import "./HasRegistry.sol"; 4 | 5 | contract RegistryToken is HasRegistry { 6 | mapping(address => mapping(bytes32 => uint256)) attributes; 7 | 8 | modifier onlyRegistry { 9 | require(msg.sender == address(registry)); 10 | _; 11 | } 12 | 13 | function syncAttributeValue(address _who, bytes32 _attribute, uint256 _value) public onlyRegistry { 14 | attributes[_who][_attribute] = _value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /contracts/mocks/ERC20TokenMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.13; 2 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 3 | 4 | contract MockToken is ERC20 { 5 | constructor(address _initialAccount, uint256 _initialBalance ) public { 6 | _mint(_initialAccount, _initialBalance); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /contracts/mocks/ForceEtherMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.13; 2 | 3 | contract ForceEther { 4 | constructor() public payable { } 5 | 6 | function destroyAndSend(address payable _recipient) public { 7 | selfdestruct(_recipient); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/mocks/RegistryMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.13; 2 | import "../Registry.sol"; 3 | import "../ProvisionalRegistry.sol"; 4 | 5 | contract RegistryMock is Registry { 6 | 7 | 8 | /** 9 | * @dev sets the original `owner` of the contract to the sender 10 | * at construction. Must then be reinitialized 11 | */ 12 | constructor() public { 13 | owner = msg.sender; 14 | emit OwnershipTransferred(address(0), owner); 15 | } 16 | 17 | function initialize() public { 18 | require(!initialized, "already initialized"); 19 | owner = msg.sender; 20 | initialized = true; 21 | } 22 | } 23 | 24 | contract ProvisionalRegistryMock is RegistryMock, ProvisionalRegistry { 25 | } 26 | -------------------------------------------------------------------------------- /contracts/mocks/RegistryTokenMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.13; 2 | 3 | import "../RegistryToken.sol"; 4 | 5 | contract RegistryTokenMock is RegistryToken { 6 | 7 | function getAttributeValue(address _who, bytes32 _attribute) public view returns (uint256) { 8 | return attributes[_who][_attribute]; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@trusttoken/registry", 3 | "version": "0.1.2", 4 | "description": "", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "files": [ 9 | "contracts", 10 | "test/helpers" 11 | ], 12 | "scripts": { 13 | "test": "truffle run coverage" 14 | }, 15 | "author": "", 16 | "license": "MIT", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/trusttoken/registry.git" 20 | }, 21 | "dependencies": { 22 | "openzeppelin-solidity": "2.4.0" 23 | }, 24 | "devDependencies": { 25 | "solidity-coverage": "^0.7.0-beta.2", 26 | "babel-polyfill": "^6.26.0", 27 | "babel-preset-env": "^1.6.1", 28 | "babel-preset-stage-2": "^6.18.0", 29 | "babel-preset-stage-3": "^6.17.0", 30 | "babel-register": "^6.26.0", 31 | "bn.js": "^4.11.8", 32 | "ganache-cli": "6.8.0-istanbul.0", 33 | "truffle": "5.0.44", 34 | "truffle-hdwallet-provider": "0.0.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/HasRegistry.test.js: -------------------------------------------------------------------------------- 1 | import assertRevert from './helpers/assertRevert' 2 | const Registry = artifacts.require('Registry') 3 | const HasRegistry = artifacts.require('HasRegistry') 4 | 5 | contract('HasRegistry', function ([_, owner, anotherAccount]) { 6 | beforeEach(async function () { 7 | this.token = await HasRegistry.new({ from: owner }) 8 | }) 9 | 10 | describe('--HasRegistry Tests--', function () { 11 | describe('setRegistry', function () { 12 | let registry2 13 | 14 | beforeEach(async function () { 15 | registry2 = await Registry.new({ from: owner }) 16 | }) 17 | 18 | it('sets the registry', async function () { 19 | await this.token.setRegistry(registry2.address, { from: owner }) 20 | 21 | let registry = await this.token.registry() 22 | assert.equal(registry, registry2.address) 23 | }) 24 | 25 | it('emits an event', async function () { 26 | const { logs } = await this.token.setRegistry(registry2.address, { from: owner }) 27 | 28 | assert.equal(logs.length, 1) 29 | assert.equal(logs[0].event, 'SetRegistry') 30 | assert.equal(logs[0].args.registry, registry2.address) 31 | }) 32 | 33 | it('cannot be called by non-owner', async function () { 34 | await assertRevert(this.token.setRegistry(registry2.address, { from: anotherAccount })) 35 | }) 36 | }) 37 | }) 38 | }) -------------------------------------------------------------------------------- /test/ProvisionalRegistry.test.js: -------------------------------------------------------------------------------- 1 | import registryTests from './Registry' 2 | const ProvisionalRegistryMock = artifacts.require('ProvisionalRegistryMock') 3 | 4 | import assertRevert from './helpers/assertRevert' 5 | const bytes32 = require('./helpers/bytes32.js') 6 | 7 | contract('ProvisionalRegistry', function ([_, owner, oneHundred, anotherAccount]) { 8 | const prop1 = web3.utils.sha3("foo") 9 | const IS_BLACKLISTED = bytes32('isBlacklisted'); 10 | const IS_REGISTERED_CONTRACT = bytes32('isRegisteredContract'); 11 | const IS_DEPOSIT_ADDRESS = bytes32('isDepositAddress'); 12 | const CAN_BURN = bytes32("canBurn"); 13 | const prop2 = bytes32("bar") 14 | 15 | beforeEach(async function () { 16 | this.registry = await ProvisionalRegistryMock.new({ from: owner }) 17 | await this.registry.initialize({ from: owner }) 18 | }) 19 | registryTests([owner, oneHundred, anotherAccount]) 20 | 21 | describe('requireCanTransfer and requireCanTransferFrom', async function() { 22 | it('return _to and false when nothing set', async function() { 23 | const result = await this.registry.requireCanTransfer(oneHundred, anotherAccount); 24 | assert.equal(result[0], anotherAccount); 25 | assert.equal(result[1], false); 26 | const resultFrom = await this.registry.requireCanTransferFrom(owner, oneHundred, anotherAccount); 27 | assert.equal(resultFrom[0], anotherAccount); 28 | assert.equal(resultFrom[1], false); 29 | }); 30 | it('revert when _from is blacklisted', async function() { 31 | await this.registry.setAttributeValue(oneHundred, IS_BLACKLISTED, 1, { from: owner }); 32 | await assertRevert(this.registry.requireCanTransfer(oneHundred, anotherAccount)); 33 | await assertRevert(this.registry.requireCanTransferFrom(owner, oneHundred, anotherAccount)); 34 | }); 35 | it('revert when _to is blacklisted', async function() { 36 | await this.registry.setAttributeValue(anotherAccount, IS_BLACKLISTED, 1, { from: owner }); 37 | await assertRevert(this.registry.requireCanTransfer(oneHundred, anotherAccount)); 38 | await assertRevert(this.registry.requireCanTransferFrom(owner, oneHundred, anotherAccount)); 39 | }); 40 | it('revert when _sender is blacklisted', async function() { 41 | await this.registry.setAttributeValue(anotherAccount, IS_BLACKLISTED, 1, { from: owner }); 42 | await assertRevert(this.registry.requireCanTransferFrom(anotherAccount, oneHundred, owner)); 43 | }); 44 | it('handle deposit addresses', async function() { 45 | await this.registry.setAttributeValue(web3.utils.toChecksumAddress('0x00000' + anotherAccount.slice(2, -5)), IS_DEPOSIT_ADDRESS, anotherAccount, { from: owner }); 46 | const result = await this.registry.requireCanTransfer(oneHundred, web3.utils.toChecksumAddress(anotherAccount.slice(0, -5) + '00000')); 47 | assert.equal(result[0], anotherAccount); 48 | assert.equal(result[1], false); 49 | const resultFrom = await this.registry.requireCanTransferFrom(oneHundred, oneHundred, web3.utils.toChecksumAddress(anotherAccount.slice(0, -5) + 'fffff')); 50 | assert.equal(resultFrom[0], anotherAccount); 51 | assert.equal(resultFrom[1], false); 52 | await this.registry.setAttributeValue(anotherAccount, IS_BLACKLISTED, 1, { from: owner }); 53 | await assertRevert(this.registry.requireCanTransfer(oneHundred, web3.utils.toChecksumAddress(anotherAccount.slice(0, -5) + 'eeeee'))); 54 | await assertRevert(this.registry.requireCanTransferFrom(oneHundred, oneHundred, web3.utils.toChecksumAddress(anotherAccount.slice(0, -5) + 'eeeee'))); 55 | }); 56 | it('return true when recipient is a registered contract', async function() { 57 | await this.registry.setAttributeValue(anotherAccount, IS_REGISTERED_CONTRACT, anotherAccount, { from: owner }); 58 | const result = await this.registry.requireCanTransfer(oneHundred, anotherAccount); 59 | assert.equal(result[0], anotherAccount); 60 | assert.equal(result[1], true); 61 | const resultFrom = await this.registry.requireCanTransferFrom(owner, oneHundred, anotherAccount); 62 | assert.equal(resultFrom[0], anotherAccount); 63 | assert.equal(resultFrom[1], true); 64 | }); 65 | it ('handles deposit addresses that are registered contracts', async function() { 66 | await this.registry.setAttributeValue(web3.utils.toChecksumAddress('0x00000' + anotherAccount.slice(2, -5)), IS_DEPOSIT_ADDRESS, anotherAccount, { from: owner }); 67 | await this.registry.setAttributeValue(anotherAccount, IS_REGISTERED_CONTRACT, anotherAccount, { from: owner }); 68 | const resultContract = await this.registry.requireCanTransfer(oneHundred, web3.utils.toChecksumAddress(anotherAccount.slice(0, -5) + '00000')); 69 | assert.equal(resultContract[0], anotherAccount); 70 | assert.equal(resultContract[1], true); 71 | const resultFromContract = await this.registry.requireCanTransferFrom(oneHundred, oneHundred, web3.utils.toChecksumAddress(anotherAccount.slice(0, -5) + 'fffff')); 72 | assert.equal(resultFromContract[0], anotherAccount); 73 | assert.equal(resultFromContract[1], true); 74 | await this.registry.setAttributeValue(anotherAccount, IS_BLACKLISTED, 1, { from: owner }); 75 | await assertRevert(this.registry.requireCanTransfer(oneHundred, web3.utils.toChecksumAddress(anotherAccount.slice(0, -5) + 'eeeee'))); 76 | await assertRevert(this.registry.requireCanTransferFrom(oneHundred, oneHundred, web3.utils.toChecksumAddress(anotherAccount.slice(0, -5) + 'eeeee'))); 77 | }); 78 | }) 79 | 80 | describe('requireCanMint', async function() { 81 | it('reverts for blacklisted recipient', async function() { 82 | await this.registry.setAttributeValue(anotherAccount, IS_BLACKLISTED, 1, { from: owner }); 83 | await assertRevert(this.registry.requireCanMint(anotherAccount)); 84 | }) 85 | it('returns false for whitelisted accounts', async function() { 86 | const result = await this.registry.requireCanMint(anotherAccount); 87 | assert.equal(result[0], anotherAccount); 88 | assert.equal(result[1], false); 89 | }) 90 | it('returns deposit address', async function() { 91 | const depositAddress = web3.utils.toChecksumAddress(anotherAccount.slice(0, -5) + '00055'); 92 | await this.registry.setAttributeValue(web3.utils.toChecksumAddress('0x00000' + anotherAccount.slice(2, -5)), IS_DEPOSIT_ADDRESS, anotherAccount, { from: owner }); 93 | const result = await this.registry.requireCanMint(depositAddress); 94 | assert.equal(result[0], anotherAccount); 95 | assert.equal(result[1], false); 96 | }) 97 | it('returns true for registered', async function() { 98 | await this.registry.setAttributeValue(anotherAccount, IS_REGISTERED_CONTRACT, 1, { from: owner }); 99 | const result = await this.registry.requireCanMint(anotherAccount); 100 | assert.equal(result[0], anotherAccount); 101 | assert.equal(result[1], true); 102 | }) 103 | it('handles registered deposit addresses', async function() { 104 | const depositAddress = web3.utils.toChecksumAddress(anotherAccount.slice(0, -5) + '00055'); 105 | await this.registry.setAttributeValue(anotherAccount, IS_REGISTERED_CONTRACT, 1, { from: owner }); 106 | await this.registry.setAttributeValue(web3.utils.toChecksumAddress('0x00000' + anotherAccount.slice(2, -5)), IS_DEPOSIT_ADDRESS, anotherAccount, { from: owner }); 107 | const result = await this.registry.requireCanMint(depositAddress); 108 | assert.equal(result[0], anotherAccount); 109 | assert.equal(result[1], true); 110 | }) 111 | }) 112 | 113 | describe('requireCanBurn', async function() { 114 | it('reverts without CAN_BURN flag', async function() { 115 | await assertRevert(this.registry.requireCanBurn(owner)); 116 | await assertRevert(this.registry.requireCanBurn(oneHundred)); 117 | await assertRevert(this.registry.requireCanBurn(anotherAccount)); 118 | }) 119 | it('works with CAN_BURN flag', async function () { 120 | await this.registry.setAttributeValue(anotherAccount, CAN_BURN, 1, { from: owner }); 121 | await this.registry.requireCanBurn(anotherAccount); 122 | await assertRevert(this.registry.requireCanBurn(owner)); 123 | }) 124 | it('reverts for blacklisted accounts', async function() { 125 | await this.registry.setAttributeValue(anotherAccount, CAN_BURN, 1, { from: owner }); 126 | await this.registry.setAttributeValue(anotherAccount, IS_BLACKLISTED, 1, { from: owner }); 127 | await assertRevert(this.registry.requireCanBurn(anotherAccount)); 128 | await assertRevert(this.registry.requireCanBurn(owner)); 129 | }) 130 | }) 131 | 132 | 133 | }) 134 | -------------------------------------------------------------------------------- /test/Registry.js: -------------------------------------------------------------------------------- 1 | const RegistryTokenMock = artifacts.require('RegistryTokenMock') 2 | const MockToken = artifacts.require("MockToken") 3 | const ForceEther = artifacts.require("ForceEther") 4 | 5 | import assertRevert from './helpers/assertRevert' 6 | const writeAttributeFor = require('./helpers/writeAttributeFor.js') 7 | const bytes32 = require('./helpers/bytes32.js') 8 | const BN = web3.utils.toBN; 9 | 10 | function registryTests([owner, oneHundred, anotherAccount]) { 11 | describe('--Registry Tests--', function () { 12 | const prop1 = web3.utils.sha3("foo") 13 | const prop2 = bytes32("bar") 14 | const notes = bytes32("blarg") 15 | const CAN_BURN = bytes32("canBurn"); 16 | 17 | describe('ownership functions', function(){ 18 | it('cannot be reinitialized', async function () { 19 | await assertRevert(this.registry.initialize({ from: owner })) 20 | }) 21 | it('can transfer ownership', async function () { 22 | await this.registry.transferOwnership(anotherAccount,{ from: owner }) 23 | assert.equal(await this.registry.pendingOwner(),anotherAccount) 24 | }) 25 | it('non owner cannot transfer ownership', async function () { 26 | await assertRevert(this.registry.transferOwnership(anotherAccount,{ from: anotherAccount })) 27 | }) 28 | it('can claim ownership', async function () { 29 | await this.registry.transferOwnership(anotherAccount,{ from: owner }) 30 | await this.registry.claimOwnership({ from: anotherAccount }) 31 | assert.equal(await this.registry.owner(),anotherAccount) 32 | }) 33 | it('only pending owner can claim ownership', async function () { 34 | await this.registry.transferOwnership(anotherAccount,{ from: owner }) 35 | await assertRevert(this.registry.claimOwnership({ from: oneHundred })) 36 | }) 37 | }) 38 | describe('read/write', function () { 39 | it('works for owner', async function () { 40 | const { receipt } = await this.registry.setAttribute(anotherAccount, prop1, 3, notes, { from: owner }) 41 | const attr = await this.registry.getAttribute(anotherAccount, prop1) 42 | assert.equal(attr[0], 3) 43 | assert.equal(attr[1], notes) 44 | assert.equal(attr[2], owner) 45 | assert.equal(attr[3], (await web3.eth.getBlock(receipt.blockNumber)).timestamp) 46 | const hasAttr = await this.registry.hasAttribute(anotherAccount, prop1) 47 | assert.equal(hasAttr, true) 48 | const value = await this.registry.getAttributeValue(anotherAccount, prop1) 49 | assert.equal(Number(value),3) 50 | const adminAddress = await this.registry.getAttributeAdminAddr(anotherAccount, prop1) 51 | assert.equal(adminAddress, owner) 52 | const timestamp = await this.registry.getAttributeTimestamp(anotherAccount, prop1) 53 | assert.equal(timestamp, (await web3.eth.getBlock(receipt.blockNumber)).timestamp) 54 | }) 55 | 56 | it('sets only desired attribute', async function () { 57 | await this.registry.setAttribute(anotherAccount, prop1, 3, notes, { from: owner }) 58 | const attr = await this.registry.getAttribute(anotherAccount, prop2) 59 | assert.equal(attr[0], 0) 60 | assert.equal(attr[1], '0x0000000000000000000000000000000000000000000000000000000000000000') 61 | assert.equal(attr[2], 0) 62 | const hasAttr = await this.registry.hasAttribute(anotherAccount, prop2) 63 | assert.equal(hasAttr, false) 64 | }) 65 | 66 | it('emits an event', async function () { 67 | const { logs } = await this.registry.setAttribute(anotherAccount, prop1, 3, notes, { from: owner }) 68 | 69 | assert.equal(logs.length, 1) 70 | assert.equal(logs[0].event, 'SetAttribute') 71 | assert.equal(logs[0].args.who, anotherAccount) 72 | assert.equal(logs[0].args.attribute, prop1) 73 | assert.equal(logs[0].args.value, 3) 74 | assert.equal(logs[0].args.notes, notes) 75 | assert.equal(logs[0].args.adminAddr, owner) 76 | }) 77 | 78 | it('cannot be called by random non-owner', async function () { 79 | await assertRevert(this.registry.setAttribute(anotherAccount, prop1, 3, notes, { from: oneHundred })) 80 | }) 81 | 82 | it('owner can let others write', async function () { 83 | const canWriteProp1 = writeAttributeFor(prop1); 84 | await this.registry.setAttribute(oneHundred, canWriteProp1, 3, notes, { from: owner }) 85 | await this.registry.setAttribute(anotherAccount, prop1, 3, notes, { from: oneHundred }) 86 | }) 87 | 88 | it('owner can let others write attribute value', async function () { 89 | const canWriteProp1 = writeAttributeFor(prop1); 90 | await this.registry.setAttributeValue(oneHundred, canWriteProp1, 3, { from: owner }) 91 | await this.registry.setAttributeValue(anotherAccount, prop1, 3, { from: oneHundred }) 92 | }) 93 | 94 | it('others can only write what they are allowed to', async function () { 95 | const canWriteProp1 = writeAttributeFor(prop1); 96 | await this.registry.setAttribute(oneHundred, canWriteProp1, 3, notes, { from: owner }) 97 | await assertRevert(this.registry.setAttribute(anotherAccount, prop2, 3, notes, { from: oneHundred })) 98 | await assertRevert(this.registry.setAttributeValue(anotherAccount, prop2, 3, { from: oneHundred })) 99 | }) 100 | }) 101 | 102 | describe('no ether and no tokens', function () { 103 | beforeEach(async function () { 104 | this.token = await MockToken.new( this.registry.address, 100, { from: owner }) 105 | }) 106 | 107 | it ('owner can transfer out token in the contract address ',async function(){ 108 | await this.registry.reclaimToken(this.token.address, owner, { from: owner }) 109 | }) 110 | 111 | it('cannot transfer ether to contract address',async function(){ 112 | await assertRevert(this.registry.sendTransaction({ 113 | value: 33, 114 | from: owner, 115 | gas: 300000 116 | })); 117 | }) 118 | 119 | it ('owner can transfer out ether in the contract address',async function(){ 120 | const emptyAddress = "0x5fef93e79a73b28a9113a618aabf84f2956eb3ba" 121 | const emptyAddressBalance = web3.utils.fromWei((await web3.eth.getBalance(emptyAddress)), 'ether') 122 | 123 | const forceEther = await ForceEther.new({ from: owner, value: "10000000000000000000" }) 124 | await forceEther.destroyAndSend(this.registry.address, { from: owner }) 125 | const registryInitialWithForcedEther = web3.utils.fromWei((await web3.eth.getBalance(this.registry.address)), 'ether') 126 | await this.registry.reclaimEther(emptyAddress, { from: owner }) 127 | const registryFinalBalance = web3.utils.fromWei((await web3.eth.getBalance(this.registry.address)), 'ether') 128 | const emptyAddressFinalBalance = web3.utils.fromWei((await web3.eth.getBalance(emptyAddress)), 'ether') 129 | assert.equal(registryInitialWithForcedEther,10) 130 | assert.equal(registryFinalBalance,0) 131 | assert.equal(emptyAddressFinalBalance,10) 132 | }) 133 | 134 | }) 135 | 136 | describe('sync', function() { 137 | beforeEach(async function() { 138 | this.registryToken = await RegistryTokenMock.new({ from: owner }) 139 | await this.registryToken.setRegistry(this.registry.address, { from: owner }) 140 | await this.registry.subscribe(prop1, this.registryToken.address, { from: owner }); 141 | await this.registry.setAttributeValue(oneHundred, prop1, 3, { from: owner }); 142 | }) 143 | it('writes sync', async function() { 144 | assert.equal(3, await this.registryToken.getAttributeValue(oneHundred, prop1)); 145 | }) 146 | it('subscription emits event', async function() { 147 | const { logs } = await this.registry.subscribe(prop2, this.registryToken.address, { from: owner }); 148 | assert.equal(logs.length, 1); 149 | assert.equal(logs[0].args.attribute, prop2); 150 | assert.equal(logs[0].args.subscriber, this.registryToken.address); 151 | assert(BN(1).eq(await this.registry.subscriberCount(prop2)), 'should have 1 subscriber'); 152 | }) 153 | it('unsubscription emits event', async function() { 154 | const { logs } = await this.registry.unsubscribe(prop1, 0, { from: owner }); 155 | assert.equal(logs.length, 1); 156 | assert.equal(logs[0].args.attribute, prop1); 157 | assert.equal(logs[0].args.subscriber, this.registryToken.address); 158 | }) 159 | it('can unsubscribe', async function() { 160 | assert(BN(1).eq(await this.registry.subscriberCount(prop1)), 'should have 1 subscriber'); 161 | await this.registry.unsubscribe(prop1, 0, { from: owner }); 162 | assert(BN(0).eq(await this.registry.subscriberCount(prop1)), 'should have 0 subscribers'); 163 | }) 164 | it('syncs prior writes', async function() { 165 | let token2 = await RegistryTokenMock.new({ from: owner }); 166 | await token2.setRegistry(this.registry.address, { from: owner }); 167 | await this.registry.subscribe(prop1, token2.address, {from: owner}); 168 | assert.equal(3, await this.registryToken.getAttributeValue(oneHundred, prop1)); 169 | assert.equal(0, await token2.getAttributeValue(oneHundred, prop1)); 170 | 171 | await this.registry.syncAttribute(prop1, 0, [oneHundred]); 172 | assert.equal(3, await token2.getAttributeValue(oneHundred, prop1)); 173 | }) 174 | it('syncs prior attribute', async function() { 175 | let token2 = await RegistryTokenMock.new({ from: owner }); 176 | await token2.setRegistry(this.registry.address, { from: owner }); 177 | await this.registry.subscribe(prop1, token2.address, {from: owner}); 178 | assert.equal(3, await this.registryToken.getAttributeValue(oneHundred, prop1)); 179 | assert.equal(0, await token2.getAttributeValue(oneHundred, prop1)); 180 | await this.registry.syncAttribute(prop1, 0, [oneHundred]); 181 | assert.equal(3, await token2.getAttributeValue(oneHundred, prop1)); 182 | }) 183 | it('syncs multiple prior writes', async function() { 184 | await this.registry.setAttributeValue(oneHundred, prop1, 3, { from: owner}); 185 | await this.registry.setAttributeValue(anotherAccount, prop1, 5, { from: owner}); 186 | await this.registry.setAttributeValue(owner, prop1, 6, { from: owner}); 187 | 188 | let token2 = await RegistryTokenMock.new({ from: owner }); 189 | await token2.setRegistry(this.registry.address, { from: owner }); 190 | await this.registry.subscribe(prop1, token2.address, { from: owner }); 191 | await this.registry.syncAttribute(prop1, 2, [oneHundred, anotherAccount, owner]); 192 | assert.equal(3, await this.registryToken.getAttributeValue(oneHundred, prop1), { from: owner }); 193 | assert.equal(0, await token2.getAttributeValue(oneHundred, prop1)); 194 | 195 | await this.registry.syncAttribute(prop1, 1, [oneHundred, anotherAccount, owner]); 196 | assert.equal(3, await token2.getAttributeValue(oneHundred, prop1)); 197 | assert.equal(5, await token2.getAttributeValue(anotherAccount, prop1)); 198 | assert.equal(6, await token2.getAttributeValue(owner, prop1)); 199 | }) 200 | }) 201 | 202 | }) 203 | } 204 | 205 | export default registryTests 206 | -------------------------------------------------------------------------------- /test/Registry.test.js: -------------------------------------------------------------------------------- 1 | import registryTests from './Registry' 2 | const RegistryMock = artifacts.require('RegistryMock') 3 | const RegistryTokenMock = artifacts.require('RegistryTokenMock') 4 | 5 | contract ('Registry', function ([_, owner, oneHundred, anotherAccount]) { 6 | beforeEach(async function () { 7 | this.registry = await RegistryMock.new({ from: owner }) 8 | await this.registry.initialize({ from: owner }) 9 | }) 10 | 11 | registryTests([owner, oneHundred, anotherAccount]) 12 | }) 13 | -------------------------------------------------------------------------------- /test/helpers/assertBalance.js: -------------------------------------------------------------------------------- 1 | async function assertBalance(token, account, value) { 2 | let balance = await token.balanceOf(account) 3 | assert.equal(balance, value) 4 | } 5 | 6 | module.exports = assertBalance 7 | -------------------------------------------------------------------------------- /test/helpers/assertRevert.js: -------------------------------------------------------------------------------- 1 | module.exports = async function assertRevert(promise) { 2 | let succeeded = false; 3 | try { 4 | await promise; 5 | succeeded = true; 6 | } catch (error) { 7 | const revertFound = error.message.search('revert') >= 0 || error.message.search('invalid opcode') >= 0 || error.message.search('invalid JUMP') >= 0; 8 | assert(revertFound, `Expected "revert", got ${error} instead`); 9 | } 10 | if(succeeded) { 11 | assert.fail('Expected revert not received'); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /test/helpers/bytes32.js: -------------------------------------------------------------------------------- 1 | module.exports = function bytes32(str) { 2 | return web3.utils.padRight(web3.utils.toHex(str), 64); 3 | } 4 | -------------------------------------------------------------------------------- /test/helpers/writeAttributeFor.js: -------------------------------------------------------------------------------- 1 | const canWriteTo = Buffer.from(web3.utils.sha3("canWriteTo-").slice(2), 'hex'); 2 | 3 | function writeAttributeFor(attribute) { 4 | let bytes = Buffer.from(attribute.slice(2), 'hex'); 5 | for (let index = 0; index < canWriteTo.length; index++) { 6 | bytes[index] ^= canWriteTo[index]; 7 | } 8 | return web3.utils.sha3('0x' + bytes.toString('hex')); 9 | } 10 | 11 | 12 | module.exports = writeAttributeFor; 13 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('babel-polyfill'); 3 | 4 | var HDWalletProvider = require("truffle-hdwallet-provider"); 5 | 6 | module.exports = { 7 | compilers: { 8 | solc: { 9 | version: "0.5.13", 10 | evmVersion: "istanbul", 11 | optimizer: { 12 | enabled: true, 13 | runs: 20000 14 | } 15 | }, 16 | }, 17 | plugins: ["solidity-coverage"], 18 | networks: { 19 | development: { 20 | host: "localhost", 21 | port: 8545, 22 | gas: 7990000, 23 | gasPrice: 1, // Specified in Wei 24 | network_id: "*" // Match any network id 25 | }, 26 | ropsten: { 27 | provider: new HDWalletProvider(process.env.MNEMONIC, "https://ropsten.infura.io/"), 28 | network_id: "3", 29 | gas: 7990000, 30 | gasPrice: 22000000000 // Specified in Wei 31 | }, 32 | coverage: { 33 | host: "localhost", 34 | network_id: "*", 35 | port: 8555, 36 | gas: 10000000000000, 37 | gasPrice: 0x01, 38 | }, 39 | rinkeby: { 40 | provider: new HDWalletProvider(process.env.MNEMONIC, "https://rinkeby.infura.io/"), 41 | network_id: "4", 42 | gas: 7990000, 43 | gasPrice: 22000000000 // Specified in Wei 44 | }, 45 | production: { 46 | provider: new HDWalletProvider(process.env.MNEMONIC, "https://mainnet.infura.io/dYWKKqsJkbv9cZlQFEpI "), 47 | network_id: "1", 48 | gas: 7990000, 49 | gasPrice: 7000000000 50 | }, 51 | } 52 | }; 53 | --------------------------------------------------------------------------------