├── .nvmrc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── proposals ├── 0x0000000000000000000000000000000000000000000000000000000000000060.txt ├── 0x00000000000000000000000000000000000000000000000000000000000000a0.txt ├── 0x00000000000000000000000000000000000000000000000000000000000000e0.txt ├── 0xcb1f81e42b5e75f000f94fc71a3ea70cab4bfc6f236b91e717f1b9516e5596b5.txt └── 0x7e825a1e9ee9d3b11e07b43aaa6d086e43f22c97b4b95a76299e3ead4cabb2a2.txt ├── contracts ├── interfaces │ ├── ITreasuryProxy.sol │ ├── ResolverInterface.sol │ └── ENS.sol ├── NFTokenReceiverTestMock.sol ├── Naive.sol ├── Migrations.sol ├── SafeMath8.sol ├── SafeMath16.sol ├── EclipticResolver.sol ├── ReadsAzimuth.sol ├── TakesPoints.sol ├── EclipticBase.sol ├── ENSRegistry.sol ├── PlanetSale.sol ├── Censures.sol ├── Claims.sol ├── DelegatedSending.sol ├── PublicResolver.sol ├── LinearStarRelease.sol └── Polls.sol ├── test ├── helpers │ ├── seeEvents.js │ ├── assertRevert.js │ └── increaseTime.js ├── TestNaive.js ├── TestCensures.js ├── TestClaims.js ├── TestPlanetSale.js ├── TestDelegatedSending.js ├── TestLinearStarRelease.js ├── TestPolls.js ├── TestConditionalStarRelease.js ├── TestAzimuth.js └── TestEcliptic.js ├── .solcover.js ├── CHANGELOG ├── truffle-config.js ├── LICENSE ├── senate.md ├── test-extras ├── SetupMainnetFork.js ├── TestERC721Extensions.js ├── TestEclipticUpgrade.js └── TestERC721Ecliptic.js ├── package.json ├── README.md └── rollup └── tiny.hoon /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.18.0 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | coverage 4 | coverageEnv 5 | coverage.json 6 | scTopics 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '11' 4 | cache: 5 | directories: 6 | - "node_modules" 7 | 8 | script: 9 | - npm run test 10 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function(deployer) { 4 | //deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /proposals/0x0000000000000000000000000000000000000000000000000000000000000060.txt: -------------------------------------------------------------------------------- 1 | Azimuth and Ecliptic are live on the Ethereum blockchain and comprise the Urbit Constitution. 2 | -------------------------------------------------------------------------------- /proposals/0x00000000000000000000000000000000000000000000000000000000000000a0.txt: -------------------------------------------------------------------------------- 1 | Tlon has designated Arvo (the Urbit operating system, as described at https://urbit.org/docs/arvo/internals/) as "stable". 2 | -------------------------------------------------------------------------------- /contracts/interfaces/ITreasuryProxy.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4 <0.9; 2 | 3 | interface ITreasuryProxy { 4 | function upgradeTo(address _impl) external returns (bool); 5 | 6 | function freeze() external returns (bool); 7 | } 8 | -------------------------------------------------------------------------------- /contracts/interfaces/ResolverInterface.sol: -------------------------------------------------------------------------------- 1 | // https://github.com/ethereum/ens 2 | 3 | pragma solidity ^0.4.18; 4 | 5 | contract ResolverInterface { 6 | function addr(bytes32 node) public view returns (address); 7 | function supportsInterface(bytes4 interfaceID) public pure returns (bool); 8 | } 9 | -------------------------------------------------------------------------------- /test/helpers/seeEvents.js: -------------------------------------------------------------------------------- 1 | module.exports = async (promise, events) => { 2 | let logs = (await promise).logs; 3 | if (logs === undefined) 4 | { 5 | assert.equal(events.length, 0, "didn't get any event, expected " + events); 6 | } 7 | else 8 | { 9 | assert.deepEqual(logs.map(l => l.event), events); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /proposals/0x00000000000000000000000000000000000000000000000000000000000000e0.txt: -------------------------------------------------------------------------------- 1 | (i) Tlon has designated the Urbit Network as having reached "continuity" ("continuity" means that there is a reasonable expectation that the Urbit Network can continue indefinitely without hard forks); and (ii) The Urbit Network has been confirmed to be secure by an independent third-party security audit. 2 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | copyPackages: ['openzeppelin-solidity'], 3 | skipFiles: [ 'interfaces/', 4 | 'ENSRegistry.sol', 5 | 'NFTokenReceiverTestMock.sol', 6 | 'PublicResolver.sol', 7 | 'EclipticResolver.sol', 8 | 'SafeMath8.sol', 9 | 'SafeMath16.sol' ] 10 | }; 11 | -------------------------------------------------------------------------------- /test/helpers/assertRevert.js: -------------------------------------------------------------------------------- 1 | module.exports = async (promise) => { 2 | try { 3 | await promise; 4 | // Don't include 'revert' in the message, because we scan for that 5 | assert.fail('succeeded'); 6 | } catch (error) { 7 | const revertFound = error.message.search('revert') >= 0; 8 | assert(revertFound, `Expected "revert", got ${error} instead`); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /contracts/NFTokenReceiverTestMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.24; 2 | 3 | contract NFTokenReceiverTestMock { 4 | 5 | function onERC721Received( 6 | address _operator, 7 | address _from, 8 | uint256 _tokenId, 9 | bytes _data 10 | ) 11 | external 12 | returns(bytes4) 13 | { 14 | _operator; 15 | _from; 16 | _tokenId; 17 | _data; 18 | return 0x150b7a02; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /contracts/Naive.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | contract Naive 4 | { 5 | event Batch(); 6 | 7 | // This function is called for all messages sent to 8 | // this contract (there is no other function). 9 | // Sending Ether to this contract will cause an exception, 10 | // because the fallback function does not have the `payable` 11 | // modifier. 12 | // 13 | function() external 14 | { 15 | emit Batch(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/TestNaive.js: -------------------------------------------------------------------------------- 1 | const Naive = artifacts.require('Naive'); 2 | 3 | const seeEvents = require('./helpers/seeEvents'); 4 | 5 | const web3 = Naive.web3; 6 | 7 | contract('Naive', function([owner, user]) { 8 | let naive; 9 | 10 | before('setting up for tests', async function() { 11 | naive = await Naive.new(); 12 | }); 13 | 14 | it('accepts batch', async function() { 15 | const tx = await web3.eth.sendTransaction({ 16 | from: owner, 17 | to: naive.address, 18 | data: '0x1234', 19 | }); 20 | assert.isTrue(tx.status); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.4; 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() { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/helpers/increaseTime.js: -------------------------------------------------------------------------------- 1 | module.exports = (duration) => { 2 | const id = Date.now(); 3 | 4 | return new Promise((resolve, reject) => { 5 | web3.currentProvider.send({ 6 | jsonrpc: '2.0', 7 | method: 'evm_increaseTime', 8 | params: [duration], 9 | id: id, 10 | }, err1 => { 11 | if (err1) return reject(err1); 12 | 13 | web3.currentProvider.send({ 14 | jsonrpc: '2.0', 15 | method: 'evm_mine', 16 | id: id + 1, 17 | }, (err2, res) => { 18 | return err2 ? reject(err2) : resolve(res); 19 | }); 20 | }); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - 1.2.3 (2021-05-25) 4 | * Makes a minor tweak to the Truffle config script name, which should 5 | better-support additional versions of Truffle. 6 | 7 | - 1.2.2 (2020-07-23) 8 | * Updates Truffle to a recent version and makes miscellaneous minor fixes 9 | to testing and deployment scripts. 10 | 11 | - 1.2.1 (2019-10-22) 12 | * Makes a minor patch to an event parameter in the DelegatedSending 13 | contract. 14 | * Misc improvements to the deployment migration script. 15 | 16 | - 1.2.0 (2019-05-30) 17 | * Adds support for delegated sending from arbitrary stars. (#17) 18 | 19 | - 1.1.0 (2019-04-18) 20 | * Adds contract support for Delegated Sending. 21 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | // Allows us to use ES6 in migrations and tests. 2 | require('babel-register') 3 | require('babel-polyfill') 4 | 5 | module.exports = { 6 | networks: { 7 | development: { 8 | host: "localhost", 9 | port: 8545, 10 | gas: 6000000, 11 | network_id: "*" // Match any network id 12 | } 13 | }, 14 | compilers: { 15 | solc: { 16 | version: "pragma", 17 | settings: { 18 | optimizer: { 19 | enabled: true, 20 | runs: 200 21 | } 22 | } 23 | } 24 | }, 25 | solc: { 26 | optimizer: { 27 | enabled: true, 28 | runs: 200 29 | } 30 | }, 31 | mocha: { 32 | enableTimeouts: false, 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /proposals/0xcb1f81e42b5e75f000f94fc71a3ea70cab4bfc6f236b91e717f1b9516e5596b5.txt: -------------------------------------------------------------------------------- 1 | The document value "0x0000000000000000000000000000000000000000000000000000000000000060" represents the claim that Azimuth and Ecliptic are live on the Ethereum blockchain and comprise the Urbit Constitution. The document value "0x00000000000000000000000000000000000000000000000000000000000000a0" represents the claim that Tlon has designated Arvo (the Urbit operating system, as described at https://urbit.org/docs/arvo/internals/) as "stable". The document value "0x00000000000000000000000000000000000000000000000000000000000000e0" represents the claims that (i) Tlon has designated the Urbit Network as having reached "continuity" ("continuity" means that there is a reasonable expectation that the Urbit Network can continue indefinitely without hard forks); and (ii) The Urbit Network has been confirmed to be secure by an independent third-party security audit. -------------------------------------------------------------------------------- /contracts/SafeMath8.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | /** 4 | * @title SafeMath8 5 | * @dev Math operations for uint8 with safety checks that throw on error 6 | */ 7 | library SafeMath8 { 8 | function mul(uint8 a, uint8 b) internal pure returns (uint8) { 9 | uint8 c = a * b; 10 | assert(a == 0 || c / a == b); 11 | return c; 12 | } 13 | 14 | function div(uint8 a, uint8 b) internal pure returns (uint8) { 15 | // assert(b > 0); // Solidity automatically throws when dividing by 0 16 | uint8 c = a / b; 17 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 18 | return c; 19 | } 20 | 21 | function sub(uint8 a, uint8 b) internal pure returns (uint8) { 22 | assert(b <= a); 23 | return a - b; 24 | } 25 | 26 | function add(uint8 a, uint8 b) internal pure returns (uint8) { 27 | uint8 c = a + b; 28 | assert(c >= a); 29 | return c; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/SafeMath16.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.4.24; 2 | 3 | /** 4 | * @title SafeMath16 5 | * @dev Math operations for uint16 with safety checks that throw on error 6 | */ 7 | library SafeMath16 { 8 | function mul(uint16 a, uint16 b) internal pure returns (uint16) { 9 | uint16 c = a * b; 10 | assert(a == 0 || c / a == b); 11 | return c; 12 | } 13 | 14 | function div(uint16 a, uint16 b) internal pure returns (uint16) { 15 | // assert(b > 0); // Solidity automatically throws when dividing by 0 16 | uint16 c = a / b; 17 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 18 | return c; 19 | } 20 | 21 | function sub(uint16 a, uint16 b) internal pure returns (uint16) { 22 | assert(b <= a); 23 | return a - b; 24 | } 25 | 26 | function add(uint16 a, uint16 b) internal pure returns (uint16) { 27 | uint16 c = a + b; 28 | assert(c >= a); 29 | return c; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/EclipticResolver.sol: -------------------------------------------------------------------------------- 1 | // ENS resolver for the Ecliptic contract 2 | 3 | pragma solidity 0.4.24; 4 | 5 | import './interfaces/ResolverInterface.sol'; 6 | import './Azimuth.sol'; 7 | 8 | contract EclipticResolver is ResolverInterface 9 | { 10 | Azimuth azimuth; 11 | 12 | constructor(Azimuth _azimuth) 13 | public 14 | { 15 | azimuth = _azimuth; 16 | } 17 | 18 | function addr(bytes32 node) 19 | constant 20 | public 21 | returns (address) 22 | { 23 | // resolve to the Ecliptic contract 24 | return azimuth.owner(); 25 | } 26 | 27 | function supportsInterface(bytes4 interfaceID) 28 | pure 29 | public 30 | returns (bool) 31 | { 32 | // supports ERC-137 addr() and ERC-165 33 | return interfaceID == 0x3b3b57de || interfaceID == 0x01ffc9a7; 34 | } 35 | 36 | // ERC-137 resolvers MUST specify a fallback function that throws 37 | function() 38 | public 39 | { 40 | revert(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /proposals/0x7e825a1e9ee9d3b11e07b43aaa6d086e43f22c97b4b95a76299e3ead4cabb2a2.txt: -------------------------------------------------------------------------------- 1 | The state of Urbit ID shall be determined by feeding events into the nock 4K 2 | program whose keccak256 hash is 3 | 4 | 0xf72a.cad7.5633.afa0.c019.8b1c.e7a4.270d.122f.d73f.bab9.ae53.7049.cb33.ec8c.4f2c 5 | 6 | These events will be logs from Azimuth[0] and Ecliptic[1] plus batches from the 7 | Roller contract[2]. 8 | 9 | This will take effect on November 9, 2021 at 9pm UTC. It will apply 10 | retroactively to all events and batches corresponding to those contracts. 11 | 12 | This nock program may be upgraded in the future by a vote of the galaxies. 13 | 14 | Before November 9, 2021 at 9pm UTC, if the Tlon Corporation discovers a security 15 | vulnerability, it may unilaterally cancel this change by publishing a statement 16 | to that effect. In that case, the galaxies may pass an amended proposal. 17 | 18 | [0] 0x223c067F8CF28ae173EE5CafEa60cA44C335fecB 19 | [1] The contract produced by calling owner() on Azimuth at any given time 20 | [2] 0xeb70029CFB3C53c778EAf68Cd28de725390A1fE9 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tlon Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/interfaces/ENS.sol: -------------------------------------------------------------------------------- 1 | // https://github.com/ethereum/ens/blob/master/contracts/ENS.sol 2 | 3 | pragma solidity ^0.4.18; 4 | 5 | interface ENS { 6 | 7 | // Logged when the owner of a node assigns a new owner to a subnode. 8 | event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner); 9 | 10 | // Logged when the owner of a node transfers ownership to a new account. 11 | event Transfer(bytes32 indexed node, address owner); 12 | 13 | // Logged when the resolver for a node changes. 14 | event NewResolver(bytes32 indexed node, address resolver); 15 | 16 | // Logged when the TTL of a node changes 17 | event NewTTL(bytes32 indexed node, uint64 ttl); 18 | 19 | 20 | function setSubnodeOwner(bytes32 node, bytes32 label, address owner) public; 21 | function setResolver(bytes32 node, address resolver) public; 22 | function setOwner(bytes32 node, address owner) public; 23 | function setTTL(bytes32 node, uint64 ttl) public; 24 | function owner(bytes32 node) public view returns (address); 25 | function resolver(bytes32 node) public view returns (address); 26 | function ttl(bytes32 node) public view returns (uint64); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /senate.md: -------------------------------------------------------------------------------- 1 | # Galactic Senate 2 | 3 | The following is a suggestion to the Galactic Senate on how to expose its proposals to the public. The Senate is not required to adhere to this procedure, and the process described herein may be replaced by alternate suggestions. 4 | 5 | At any point, before or after submission to the blockchain, before or after achieving majority, anyone may create a Pull Request containing a proposal. The proposal does not need to be fully-formed, creating a PR with a suggestion for a proposal is valid. 6 | 7 | For Ecliptic proposals, the final PR should contain the new code for `contracts/Ecliptic.sol`, and any other changes needed to support it. If the contract has already been deployed to the Ethereum blockchain, the PR may mention its address. 8 | 9 | For document proposals, the final PR should contain the addition of a `.txt` file in `proposals/`, where its filename is the `keccak-256` hash of its contents. 10 | 11 | When a proposal has achieved majority, as verified by looking up either the proposed Ecliptic contract address or the document's hash in `Polls.sol`'s `upgradeHasAchievedMajority(address)` or `documentHasAchievedMajority(bytes32)` respectively, the PR matching that proposal will be merged. -------------------------------------------------------------------------------- /test-extras/SetupMainnetFork.js: -------------------------------------------------------------------------------- 1 | 2 | const { spawn } = require('child_process'); 3 | const Web3 = require('web3'); 4 | 5 | const node = 'https://mainnet.infura.io/v3/2599df54929b47099bda360958d75aaf'; 6 | const web3 = new Web3(node); 7 | 8 | const template = { 9 | to: '0x223c067F8CF28ae173EE5CafEa60cA44C335fecB', 10 | data: web3.utils.sha3('getOwner(uint32)').slice(0, 10) 11 | + '00000000000000000000000000000000000000000000000000000000000000' 12 | }; 13 | 14 | async function go() { 15 | console.log('setting up...'); 16 | let senators = []; 17 | for (let i = 0; i < 129; i++) { 18 | let hex = i.toString(16); 19 | if (hex.length == 1) hex = '0' + hex; 20 | const callObj = { 21 | ...template, 22 | data: template.data + hex 23 | } 24 | const res = await web3.eth.call(callObj); 25 | senators.push('0x' + res.slice(26)); 26 | } 27 | senators = [...new Set(senators)]; 28 | const unlocks = senators.reduce((acc, cur) => { 29 | if (typeof acc === 'string') acc = ['--unlock', acc]; 30 | acc.push('--unlock'); 31 | acc.push(cur); 32 | return acc; 33 | }); 34 | 35 | spawn('ganache-cli', ['--fork', node, ...unlocks]); 36 | 37 | console.log('ready!'); 38 | return; 39 | } 40 | 41 | return go(); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azimuth-solidity", 3 | "version": "1.2.4", 4 | "description": "A general-purpose PKI on the Ethereum blockchain.", 5 | "main": "truffle.js", 6 | "engines": { 7 | "node": "16.18.0" 8 | }, 9 | "directories": { 10 | "test": "test" 11 | }, 12 | "dependencies": { 13 | "babel-polyfill": "^6.26.0", 14 | "babel-register": "^6.26.0", 15 | "openzeppelin-solidity": "1.12.0" 16 | }, 17 | "devDependencies": { 18 | "ganache-cli": "^6.12.2", 19 | "npm-run-all": "^4.1.3", 20 | "solidity-coverage": "^0.7.5", 21 | "web3": "^1.3.1", 22 | "web3-eth-abi": "^1.0.0-beta.34" 23 | }, 24 | "bundledDependencies": [ 25 | "openzeppelin-solidity" 26 | ], 27 | "scripts": { 28 | "build": "truffle compile", 29 | "test:ganache": "ganache-cli --gasLimit 6000000 > /dev/null &", 30 | "test:setup": "npm run build && npm run test:ganache", 31 | "test:truffle": "truffle test", 32 | "test:extras:upgrade": "truffle test ./test-extras/TestEclipticUpgrade.js", 33 | "test:extras:ecliptic": "truffle test ./test-extras/TestERC721Ecliptic.js", 34 | "test:extras:extensions": "truffle test ./test-extras/TestERC721Extensions.js", 35 | "test:cleanup": "pkill -f ganache-cli", 36 | "test": "npm-run-all test:setup test:truffle test:cleanup --continue-on-error", 37 | "test-extras": "npm-run-all test:setup test:extras:ecliptic test:extras:extensions test:cleanup --continue-on-error", 38 | "fork-mainnet": "node ./test-extras/SetupMainnetFork.js", 39 | "test-upgrade": "npm-run-all test:extras:upgrade test:cleanup --continue-on-error", 40 | "install": "npm run build" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/urbit/azimuth.git" 45 | }, 46 | "author": "Tlon", 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/urbit/azimuth/issues" 50 | }, 51 | "homepage": "https://github.com/urbit/azimuth#readme" 52 | } 53 | -------------------------------------------------------------------------------- /contracts/ReadsAzimuth.sol: -------------------------------------------------------------------------------- 1 | // contract that uses the Azimuth data contract 2 | 3 | pragma solidity 0.4.24; 4 | 5 | import './Azimuth.sol'; 6 | 7 | // ReadsAzimuth: referring to and testing against the Azimuth 8 | // data contract 9 | // 10 | // To avoid needless repetition, this contract provides common 11 | // checks and operations using the Azimuth contract. 12 | // 13 | contract ReadsAzimuth 14 | { 15 | // azimuth: points data storage contract. 16 | // 17 | Azimuth public azimuth; 18 | 19 | // constructor(): set the Azimuth data contract's address 20 | // 21 | constructor(Azimuth _azimuth) 22 | public 23 | { 24 | azimuth = _azimuth; 25 | } 26 | 27 | // activePointOwner(): require that :msg.sender is the owner of _point, 28 | // and that _point is active 29 | // 30 | modifier activePointOwner(uint32 _point) 31 | { 32 | require( azimuth.isOwner(_point, msg.sender) && 33 | azimuth.isActive(_point) ); 34 | _; 35 | } 36 | 37 | // activePointManager(): require that :msg.sender can manage _point, 38 | // and that _point is active 39 | // 40 | modifier activePointManager(uint32 _point) 41 | { 42 | require( azimuth.canManage(_point, msg.sender) && 43 | azimuth.isActive(_point) ); 44 | _; 45 | } 46 | 47 | // activePointSpawner(): require that :msg.sender can spawn as _point, 48 | // and that _point is active 49 | // 50 | modifier activePointSpawner(uint32 _point) 51 | { 52 | require( azimuth.canSpawnAs(_point, msg.sender) && 53 | azimuth.isActive(_point) ); 54 | _; 55 | } 56 | 57 | // activePointVoter(): require that :msg.sender can vote as _point, 58 | // and that _point is active 59 | // 60 | modifier activePointVoter(uint32 _point) 61 | { 62 | require( azimuth.canVoteAs(_point, msg.sender) && 63 | azimuth.isActive(_point) ); 64 | _; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test-extras/TestERC721Extensions.js: -------------------------------------------------------------------------------- 1 | // adapted from: 2 | // https://github.com/0xcert/ethereum-erc721/blob/master/test/tokens/NFTokenMetadataEnumerable.test.js 3 | 4 | const Azimuth = artifacts.require('Azimuth'); 5 | const Polls = artifacts.require('Polls'); 6 | const Claims = artifacts.require('Claims'); 7 | const Ecliptic = artifacts.require('Ecliptic'); 8 | 9 | const assertRevert = require('../test/helpers/assertRevert'); 10 | 11 | contract('NFTokenMetadataMock', (accounts) => { 12 | let azimuth, polls, claims, nftoken; 13 | const id1 = 1; 14 | const id2 = 2; 15 | const id3 = 3; 16 | const id4 = 4294967297; 17 | 18 | beforeEach(async () => { 19 | azimuth = await Azimuth.new(); 20 | polls = await Polls.new(432000, 432000); 21 | claims = await Claims.new(azimuth.address); 22 | nftoken = await Ecliptic.new('0x0000000000000000000000000000000000000000', 23 | azimuth.address, 24 | polls.address, 25 | claims.address); 26 | azimuth.transferOwnership(nftoken.address); 27 | polls.transferOwnership(nftoken.address); 28 | }); 29 | 30 | it('correctly checks all the supported interfaces', async () => { 31 | const nftokenInterface = await nftoken.supportsInterface('0x80ac58cd'); 32 | const nftokenMetadataInterface = await nftoken.supportsInterface('0x5b5e139f'); 33 | assert.equal(nftokenInterface, true); 34 | assert.equal(nftokenMetadataInterface, true); 35 | }); 36 | 37 | it('returns the correct issuer name', async () => { 38 | const name = await nftoken.name(); 39 | assert.equal(name, 'Azimuth Points'); 40 | }); 41 | 42 | it('returns the correct issuer symbol', async () => { 43 | const symbol = await nftoken.symbol(); 44 | assert.equal(symbol, 'AZP'); 45 | }); 46 | 47 | it('returns the correct NFT id 2 url', async () => { 48 | await nftoken.createGalaxy(id2, accounts[1]); 49 | const tokenURI = await nftoken.tokenURI(id2); 50 | assert.equal(tokenURI, 'https://azimuth.network/erc721/0000000002.json'); 51 | }); 52 | 53 | it('throws when trying to get uri of none existant NFT id', async () => { 54 | await assertRevert(nftoken.tokenURI(id4)); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/TestCensures.js: -------------------------------------------------------------------------------- 1 | const Azimuth = artifacts.require('Azimuth'); 2 | const Censures = artifacts.require('Censures'); 3 | 4 | const assertRevert = require('./helpers/assertRevert'); 5 | 6 | contract('Censures', function([owner, user]) { 7 | let azimuth, cens; 8 | 9 | before('setting up for tests', async function() { 10 | azimuth = await Azimuth.new(); 11 | await azimuth.setOwner(0, owner); 12 | await azimuth.activatePoint(0); 13 | await azimuth.setOwner(256, owner); 14 | await azimuth.activatePoint(256); 15 | await azimuth.setOwner(65792, owner); 16 | await azimuth.activatePoint(65792); 17 | cens = await Censures.new(azimuth.address); 18 | }); 19 | 20 | it('censuring', async function() { 21 | assert.equal(await cens.getCensuringCount(0), 0); 22 | // stars can't censor galaxies. 23 | await assertRevert(cens.censure(256, 0)); 24 | // can't self-censor. 25 | await assertRevert(cens.censure(256, 256)); 26 | // only point owner can do this. 27 | await assertRevert(cens.censure(0, 1, {from:user})); 28 | await cens.censure(0, 1); 29 | assert.equal(await cens.getCensuringCount(0), 1); 30 | // can't censure twice. 31 | await assertRevert(cens.censure(0, 1)); 32 | await cens.censure(0, 2); 33 | await cens.censure(0, 3); 34 | await cens.censure(0, 4); 35 | let censures = await cens.getCensuring(0); 36 | assert.equal(censures[0].toNumber(), 1); 37 | assert.equal(censures[1].toNumber(), 2); 38 | assert.equal(censures[2].toNumber(), 3); 39 | assert.equal(censures[3].toNumber(), 4); 40 | // check reverse lookup 41 | assert.equal(await cens.getCensuredByCount(1), 1); 42 | let censured = await cens.getCensuredBy(1); 43 | assert.equal(censured[0].toNumber(), 0); 44 | }); 45 | 46 | it('forgiving', async function() { 47 | // can't forgive the uncensured. 48 | await assertRevert(cens.forgive(0, 5)); 49 | // only point owner can do this. 50 | await assertRevert(cens.forgive(0, 2, {from:user})); 51 | await cens.forgive(0, 2); 52 | let censures = await cens.getCensuring(0); 53 | assert.equal(censures[0].toNumber(), 1); 54 | assert.equal(censures[1].toNumber(), 4); 55 | assert.equal(censures[2].toNumber(), 3); 56 | assert.equal(await cens.getCensuringCount(0), 3); 57 | assert.equal(await cens.getCensuredByCount(2), 0); 58 | // ensure we can safely interact with a censure that got moved internally 59 | await cens.forgive(0, 4); 60 | censures = await cens.getCensuring(0); 61 | assert.equal(censures[0].toNumber(), 1); 62 | assert.equal(censures[1].toNumber(), 3); 63 | assert.equal(await cens.getCensuringCount(0), 2); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /contracts/TakesPoints.sol: -------------------------------------------------------------------------------- 1 | // contract that takes and gives Azimuth points 2 | 3 | pragma solidity 0.4.24; 4 | 5 | import './Ecliptic.sol'; 6 | import './ReadsAzimuth.sol'; 7 | 8 | contract TakesPoints is ReadsAzimuth 9 | { 10 | constructor(Azimuth _azimuth) 11 | ReadsAzimuth(_azimuth) 12 | public 13 | { 14 | // 15 | } 16 | 17 | // takePoint(): transfer _point to this contract. if _clean is true, require 18 | // that the point be unlinked. 19 | // returns true if this succeeds, false otherwise. 20 | // 21 | function takePoint(uint32 _point, bool _clean) 22 | internal 23 | returns (bool success) 24 | { 25 | Ecliptic ecliptic = Ecliptic(azimuth.owner()); 26 | 27 | // There are two ways for a contract to get a point. 28 | // One way is for a prefix point to grant the contract permission to 29 | // spawn its points. 30 | // The contract will spawn the point directly to itself. 31 | // 32 | uint16 prefix = azimuth.getPrefix(_point); 33 | if ( azimuth.isOwner(_point, 0x0) && 34 | azimuth.isOwner(prefix, msg.sender) && 35 | azimuth.isSpawnProxy(prefix, this) && 36 | (ecliptic.getSpawnLimit(prefix, now) > azimuth.getSpawnCount(prefix)) ) 37 | { 38 | // first model: spawn _point to :this contract 39 | // 40 | ecliptic.spawn(_point, this); 41 | return true; 42 | } 43 | 44 | // The second way is to accept existing points, optionally 45 | // requiring they be unlinked. 46 | // To deposit a point this way, the owner grants the contract 47 | // permission to transfer ownership of the point. 48 | // The contract will transfer the point to itself. 49 | // 50 | if ( (!_clean || !azimuth.hasBeenLinked(_point)) && 51 | azimuth.isOwner(_point, msg.sender) && 52 | azimuth.canTransfer(_point, this) ) 53 | { 54 | // second model: transfer active, unlinked _point to :this contract 55 | // 56 | ecliptic.transferPoint(_point, this, true); 57 | return true; 58 | } 59 | 60 | // point is not for us to take 61 | // 62 | return false; 63 | } 64 | 65 | // givePoint(): transfer a _point we own to _to, optionally resetting. 66 | // returns true if this succeeds, false otherwise. 67 | // 68 | // Note that _reset is unnecessary if the point was taken 69 | // using this contract's takePoint() function, which always 70 | // resets, and not touched since. 71 | // 72 | function givePoint(uint32 _point, address _to, bool _reset) 73 | internal 74 | returns (bool success) 75 | { 76 | // only give points we've taken, points we fully own 77 | // 78 | if (azimuth.isOwner(_point, this)) 79 | { 80 | Ecliptic(azimuth.owner()).transferPoint(_point, _to, _reset); 81 | return true; 82 | } 83 | 84 | // point is not for us to give 85 | // 86 | return false; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /contracts/EclipticBase.sol: -------------------------------------------------------------------------------- 1 | // base contract for the azimuth logic contract 2 | // encapsulates dependencies all ecliptics need. 3 | 4 | pragma solidity 0.4.24; 5 | 6 | import './ReadsAzimuth.sol'; 7 | import './Polls.sol'; 8 | 9 | import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; 10 | 11 | // EclipticBase: upgradable ecliptic 12 | // 13 | // This contract implements the upgrade logic for the Ecliptic. 14 | // Newer versions of the Ecliptic are expected to provide at least 15 | // the onUpgrade() function. If they don't, upgrading to them will 16 | // fail. 17 | // 18 | // Note that even though this contract doesn't specify any required 19 | // interface members aside from upgrade() and onUpgrade(), contracts 20 | // and clients may still rely on the presence of certain functions 21 | // provided by the Ecliptic proper. Keep this in mind when writing 22 | // new versions of it. 23 | // 24 | contract EclipticBase is Ownable, ReadsAzimuth 25 | { 26 | // Upgraded: _to is the new canonical Ecliptic 27 | // 28 | event Upgraded(address to); 29 | 30 | // polls: senate voting contract 31 | // 32 | Polls public polls; 33 | 34 | // previousEcliptic: address of the previous ecliptic this 35 | // instance expects to upgrade from, stored and 36 | // checked for to prevent unexpected upgrade paths 37 | // 38 | address public previousEcliptic; 39 | 40 | constructor( address _previous, 41 | Azimuth _azimuth, 42 | Polls _polls ) 43 | ReadsAzimuth(_azimuth) 44 | internal 45 | { 46 | previousEcliptic = _previous; 47 | polls = _polls; 48 | } 49 | 50 | // onUpgrade(): called by previous ecliptic when upgrading 51 | // 52 | // in future ecliptics, this might perform more logic than 53 | // just simple checks and verifications. 54 | // when overriding this, make sure to call this original as well. 55 | // 56 | function onUpgrade() 57 | external 58 | { 59 | // make sure this is the expected upgrade path, 60 | // and that we have gotten the ownership we require 61 | // 62 | require( msg.sender == previousEcliptic && 63 | this == azimuth.owner() && 64 | this == polls.owner() ); 65 | } 66 | 67 | // upgrade(): transfer ownership of the ecliptic data to the new 68 | // ecliptic contract, notify it, then self-destruct. 69 | // 70 | // Note: any eth that have somehow ended up in this contract 71 | // are also sent to the new ecliptic. 72 | // 73 | function upgrade(EclipticBase _new) 74 | internal 75 | { 76 | // transfer ownership of the data contracts 77 | // 78 | azimuth.transferOwnership(_new); 79 | polls.transferOwnership(_new); 80 | 81 | // trigger upgrade logic on the target contract 82 | // 83 | _new.onUpgrade(); 84 | 85 | // emit event and destroy this contract 86 | // 87 | emit Upgraded(_new); 88 | selfdestruct(_new); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/ENSRegistry.sol: -------------------------------------------------------------------------------- 1 | // https://github.com/ethereum/ens/blob/master/contracts/ENSRegistry.sol 2 | 3 | pragma solidity ^0.4.18; 4 | 5 | import './interfaces/ENS.sol'; 6 | 7 | /** 8 | * The ENS registry contract. 9 | */ 10 | contract ENSRegistry is ENS { 11 | struct Record { 12 | address owner; 13 | address resolver; 14 | uint64 ttl; 15 | } 16 | 17 | mapping (bytes32 => Record) records; 18 | 19 | // Permits modifications only by the owner of the specified node. 20 | modifier only_owner(bytes32 node) { 21 | require(records[node].owner == msg.sender); 22 | _; 23 | } 24 | 25 | /** 26 | * @dev Constructs a new ENS registrar. 27 | */ 28 | constructor() public { 29 | records[0x0].owner = msg.sender; 30 | } 31 | 32 | /** 33 | * @dev Transfers ownership of a node to a new address. May only be called by the current owner of the node. 34 | * @param node The node to transfer ownership of. 35 | * @param owner The address of the new owner. 36 | */ 37 | function setOwner(bytes32 node, address owner) public only_owner(node) { 38 | emit Transfer(node, owner); 39 | records[node].owner = owner; 40 | } 41 | 42 | /** 43 | * @dev Transfers ownership of a subnode keccak256(node, label) to a new address. May only be called by the owner of the parent node. 44 | * @param node The parent node. 45 | * @param label The hash of the label specifying the subnode. 46 | * @param owner The address of the new owner. 47 | */ 48 | function setSubnodeOwner(bytes32 node, bytes32 label, address owner) public only_owner(node) { 49 | var subnode = keccak256(abi.encodePacked(node, label)); 50 | emit NewOwner(node, label, owner); 51 | records[subnode].owner = owner; 52 | } 53 | 54 | /** 55 | * @dev Sets the resolver address for the specified node. 56 | * @param node The node to update. 57 | * @param resolver The address of the resolver. 58 | */ 59 | function setResolver(bytes32 node, address resolver) public only_owner(node) { 60 | emit NewResolver(node, resolver); 61 | records[node].resolver = resolver; 62 | } 63 | 64 | /** 65 | * @dev Sets the TTL for the specified node. 66 | * @param node The node to update. 67 | * @param ttl The TTL in seconds. 68 | */ 69 | function setTTL(bytes32 node, uint64 ttl) public only_owner(node) { 70 | emit NewTTL(node, ttl); 71 | records[node].ttl = ttl; 72 | } 73 | 74 | /** 75 | * @dev Returns the address that owns the specified node. 76 | * @param node The specified node. 77 | * @return address of the owner. 78 | */ 79 | function owner(bytes32 node) public view returns (address) { 80 | return records[node].owner; 81 | } 82 | 83 | /** 84 | * @dev Returns the address of the resolver for the specified node. 85 | * @param node The specified node. 86 | * @return address of the resolver. 87 | */ 88 | function resolver(bytes32 node) public view returns (address) { 89 | return records[node].resolver; 90 | } 91 | 92 | /** 93 | * @dev Returns the TTL of a node, and any records associated with it. 94 | * @param node The specified node. 95 | * @return ttl of the node. 96 | */ 97 | function ttl(bytes32 node) public view returns (uint64) { 98 | return records[node].ttl; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /test-extras/TestEclipticUpgrade.js: -------------------------------------------------------------------------------- 1 | // tests upgrade to either the ecliptic included in the repo, 2 | // or a specified target already on-chain 3 | 4 | let nuEclipticAddr = undefined; 5 | const cooked = JSON.parse(process.env.npm_config_argv).cooked; 6 | if (cooked[1] === '--target') { 7 | nuEclipticAddr = cooked[2]; 8 | } 9 | 10 | const Azimuth = artifacts.require('Azimuth'); 11 | const Ecliptic = artifacts.require('Ecliptic'); 12 | const Polls = artifacts.require('Polls'); 13 | const Claims = artifacts.require('Claims'); 14 | 15 | const azimuthAddr = '0x223c067F8CF28ae173EE5CafEa60cA44C335fecB'; 16 | 17 | contract('Ecliptic', function() { 18 | let azimuth, ecliptic, polls, nuEcliptic, pollsAddr, senators; 19 | 20 | before('setting up for tests', async function() { 21 | // get existing contracts 22 | // 23 | azimuth = await Azimuth.at(azimuthAddr); 24 | const eclipticAddr = await azimuth.owner(); 25 | ecliptic = await Ecliptic.at(eclipticAddr); 26 | pollsAddr = await ecliptic.polls(); 27 | polls = await Polls.at(pollsAddr); 28 | claimsAddr = await ecliptic.claims(); 29 | 30 | console.log('upgrading from', eclipticAddr); 31 | 32 | // find addresses to use for voting 33 | // 34 | console.log('indexing senators...'); 35 | senators = []; 36 | for (let i = 0; i < 129; i++) { 37 | senators.push(azimuth.getOwner(i)); 38 | } 39 | senators = await Promise.all(senators); 40 | 41 | // deploy new contracts 42 | // 43 | if (nuEclipticAddr) { 44 | nuEcliptic = await Ecliptic.at(nuEclipticAddr); 45 | } else { 46 | nuEcliptic = await Ecliptic.new(eclipticAddr, azimuthAddr, pollsAddr, claimsAddr, '0x0000000000000000000000000000000000000000'); 47 | nuEclipticAddr = nuEcliptic.address; 48 | } 49 | console.log('new ecliptic at', nuEclipticAddr); 50 | }); 51 | 52 | it('can be upgraded to', async function() { 53 | // start poll, cast majority vote 54 | // 55 | console.log('casting votes...'); 56 | const poll = await polls.upgradePolls(nuEclipticAddr); 57 | const start = poll.start.toNumber(); 58 | const duration = poll.duration.toNumber(); 59 | const cooldown = poll.cooldown.toNumber(); 60 | if (start === 0 || (start + duration + cooldown) < (Date.now() / 1000)) { 61 | await ecliptic.startUpgradePoll(0, nuEclipticAddr, { 62 | from: senators[0], 63 | gasPrice: 0 64 | }); 65 | } 66 | for (let i = 0; i < 129; i++) { 67 | await ecliptic.castUpgradeVote(i, nuEclipticAddr, true, { 68 | from: senators[i], 69 | gasPrice: 0 70 | }); 71 | } 72 | assert.equal(await azimuth.owner(), nuEclipticAddr); 73 | }); 74 | 75 | it('can still do transfer', async function() { 76 | assert.isFalse(await azimuth.isOwner(0, senators[1])); //NOTE fragile 77 | await nuEcliptic.transferPoint(0, senators[1], false, { 78 | from: senators[0], 79 | gasPrice: 0 80 | }); 81 | assert.isTrue(await azimuth.isOwner(0, senators[1])); 82 | senators[0] = senators[1]; 83 | }); 84 | 85 | it('can still upgrade', async function () { 86 | const third = await Ecliptic.new(nuEclipticAddr, azimuthAddr, pollsAddr, await nuEcliptic.claims(), '0x0000000000000000000000000000000000000000'); 87 | await nuEcliptic.startUpgradePoll(0, third.address, { 88 | from: senators[0], 89 | gasPrice: 0 90 | }); 91 | console.log('casting votes...'); 92 | for (let i = 0; i < 129; i++) { 93 | await nuEcliptic.castUpgradeVote(i, third.address, true, { 94 | from: senators[i], 95 | gasPrice: 0 96 | }); 97 | } 98 | assert.equal(await azimuth.owner(), third.address); 99 | }); 100 | 101 | }); 102 | 103 | -------------------------------------------------------------------------------- /test/TestClaims.js: -------------------------------------------------------------------------------- 1 | const Azimuth = artifacts.require('Azimuth'); 2 | const Claims = artifacts.require('Claims'); 3 | 4 | const assertRevert = require('./helpers/assertRevert'); 5 | const seeEvents = require('./helpers/seeEvents'); 6 | 7 | contract('Claims', function([owner, user]) { 8 | let azimuth, claims; 9 | 10 | before('setting up for tests', async function() { 11 | azimuth = await Azimuth.new(); 12 | await azimuth.setOwner(0, owner); 13 | await azimuth.activatePoint(0); 14 | claims = await Claims.new(azimuth.address); 15 | }); 16 | 17 | it('claiming', async function() { 18 | // only point owner can do this. 19 | await assertRevert(claims.addClaim(0, "prot1", "claim", "0x0", {from:user})); 20 | // cannot set empty claim/protocol 21 | await assertRevert(claims.addClaim(0, "prot1", "", "0x0")); 22 | await assertRevert(claims.addClaim(0, "", "claim", "0x0")); 23 | await seeEvents(claims.addClaim(0, "prot1", "claim", "0x0"), ['ClaimAdded']); 24 | // can update the proof. 25 | await claims.addClaim(0, "prot1", "claim", "0x01"); 26 | await claims.addClaim(0, "prot2", "claim", "0x02"); 27 | await claims.addClaim(0, "prot3", "claim", "0x03"); 28 | await claims.addClaim(0, "prot3", "claim4", "0x04"); 29 | let clam0 = await claims.claims(0, 0); 30 | assert.equal(clam0[0], "prot1"); 31 | assert.equal(clam0[1], "claim"); 32 | assert.equal(clam0[2], "0x01"); 33 | let clam3 = await claims.claims(0, 3); 34 | assert.equal(clam3[0], "prot3"); 35 | assert.equal(clam3[1], "claim4"); 36 | assert.equal(clam3[2], "0x04"); 37 | let clam4 = await claims.claims(0, 4); 38 | assert.equal(clam4[0], ""); 39 | assert.equal(clam4[1], ""); 40 | assert.equal(clam4[2], null); 41 | }); 42 | 43 | it('removing claim', async function() { 44 | // only point owner can do this. 45 | await assertRevert(claims.removeClaim(0, "prot2", "claim", {from:user})); 46 | // can't remove non-existent claim 47 | await assertRevert(claims.removeClaim(0, "prot2", "!!!")); 48 | await seeEvents(claims.removeClaim(0, "prot2", "claim"), ['ClaimRemoved']); 49 | let clam1 = await claims.claims(0, 1); 50 | assert.equal(clam1[0], ""); 51 | assert.equal(clam1[1], ""); 52 | assert.equal(clam1[2], null); 53 | await claims.addClaim(0, "prot2", "claim2", "0x22"); 54 | clam1 = await claims.claims(0, 1); 55 | assert.equal(clam1[0], "prot2"); 56 | assert.equal(clam1[1], "claim2"); 57 | assert.equal(clam1[2], "0x22"); 58 | }); 59 | 60 | it('clearing claims', async function() { 61 | // fill up with claims to ensure we can run the most expensive case 62 | for (var i = 0; i < 16-4; i++) { 63 | await claims.addClaim(0, "some protocol", "some claim "+i, "0x0"); 64 | } 65 | // can't go over the limit 66 | await assertRevert(claims.addClaim(0, "some protocol", "some claim", "0x0")); 67 | let clam16 = await claims.claims(0, 15); 68 | assert.equal(clam16[0], "some protocol"); 69 | assert.equal(clam16[1], "some claim "+(15-4)); 70 | assert.equal(clam16[2], "0x00"); 71 | // only point owner (and ecliptic) can clear 72 | await assertRevert(claims.clearClaims(0, {from:user})); 73 | // removing one ahead of clearClaims so we can make sure it emits only as 74 | // many events as claims that were actually removed 75 | await claims.removeClaim(0, "some protocol", "some claim 0"); 76 | await seeEvents(claims.clearClaims(0), Array(15).fill('ClaimRemoved')); 77 | for (var i = 0; i < 16; i++) { 78 | let clam = await claims.claims(0, i); 79 | assert.equal(clam[0], ""); 80 | assert.equal(clam[1], ""); 81 | assert.equal(clam[2], null); 82 | } 83 | // make sure things still work as expected. 84 | await claims.addClaim(0, "prot1", "claim", "0x01"); 85 | await claims.addClaim(0, "prot2", "claim", "0x02"); 86 | let clam0 = await claims.claims(0, 1); 87 | assert.equal(clam0[0], "prot2"); 88 | assert.equal(clam0[1], "claim"); 89 | assert.equal(clam0[2], "0x02"); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | var Azimuth = artifacts.require("Azimuth"); 2 | var Polls = artifacts.require("Polls"); 3 | var Claims = artifacts.require("Claims"); 4 | var Censures = artifacts.require("Censures"); 5 | var Ecliptic = artifacts.require("Ecliptic"); 6 | var DelegatedSending = artifacts.require("DelegatedSending"); 7 | var LinearStarRelease = artifacts.require("LinearStarRelease"); 8 | var ConditionalStarRelease = artifacts.require("ConditionalStarRelease"); 9 | var Naive = artifacts.require("Naive"); 10 | 11 | const WITH_TEST_STATE = process.argv[3] === "with-state"; 12 | 13 | const windup = 20; 14 | const rateUnit = 50; 15 | const deadlineStep = 100; 16 | const condit2 = web3.utils.fromAscii("1234"); 17 | const escapeHatchTime = deadlineStep * 100; 18 | 19 | async function getChainTime() { 20 | const block = await web3.eth.getBlock("latest"); 21 | 22 | return block.timestamp; 23 | } 24 | 25 | module.exports = async function(deployer, network, accounts) { 26 | await deployer; 27 | 28 | // setup contracts 29 | const azimuth = await deployer.deploy(Azimuth); 30 | const polls = await deployer.deploy(Polls, 1209600, 604800); 31 | const claims = await deployer.deploy(Claims, azimuth.address); 32 | const censures = await deployer.deploy(Censures, azimuth.address); 33 | const naive = await deployer.deploy(Naive); 34 | 35 | 36 | //NOTE for real deployment, use a real ENS registry 37 | const ecliptic = await deployer.deploy( 38 | Ecliptic, 39 | "0x0000000000000000000000000000000000000000", 40 | azimuth.address, 41 | polls.address, 42 | claims.address, 43 | "0x0000000000000000000000000000000000000000" 44 | ); 45 | 46 | // configure contract ownership 47 | await azimuth.transferOwnership(ecliptic.address); 48 | await polls.transferOwnership(ecliptic.address); 49 | 50 | // deploy secondary contracts 51 | const sending = await deployer.deploy(DelegatedSending, azimuth.address); 52 | 53 | const deadline1 = web3.utils.toDecimal(await getChainTime()) + 10; 54 | const deadline2 = deadline1 + deadlineStep; 55 | const escapeHatchDate = 56 | web3.utils.toDecimal(await getChainTime()) + escapeHatchTime; 57 | 58 | const conditionalSR = await deployer.deploy( 59 | ConditionalStarRelease, 60 | azimuth.address, 61 | ["0x0", condit2], 62 | [0, 0], 63 | [deadline1, deadline2], 64 | escapeHatchDate 65 | ); 66 | const linearSR = await deployer.deploy(LinearStarRelease, azimuth.address); 67 | linearSR.startReleasing(); 68 | 69 | 70 | // beyond this point: "default" state for qa & testing purposes 71 | if (!WITH_TEST_STATE) return; 72 | 73 | const own = await ecliptic.owner(); 74 | const releaseUser = accounts[1]; 75 | 76 | 77 | await ecliptic.createGalaxy(0, own); 78 | await ecliptic.configureKeys(0, "0x123", "0x456", 1, false); 79 | await ecliptic.spawn(256, own); 80 | await ecliptic.configureKeys(256, "0x456", "0x789", 1, false); 81 | // set transfer proxy to delegated sending, very brittle 82 | await ecliptic.setSpawnProxy(256, sending.address); 83 | await ecliptic.spawn(65792, own); 84 | await ecliptic.spawn(131328, own); 85 | await ecliptic.spawn(512, own); 86 | await sending.setPoolSize(256, 65792, 1000); 87 | 88 | // Linear Star Release 89 | await ecliptic.createGalaxy(1, own); 90 | 91 | await ecliptic.configureKeys(1, "0xABC", "0xDEF", 1, false); 92 | 93 | await ecliptic.setSpawnProxy(1, linearSR.address); 94 | 95 | await linearSR.register(releaseUser, windup, 8, 2, rateUnit); 96 | 97 | // Conditional Star Release 98 | await ecliptic.createGalaxy(2, own); 99 | await ecliptic.configureKeys(2, "0x321", "0x654", 1, false); 100 | 101 | await ecliptic.setSpawnProxy(2, conditionalSR.address); 102 | 103 | await conditionalSR.register(releaseUser, [4, 4], 1, rateUnit); 104 | 105 | for (let i = 1; i < 9; i++) { 106 | const offset = 256 * i; 107 | await linearSR.deposit(releaseUser, offset + 1); 108 | await conditionalSR.deposit(releaseUser, offset + 2); 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /contracts/PlanetSale.sol: -------------------------------------------------------------------------------- 1 | // bare-bones sample planet sale contract 2 | 3 | pragma solidity 0.4.24; 4 | 5 | import './Ecliptic.sol'; 6 | 7 | import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; 8 | 9 | // PlanetSale: a practically stateless point sale contract 10 | // 11 | // This contract facilitates the sale of points (most commonly planets). 12 | // Instead of "depositing" points into this contract, points are 13 | // available for sale when this contract is able to spawn them. 14 | // This is the case when the point is inactive and its prefix has 15 | // allowed this contract to spawn for it. 16 | // 17 | // The contract owner can determine the price per point, withdraw funds 18 | // that have been sent to this contract, and shut down the contract 19 | // to prevent further sales. 20 | // 21 | // This contract is intended to be deployed by star owners that want 22 | // to sell their planets on-chain. 23 | // 24 | contract PlanetSale is Ownable 25 | { 26 | // PlanetSold: _planet has been sold 27 | // 28 | event PlanetSold(uint32 indexed prefix, uint32 indexed planet); 29 | 30 | // azimuth: points state data store 31 | // 32 | Azimuth public azimuth; 33 | 34 | // price: ether per planet, in wei 35 | // 36 | uint256 public price; 37 | 38 | // constructor(): configure the points data store and initial sale price 39 | // 40 | constructor(Azimuth _azimuth, uint256 _price) 41 | public 42 | { 43 | require(0 < _price); 44 | azimuth = _azimuth; 45 | setPrice(_price); 46 | } 47 | 48 | // 49 | // Buyer operations 50 | // 51 | 52 | // available(): returns true if the _planet is available for purchase 53 | // 54 | function available(uint32 _planet) 55 | public 56 | view 57 | returns (bool result) 58 | { 59 | uint16 prefix = azimuth.getPrefix(_planet); 60 | 61 | return ( // planet must not have an owner yet 62 | // 63 | azimuth.isOwner(_planet, 0x0) && 64 | // 65 | // this contract must be allowed to spawn for the prefix 66 | // 67 | azimuth.isSpawnProxy(prefix, this) && 68 | // 69 | // prefix must be linked 70 | // 71 | azimuth.hasBeenLinked(prefix) ); 72 | } 73 | 74 | // purchase(): pay the :price, acquire ownership of the _planet 75 | // 76 | // discovery of available planets can be done off-chain 77 | // 78 | function purchase(uint32 _planet) 79 | external 80 | payable 81 | { 82 | require( // caller must pay exactly the price of a planet 83 | // 84 | (msg.value == price) && 85 | // 86 | // the planet must be available for purchase 87 | // 88 | available(_planet) ); 89 | 90 | // spawn the planet to us, then immediately transfer to the caller 91 | // 92 | // spawning to the caller would give the point's prefix's owner 93 | // a window of opportunity to cancel the transfer 94 | // 95 | Ecliptic ecliptic = Ecliptic(azimuth.owner()); 96 | ecliptic.spawn(_planet, this); 97 | ecliptic.transferPoint(_planet, msg.sender, false); 98 | emit PlanetSold(azimuth.getPrefix(_planet), _planet); 99 | } 100 | 101 | // 102 | // Seller operations 103 | // 104 | 105 | // setPrice(): configure the price in wei per planet 106 | // 107 | function setPrice(uint256 _price) 108 | public 109 | onlyOwner 110 | { 111 | require(0 < _price); 112 | price = _price; 113 | } 114 | 115 | // withdraw(): withdraw ether funds held by this contract to _target 116 | // 117 | function withdraw(address _target) 118 | external 119 | onlyOwner 120 | { 121 | require(0x0 != _target); 122 | _target.transfer(address(this).balance); 123 | } 124 | 125 | // close(): end the sale by destroying this contract and transferring 126 | // remaining funds to _target 127 | // 128 | function close(address _target) 129 | external 130 | onlyOwner 131 | { 132 | require(0x0 != _target); 133 | selfdestruct(_target); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/TestPlanetSale.js: -------------------------------------------------------------------------------- 1 | const Azimuth = artifacts.require('Azimuth'); 2 | const Polls = artifacts.require('Polls'); 3 | const Claims = artifacts.require('Claims'); 4 | const Ecliptic = artifacts.require('Ecliptic'); 5 | const PlanetSale = artifacts.require('PlanetSale'); 6 | 7 | const assertRevert = require('./helpers/assertRevert'); 8 | 9 | contract('Planet Sale', function([owner, user]) { 10 | let azimuth, polls, claims, eclipt, sale, price; 11 | 12 | before('setting up for tests', async function() { 13 | price = 100000000; 14 | azimuth = await Azimuth.new(); 15 | polls = await Polls.new(432000, 432000); 16 | claims = await Claims.new(azimuth.address); 17 | eclipt = await Ecliptic.new('0x0000000000000000000000000000000000000000', 18 | azimuth.address, 19 | polls.address, 20 | claims.address); 21 | await azimuth.transferOwnership(eclipt.address); 22 | await polls.transferOwnership(eclipt.address); 23 | await eclipt.createGalaxy(0, owner); 24 | await eclipt.configureKeys(web3.utils.toHex(0), 25 | web3.utils.toHex(10), 26 | web3.utils.toHex(11), 27 | web3.utils.toHex(1), 28 | false); 29 | await eclipt.spawn(256, owner); 30 | await eclipt.configureKeys(web3.utils.toHex(256), 31 | web3.utils.toHex(12), 32 | web3.utils.toHex(13), 33 | web3.utils.toHex(1), 34 | false); 35 | sale = await PlanetSale.new(azimuth.address, price / 10); 36 | }); 37 | 38 | it('configuring price', async function() { 39 | assert.equal(await sale.price(), price / 10); 40 | // only owner can do this. 41 | await assertRevert(sale.setPrice(price, {from:user})); 42 | // must be more than zero 43 | await assertRevert(PlanetSale.new(azimuth.address, 0)); 44 | await assertRevert(sale.setPrice(0)); 45 | await sale.setPrice(price); 46 | assert.equal(await sale.price(), price); 47 | }); 48 | 49 | it('checking availability', async function() { 50 | assert.isFalse(await sale.available(65792)); 51 | await eclipt.setSpawnProxy(256, sale.address); 52 | assert.isTrue(await sale.available(65792)); 53 | assert.isFalse(await sale.available(65793)); 54 | }); 55 | 56 | it('purchasing', async function() { 57 | // can only purchase available planets. 58 | await assertRevert(sale.purchase(65793, {from:user,value:price})); 59 | // must pay the price 60 | await assertRevert(sale.purchase(65792, {from:user,value:price-1})); 61 | await sale.purchase(65792, {from:user,value:price}); 62 | assert.isTrue(await azimuth.isOwner(65792, user)); 63 | assert.isFalse(await sale.available(65792)); 64 | assert.equal(await web3.eth.getBalance(sale.address), price); 65 | // can only purchase available planets. 66 | await assertRevert(sale.purchase(65792, {from:user,value:price})); 67 | }); 68 | 69 | it('withdrawing', async function() { 70 | // only owner can do this. 71 | await assertRevert(sale.withdraw(user, {from:user})); 72 | // can't withdraw to zero address 73 | await assertRevert(sale.withdraw('0x0000000000000000000000000000000000000000')); 74 | let userBal = await web3.eth.getBalance(user); 75 | let saleBal = await web3.eth.getBalance(sale.address); 76 | let expected = web3.utils.toBN(userBal).add(web3.utils.toBN(saleBal)); 77 | await sale.withdraw(user, {gasPrice:0}); 78 | assert.isTrue(expected.eq(web3.utils.toBN(await web3.eth.getBalance(user)))); 79 | }); 80 | 81 | it('ending', async function() { 82 | // only owner can do this. 83 | await assertRevert(sale.close(user, {from:user})); 84 | // can't send remaining funds to zero address 85 | await assertRevert(sale.close('0x0000000000000000000000000000000000000000')); 86 | await sale.purchase(131328, {from:user,value:price}); 87 | let userBal = await web3.eth.getBalance(user); 88 | let saleBal = await web3.eth.getBalance(sale.address); 89 | let expected = web3.utils.toBN(userBal).add(web3.utils.toBN(saleBal)); 90 | await sale.close(user, {gasPrice:0}); 91 | assert.isTrue(expected.eq(web3.utils.toBN(await web3.eth.getBalance(user)))); 92 | // should no longer exist 93 | assert.equal(await web3.eth.getCode(sale.address), '0x'); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azimuth 2 | 3 | [![Build Status](https://secure.travis-ci.org/urbit/azimuth.png?branch=master)](http://travis-ci.org/urbit/azimuth) 4 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/urbit/azimuth/blob/master/LICENSE) 5 | [![npm](https://img.shields.io/npm/v/azimuth-solidity.svg)](https://www.npmjs.com/package/azimuth-solidity) 6 | 7 | A general-purpose PKI, live on the Ethereum blockchain. 8 | 9 | ## Overview 10 | 11 | This is just a quick summary of the different contracts and their purposes. For more detailed descriptions, see the inline documentation in the contracts themselves. 12 | 13 | * **Azimuth**: contains all on-chain state for azimuth. Most notably, ownership and public keys. Can't be modified directly, you must use the Ecliptic. 14 | * **Ecliptic**: is used as an interface for interacting with your points on-chain. Allows you to configure keys, transfer ownership, etc. 15 | * **Polls**: registers votes by the Galactic Senate on proposals. These can be either static documents or Ecliptic upgrades. 16 | * **Linear Star Release**: facilitates the release of blocks of stars to their owners over a period of time. 17 | * **Conditional Star Release**: facilitates the release of blocks of stars to their owners based on milestones. 18 | * **Claims**: allows point owners to make claims about (for example) their identity, and associate that with their point. 19 | * **Censures**: simple reputation management, allowing galaxies and stars to flag points for negative reputation. 20 | * **Delegated Sending**: enables network-effect like distributing of planets. 21 | * **Planet Sale**: gives an example of a way in which stars could sell planets on-chain. 22 | 23 | ## Live contracts 24 | 25 | The core Azimuth contracts can be found on the Ethereum blockchain. 26 | 27 | * **Azimuth**: [azimuth.eth / `0x223c067f8cf28ae173ee5cafea60ca44c335fecb`](https://etherscan.io/address/azimuth.eth) 28 | * **Ecliptic**: [ecliptic.eth / `0x33eecbf908478c10614626a9d304bfe18b78dd73`](https://etherscan.io/address/ecliptic.eth) 29 | * **Polls**: [`0x7fecab617c868bb5996d99d95200d2fa708218e4`](https://etherscan.io/address/0x7fecab617c868bb5996d99d95200d2fa708218e4) 30 | * **Linear Star Release**: [`0x86cd9cd0992f04231751e3761de45cecea5d1801`](https://etherscan.io/address/0x86cd9cd0992f04231751e3761de45cecea5d1801) 31 | * **Conditional Star Release**: [`0x8c241098c3d3498fe1261421633fd57986d74aea`](https://etherscan.io/address/0x8c241098c3d3498fe1261421633fd57986d74aea) 32 | * **Claims**: [`0xe7e7f69b34d7d9bd8d61fb22c33b22708947971a`](https://etherscan.io/address/0xe7e7f69b34d7d9bd8d61fb22c33b22708947971a) 33 | * **Censures**: [`0x325f68d32bdee6ed86e7235ff2480e2a433d6189`](https://etherscan.io/address/0x325f68d32bdee6ed86e7235ff2480e2a433d6189) 34 | * **Delegated Sending**: [`0xf6b461fe1ad4bd2ce25b23fe0aff2ac19b3dfa76`](https://etherscan.io/address/0xf6b461fe1ad4bd2ce25b23fe0aff2ac19b3dfa76) 35 | * **Planet Sale**: Deploy It Yourself! 36 | 37 | ## Galactic Senate 38 | 39 | A suggested process for publicizing the proposals voted on by the Galactic Senate is described in [`senate.md`](./senate.md). Following that process, proposals that have been voted on and achieved majority can be found in [`proposals/`](./proposals/). 40 | 41 | ## Running 42 | 43 | As a pre-requisite, `truffle` is required to be installed globally: 44 | 45 | ``` 46 | npm install -g truffle 47 | ``` 48 | 49 | Install dependencies. Most notable inclusion is [Zeppelin-Solidity](https://openzeppelin.org/). 50 | 51 | ``` 52 | npm install 53 | ``` 54 | 55 | Build, deploy and test via [Truffle](http://truffleframework.com/) using the following commands: 56 | 57 | ``` 58 | npx truffle compile 59 | npx truffle deploy 60 | npx truffle test 61 | ``` 62 | 63 | When verifying deployed contracts on services like Etherscan, be sure to use [truffle-flattener](https://github.com/alcuadrado/truffle-flattener) for flattening contracts into single files. 64 | 65 | ## Tests 66 | 67 | To run the contract test suite automatically, use a simple: 68 | 69 | ``` 70 | npm test 71 | ``` 72 | 73 | This will spin up a local [Ganache](https://github.com/trufflesuite/ganache-cli) node in the background. If you'd like to use a persistent node, you can run 74 | 75 | ``` 76 | npx ganache-cli --gasLimit 6000000 77 | ``` 78 | 79 | and then test via `npx truffle test`. 80 | 81 | For testing Ecliptic upgrades against whatever version of the contract is on mainnet, first run: 82 | 83 | ``` 84 | npm run fork-mainnet 85 | ``` 86 | 87 | This will start a local fork of mainnet, with the ownership addresses of the first 128 galaxies unlocked. Once that's ready, you can run the following in a separate terminal: 88 | 89 | ``` 90 | npm run test-upgrade 91 | // or, to upgrade to a pre-existing contract, specify its address: 92 | npm run test-upgrade -- --target='0xabcd...' 93 | ``` 94 | 95 | This will deploy the Ecliptic contract currently in the repository to the local fork (or refer to the specified upgrade target), and test if it can be upgraded to cleanly. Because this involves many transactions (for voting), this may take a couple minutes. 96 | 97 | There are also tests located in `test-extras` that are not meant to be run via 98 | a basic `npx truffle test` as they can fail nondeterministically. You can run 99 | these via: 100 | 101 | ``` 102 | npm run test-extras 103 | ``` 104 | 105 | -------------------------------------------------------------------------------- /contracts/Censures.sol: -------------------------------------------------------------------------------- 1 | // simple reputations store 2 | // https://azimuth.network 3 | 4 | pragma solidity 0.4.24; 5 | 6 | import './ReadsAzimuth.sol'; 7 | 8 | // Censures: simple reputation management 9 | // 10 | // This contract allows stars and galaxies to assign a negative 11 | // reputation (censure) to other points of the same or lower rank. 12 | // These censures are not permanent, they can be forgiven. 13 | // 14 | // Since Azimuth-based networks provide incentives for good behavior, 15 | // making bad behavior the exception rather than the rule, this 16 | // contract only provides registration of negative reputation. 17 | // 18 | contract Censures is ReadsAzimuth 19 | { 20 | // Censured: :who got censured by :by 21 | // 22 | event Censured(uint16 indexed by, uint32 indexed who); 23 | 24 | // Forgiven: :who is no longer censured by :by 25 | // 26 | event Forgiven(uint16 indexed by, uint32 indexed who); 27 | 28 | // censuring: per point, the points they're censuring 29 | // 30 | mapping(uint16 => uint32[]) public censuring; 31 | 32 | // censuredBy: per point, those who have censured them 33 | // 34 | mapping(uint32 => uint16[]) public censuredBy; 35 | 36 | // censuringIndexes: per point per censure, (index + 1) in censures array 37 | // 38 | // We delete censures by moving the last entry in the array to the 39 | // newly emptied slot, which is (n - 1) where n is the value of 40 | // indexes[point][censure]. 41 | // 42 | mapping(uint16 => mapping(uint32 => uint256)) public censuringIndexes; 43 | 44 | // censuredByIndexes: per censure per point, (index + 1) in censured array 45 | // 46 | // see also explanation for indexes_censures above 47 | // 48 | mapping(uint32 => mapping(uint16 => uint256)) public censuredByIndexes; 49 | 50 | // constructor(): register the azimuth contract 51 | // 52 | constructor(Azimuth _azimuth) 53 | ReadsAzimuth(_azimuth) 54 | public 55 | { 56 | // 57 | } 58 | 59 | // getCensuringCount(): return length of array of censures made by _whose 60 | // 61 | function getCensuringCount(uint16 _whose) 62 | view 63 | public 64 | returns (uint256 count) 65 | { 66 | return censuring[_whose].length; 67 | } 68 | 69 | // getCensuring(): return array of censures made by _whose 70 | // 71 | // Note: only useful for clients, as Solidity does not currently 72 | // support returning dynamic arrays. 73 | // 74 | function getCensuring(uint16 _whose) 75 | view 76 | public 77 | returns (uint32[] cens) 78 | { 79 | return censuring[_whose]; 80 | } 81 | 82 | // getCensuredByCount(): return length of array of censures made against _who 83 | // 84 | function getCensuredByCount(uint16 _who) 85 | view 86 | public 87 | returns (uint256 count) 88 | { 89 | return censuredBy[_who].length; 90 | } 91 | 92 | // getCensuredBy(): return array of censures made against _who 93 | // 94 | // Note: only useful for clients, as Solidity does not currently 95 | // support returning dynamic arrays. 96 | // 97 | function getCensuredBy(uint16 _who) 98 | view 99 | public 100 | returns (uint16[] cens) 101 | { 102 | return censuredBy[_who]; 103 | } 104 | 105 | // censure(): register a censure of _who as _as 106 | // 107 | function censure(uint16 _as, uint32 _who) 108 | external 109 | activePointManager(_as) 110 | { 111 | require( // can't censure self 112 | // 113 | (_as != _who) && 114 | // 115 | // must not haven censured _who already 116 | // 117 | (censuringIndexes[_as][_who] == 0) ); 118 | 119 | // only stars and galaxies may censure, and only galaxies may censure 120 | // other galaxies. (enum gets smaller for higher point sizes) 121 | // this function's signature makes sure planets cannot censure. 122 | // 123 | Azimuth.Size asSize = azimuth.getPointSize(_as); 124 | Azimuth.Size whoSize = azimuth.getPointSize(_who); 125 | require( whoSize >= asSize ); 126 | 127 | // update contract state with the new censure 128 | // 129 | censuring[_as].push(_who); 130 | censuringIndexes[_as][_who] = censuring[_as].length; 131 | 132 | // and update the reverse lookup 133 | // 134 | censuredBy[_who].push(_as); 135 | censuredByIndexes[_who][_as] = censuredBy[_who].length; 136 | 137 | emit Censured(_as, _who); 138 | } 139 | 140 | // forgive(): unregister a censure of _who as _as 141 | // 142 | function forgive(uint16 _as, uint32 _who) 143 | external 144 | activePointManager(_as) 145 | { 146 | // below, we perform the same logic twice: once on the canonical data, 147 | // and once on the reverse lookup 148 | // 149 | // i: current index in _as's list of censures 150 | // j: current index in _who's list of points that have censured it 151 | // 152 | uint256 i = censuringIndexes[_as][_who]; 153 | uint256 j = censuredByIndexes[_who][_as]; 154 | 155 | // we store index + 1, because 0 is the eth default value 156 | // can only delete an existing censure 157 | // 158 | require( (i > 0) && (j > 0) ); 159 | i--; 160 | j--; 161 | 162 | // copy last item in the list into the now-unused slot, 163 | // making sure to update the :indexes_ references 164 | // 165 | uint32[] storage cens = censuring[_as]; 166 | uint16[] storage cend = censuredBy[_who]; 167 | uint256 lastCens = cens.length - 1; 168 | uint256 lastCend = cend.length - 1; 169 | uint32 movedCens = cens[lastCens]; 170 | uint16 movedCend = cend[lastCend]; 171 | cens[i] = movedCens; 172 | cend[j] = movedCend; 173 | censuringIndexes[_as][movedCens] = i + 1; 174 | censuredByIndexes[_who][movedCend] = j + 1; 175 | 176 | // delete the last item 177 | // 178 | cens.length = lastCens; 179 | cend.length = lastCend; 180 | censuringIndexes[_as][_who] = 0; 181 | censuredByIndexes[_who][_as] = 0; 182 | 183 | emit Forgiven(_as, _who); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /test/TestDelegatedSending.js: -------------------------------------------------------------------------------- 1 | const Azimuth = artifacts.require('Azimuth'); 2 | const Polls = artifacts.require('Polls'); 3 | const Claims = artifacts.require('Claims'); 4 | const Ecliptic = artifacts.require('Ecliptic'); 5 | const DelegatedSending = artifacts.require('DelegatedSending'); 6 | 7 | const assertRevert = require('./helpers/assertRevert'); 8 | const seeEvents = require('./helpers/seeEvents'); 9 | 10 | contract('Delegated Sending', function([owner, user1, user2, user3, user4, user5]) { 11 | let azimuth, eclipt, dese, 12 | p1, p2, p3, p4, p5, p6, 13 | s1, s2; 14 | 15 | before('setting up for tests', async function() { 16 | s1 = 256; 17 | s2 = 512; 18 | p1 = 65792; 19 | p2 = 131328; 20 | p3 = 196864; 21 | p4 = 262400; 22 | p5 = 327936; 23 | p6 = 66048; 24 | // 25 | azimuth = await Azimuth.new(); 26 | polls = await Polls.new(432000, 432000); 27 | claims = await Claims.new(azimuth.address); 28 | eclipt = await Ecliptic.new('0x0000000000000000000000000000000000000000', 29 | azimuth.address, 30 | polls.address, 31 | claims.address); 32 | await azimuth.transferOwnership(eclipt.address); 33 | await polls.transferOwnership(eclipt.address); 34 | dese = await DelegatedSending.new(azimuth.address); 35 | // 36 | await eclipt.createGalaxy(0, owner); 37 | await eclipt.configureKeys(web3.utils.toHex(0), 38 | web3.utils.toHex(1), 39 | web3.utils.toHex(1), 40 | web3.utils.toHex(1), 41 | false); 42 | await eclipt.spawn(s1, owner); 43 | await eclipt.configureKeys(s1, 44 | web3.utils.toHex(1), 45 | web3.utils.toHex(1), 46 | web3.utils.toHex(1), 47 | false); 48 | await eclipt.setSpawnProxy(s1, dese.address); 49 | await eclipt.spawn(s2, owner); 50 | await eclipt.configureKeys(s2, 51 | web3.utils.toHex(1), 52 | web3.utils.toHex(1), 53 | web3.utils.toHex(1), 54 | false); 55 | await eclipt.setSpawnProxy(s2, dese.address); 56 | await eclipt.spawn(p1, owner); 57 | await eclipt.transferPoint(p1, owner, false); 58 | }); 59 | 60 | it('configuring', async function() { 61 | assert.equal(await dese.pools(p1, s1), 0); 62 | assert.isFalse(await dese.canSend(p1, p2)); 63 | // can only be done by owner of any star 64 | await assertRevert(dese.setPoolSize(s1, p1, 3, {from:user1})); 65 | await dese.setPoolSize(s1, p1, 3); 66 | await dese.setPoolSize(s2, p1, 9); 67 | let poolStars = await dese.getPoolStars(p1); 68 | assert.equal(poolStars.length, 2); 69 | assert.equal(poolStars[0], s1); 70 | assert.equal(poolStars[1], s2); 71 | assert.equal(await dese.pools(p1, s1), 3); 72 | assert.equal(await dese.pools(p1, s2), 9); 73 | let inviters = await dese.getInviters(); 74 | assert.equal(inviters.length, 1); 75 | assert.equal(inviters[0], p1); 76 | assert.isTrue(await dese.canSend(p1, p2)); 77 | }); 78 | 79 | it('sending', async function() { 80 | // can only be done by point owner 81 | await assertRevert(dese.sendPoint(p1, p2, user1, {from:user1})); 82 | // can't send to self 83 | await assertRevert(dese.sendPoint(p1, p2, owner)); 84 | // send as regular planet 85 | assert.isTrue(await dese.canReceive(user1)); 86 | await seeEvents(dese.sendPoint(p1, p2, user1), ['Sent']); 87 | assert.isTrue(await azimuth.isTransferProxy(p2, user1)); 88 | assert.equal(await dese.pools(p1, s1), 2); 89 | assert.equal(await dese.fromPool(p2), p1); 90 | let invited = await dese.getInvited(p1); 91 | assert.equal(invited.length, 1); 92 | assert.equal(invited[0], p2); 93 | assert.equal(await dese.invitedBy(p2), p1); 94 | assert.isFalse(await dese.canSend(p1, p2)); 95 | await assertRevert(dese.sendPoint(p1, p2, user1)); 96 | // can't send to users with pending transfers 97 | assert.isFalse(await dese.canReceive(user1)); 98 | await assertRevert(dese.sendPoint(p2, p3, user1)); 99 | await eclipt.transferPoint(p2, user1, true); 100 | assert.isFalse(await dese.canSend(p1, p2)); 101 | // can't send to users who own points 102 | assert.isFalse(await dese.canReceive(user1)); 103 | await assertRevert(dese.sendPoint(p1, p3, user1)); 104 | // send as invited planet 105 | assert.equal(await dese.getPool(p3), p3); 106 | await dese.sendPoint(p2, p3, user2, {from:user1}); 107 | assert.equal(await dese.pools(p1, s1), 1); 108 | assert.equal(await dese.fromPool(p3), p1); 109 | assert.equal(await dese.getPool(p3), p1); 110 | invited = await dese.getInvited(p2); 111 | assert.equal(invited.length, 1); 112 | assert.equal(invited[0], p3); 113 | assert.equal(await dese.invitedBy(p3), p2); 114 | await eclipt.transferPoint(p3, user2, true); 115 | await dese.sendPoint(p3, p4, user3, {from:user2}); 116 | assert.equal(await dese.pools(p1, s1), 0); 117 | await eclipt.transferPoint(p4, user3, true); 118 | // can't send once pool depleted 119 | assert.isFalse(await dese.canSend(p1, p5)); 120 | assert.isFalse(await dese.canSend(p3, p5)); 121 | await assertRevert(dese.sendPoint(p3, p5, user4)); 122 | // but can still send from other pool, even as invitee 123 | assert.isTrue(await dese.canSend(p1, p6)); 124 | assert.isTrue(await dese.canSend(p3, p6)); 125 | await dese.sendPoint(p3, p6, user4, {from:user2}); 126 | assert.equal(await dese.pools(p1, s2), 8); 127 | }); 128 | 129 | it('resetting an invitee\'s pool', async function() { 130 | await dese.setPoolSize(s1, p3, 3); 131 | assert.isTrue(await dese.canSend(p3, p5)); 132 | // shouldn't affect the pool it came from 133 | assert.isFalse(await dese.canSend(p1, p5)); 134 | await dese.sendPoint(p3, p5, user5, {from:user2}); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /contracts/Claims.sol: -------------------------------------------------------------------------------- 1 | // simple claims store 2 | // https://azimuth.network 3 | 4 | pragma solidity 0.4.24; 5 | 6 | import './ReadsAzimuth.sol'; 7 | 8 | // Claims: simple identity management 9 | // 10 | // This contract allows points to document claims about their owner. 11 | // Most commonly, these are about identity, with a claim's protocol 12 | // defining the context or platform of the claim, and its dossier 13 | // containing proof of its validity. 14 | // Points are limited to a maximum of 16 claims. 15 | // 16 | // For existing claims, the dossier can be updated, or the claim can 17 | // be removed entirely. It is recommended to remove any claims associated 18 | // with a point when it is about to be transferred to a new owner. 19 | // For convenience, the owner of the Azimuth contract (the Ecliptic) 20 | // is allowed to clear claims for any point, allowing it to do this for 21 | // you on-transfer. 22 | // 23 | contract Claims is ReadsAzimuth 24 | { 25 | // ClaimAdded: a claim was added by :by 26 | // 27 | event ClaimAdded( uint32 indexed by, 28 | string _protocol, 29 | string _claim, 30 | bytes _dossier ); 31 | 32 | // ClaimRemoved: a claim was removed by :by 33 | // 34 | event ClaimRemoved(uint32 indexed by, string _protocol, string _claim); 35 | 36 | // maxClaims: the amount of claims that can be registered per point 37 | // 38 | uint8 constant maxClaims = 16; 39 | 40 | // Claim: claim details 41 | // 42 | struct Claim 43 | { 44 | // protocol: context of the claim 45 | // 46 | string protocol; 47 | 48 | // claim: the claim itself 49 | // 50 | string claim; 51 | 52 | // dossier: data relating to the claim, as proof 53 | // 54 | bytes dossier; 55 | } 56 | 57 | // per point, list of claims 58 | // 59 | mapping(uint32 => Claim[maxClaims]) public claims; 60 | 61 | // constructor(): register the azimuth contract. 62 | // 63 | constructor(Azimuth _azimuth) 64 | ReadsAzimuth(_azimuth) 65 | public 66 | { 67 | // 68 | } 69 | 70 | // addClaim(): register a claim as _point 71 | // 72 | function addClaim(uint32 _point, 73 | string _protocol, 74 | string _claim, 75 | bytes _dossier) 76 | external 77 | activePointManager(_point) 78 | { 79 | // require non-empty protocol and claim fields 80 | // 81 | require( ( 0 < bytes(_protocol).length ) && 82 | ( 0 < bytes(_claim).length ) ); 83 | 84 | // cur: index + 1 of the claim if it already exists, 0 otherwise 85 | // 86 | uint8 cur = findClaim(_point, _protocol, _claim); 87 | 88 | // if the claim doesn't yet exist, store it in state 89 | // 90 | if (cur == 0) 91 | { 92 | // if there are no empty slots left, this throws 93 | // 94 | uint8 empty = findEmptySlot(_point); 95 | claims[_point][empty] = Claim(_protocol, _claim, _dossier); 96 | } 97 | // 98 | // if the claim has been made before, update the version in state 99 | // 100 | else 101 | { 102 | claims[_point][cur-1] = Claim(_protocol, _claim, _dossier); 103 | } 104 | emit ClaimAdded(_point, _protocol, _claim, _dossier); 105 | } 106 | 107 | // removeClaim(): unregister a claim as _point 108 | // 109 | function removeClaim(uint32 _point, string _protocol, string _claim) 110 | external 111 | activePointManager(_point) 112 | { 113 | // i: current index + 1 in _point's list of claims 114 | // 115 | uint256 i = findClaim(_point, _protocol, _claim); 116 | 117 | // we store index + 1, because 0 is the eth default value 118 | // can only delete an existing claim 119 | // 120 | require(i > 0); 121 | i--; 122 | 123 | // clear out the claim 124 | // 125 | delete claims[_point][i]; 126 | 127 | emit ClaimRemoved(_point, _protocol, _claim); 128 | } 129 | 130 | // clearClaims(): unregister all of _point's claims 131 | // 132 | // can also be called by the ecliptic during point transfer 133 | // 134 | function clearClaims(uint32 _point) 135 | external 136 | { 137 | // both point owner and ecliptic may do this 138 | // 139 | // We do not necessarily need to check for _point's active flag here, 140 | // since inactive points cannot have claims set. Doing the check 141 | // anyway would make this function slightly harder to think about due 142 | // to its relation to Ecliptic's transferPoint(). 143 | // 144 | require( azimuth.canManage(_point, msg.sender) || 145 | ( msg.sender == azimuth.owner() ) ); 146 | 147 | Claim[maxClaims] storage currClaims = claims[_point]; 148 | 149 | // clear out all claims 150 | // 151 | for (uint8 i = 0; i < maxClaims; i++) 152 | { 153 | // only emit the removed event if there was a claim here 154 | // 155 | if ( 0 < bytes(currClaims[i].claim).length ) 156 | { 157 | emit ClaimRemoved(_point, currClaims[i].protocol, currClaims[i].claim); 158 | } 159 | 160 | delete currClaims[i]; 161 | } 162 | } 163 | 164 | // findClaim(): find the index of the specified claim 165 | // 166 | // returns 0 if not found, index + 1 otherwise 167 | // 168 | function findClaim(uint32 _whose, string _protocol, string _claim) 169 | public 170 | view 171 | returns (uint8 index) 172 | { 173 | // we use hashes of the string because solidity can't do string 174 | // comparison yet 175 | // 176 | bytes32 protocolHash = keccak256(bytes(_protocol)); 177 | bytes32 claimHash = keccak256(bytes(_claim)); 178 | Claim[maxClaims] storage theirClaims = claims[_whose]; 179 | for (uint8 i = 0; i < maxClaims; i++) 180 | { 181 | Claim storage thisClaim = theirClaims[i]; 182 | if ( ( protocolHash == keccak256(bytes(thisClaim.protocol)) ) && 183 | ( claimHash == keccak256(bytes(thisClaim.claim)) ) ) 184 | { 185 | return i+1; 186 | } 187 | } 188 | return 0; 189 | } 190 | 191 | // findEmptySlot(): find the index of the first empty claim slot 192 | // 193 | // returns the index of the slot, throws if there are no empty slots 194 | // 195 | function findEmptySlot(uint32 _whose) 196 | internal 197 | view 198 | returns (uint8 index) 199 | { 200 | Claim[maxClaims] storage theirClaims = claims[_whose]; 201 | for (uint8 i = 0; i < maxClaims; i++) 202 | { 203 | Claim storage thisClaim = theirClaims[i]; 204 | if ( (0 == bytes(thisClaim.claim).length) ) 205 | { 206 | return i; 207 | } 208 | } 209 | revert(); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /test/TestLinearStarRelease.js: -------------------------------------------------------------------------------- 1 | const Azimuth = artifacts.require('Azimuth'); 2 | const Polls = artifacts.require('Polls'); 3 | const Claims = artifacts.require('Claims'); 4 | const Ecliptic = artifacts.require('Ecliptic'); 5 | const LSR = artifacts.require('LinearStarRelease'); 6 | 7 | const assertRevert = require('./helpers/assertRevert'); 8 | const increaseTime = require('./helpers/increaseTime'); 9 | 10 | const deposit = '0x1111111111111111111111111111111111111111'; 11 | 12 | contract('Linear Star Release', function([owner, user1, user2, user3]) { 13 | let azimuth, polls, claims, eclipt, lsr, windup, rateUnit; 14 | 15 | before('setting up for tests', async function() { 16 | windup = 20; 17 | rateUnit = 50; 18 | azimuth = await Azimuth.new(); 19 | polls = await Polls.new(432000, 432000); 20 | claims = await Claims.new(azimuth.address); 21 | eclipt = await Ecliptic.new('0x0000000000000000000000000000000000000000', 22 | azimuth.address, 23 | polls.address, 24 | claims.address); 25 | await azimuth.transferOwnership(eclipt.address); 26 | await polls.transferOwnership(eclipt.address); 27 | await eclipt.createGalaxy(0, owner); 28 | await eclipt.configureKeys(web3.utils.toHex(0), 29 | web3.utils.toHex(1), 30 | web3.utils.toHex(2), 31 | web3.utils.toHex(1), 32 | false); 33 | await eclipt.spawn(256, owner); 34 | await eclipt.spawn(2560, owner); 35 | await eclipt.configureKeys(web3.utils.toHex(2560), 36 | web3.utils.toHex(1), 37 | web3.utils.toHex(2), 38 | web3.utils.toHex(1), 39 | false); 40 | lsr = await LSR.new(azimuth.address); 41 | lsr.startReleasing(); 42 | await eclipt.setSpawnProxy(0, lsr.address); 43 | await eclipt.setTransferProxy(256, lsr.address); 44 | }); 45 | 46 | it('registering batches', async function() { 47 | // only owner can do this 48 | await assertRevert(lsr.register(user1, windup, 5, 2, rateUnit, {from:user1})); 49 | // need a sane rate 50 | await assertRevert(lsr.register(user1, windup, 8, 0, rateUnit)); 51 | assert.isTrue(await lsr.verifyBalance(user1)); 52 | await lsr.register(user1, windup, 8, 2, rateUnit); 53 | await lsr.register(user3, windup, 8, 2, rateUnit); 54 | let bat = await lsr.batches(user1); 55 | assert.equal(bat[0], windup); 56 | assert.equal(bat[1], rateUnit); 57 | assert.equal(bat[2], 0); 58 | assert.equal(bat[3], 2); 59 | assert.equal(bat[4], 8); 60 | assert.isFalse(await lsr.verifyBalance(user1)); 61 | // can always withdraw at least one star 62 | assert.equal(await lsr.withdrawLimit(user1), 1); 63 | }); 64 | 65 | it('withdraw limit', async function() { 66 | // pass windup, still need to wait a rateUnit 67 | await increaseTime(windup); 68 | assert.equal(await lsr.withdrawLimit(user1), 1); 69 | // pass a rateUnit 70 | await increaseTime(rateUnit); 71 | assert.equal(await lsr.withdrawLimit(user1), 2); 72 | // pass two rateUnits 73 | await increaseTime(rateUnit); 74 | assert.equal(await lsr.withdrawLimit(user1), 4); 75 | // unregistered address should not yet have a withdraw limit 76 | try { 77 | await lsr.withdrawLimit(user2); 78 | assert.fail('should have thrown before'); 79 | } catch(err) { 80 | assert.isAbove(err.message.search('invalid opcode'), -1, 'Invalid opcode must be returned, but got ' + err); 81 | } 82 | }); 83 | 84 | it('depositing stars', async function() { 85 | // only owner can do this 86 | await assertRevert(lsr.deposit(user1, 256, {from:user1})); 87 | // can't deposit a live star 88 | await assertRevert(lsr.deposit(user1, 2560)); 89 | // deposit spawned star, as star owner 90 | await lsr.deposit(user1, 256); 91 | // deposit unspawned stars, as galaxy owner 92 | for (var s = 2; s < 9; s++) { 93 | await lsr.deposit(user1, s*256); 94 | } 95 | assert.equal((await lsr.getRemainingStars(user1)).length, 8); 96 | assert.equal((await lsr.getRemainingStars(user1))[7], 2048); 97 | assert.isTrue(await azimuth.isOwner(256, lsr.address)); 98 | assert.isTrue(await lsr.verifyBalance(user1)); 99 | // can't deposit too many 100 | await assertRevert(lsr.deposit(user1, 2304)); 101 | }); 102 | 103 | it('transferring batch', async function() { 104 | assert.equal((await lsr.batches(user1))[5], 0); 105 | // can't transfer to other participant 106 | await assertRevert(lsr.approveBatchTransfer(user3, {from:user1})); 107 | // can't transfer without permission 108 | await assertRevert(lsr.transferBatch(user1, {from:user2})); 109 | await lsr.approveBatchTransfer(user2, {from:user1}); 110 | await lsr.approveBatchTransfer(user2, {from:user3}); 111 | assert.equal((await lsr.batches(user1))[5], user2); 112 | await lsr.transferBatch(user1, {from:user2}); 113 | // can't if we became a participant in the mean time 114 | await assertRevert(lsr.transferBatch(user3, {from:user2})); 115 | await lsr.withdrawLimit(user2); 116 | // unregistered address should no longer have stars, etc 117 | assert.equal((await lsr.getRemainingStars(user1)).length, 0); 118 | }); 119 | 120 | it('withdrawing', async function() { 121 | assert.equal(await lsr.withdrawLimit(user2), 4); 122 | // only commitment participant can do this 123 | await assertRevert(lsr.withdraw({from:owner})); 124 | await lsr.withdraw({from:user2}); 125 | assert.isTrue(await azimuth.isOwner(2048, user2)); 126 | assert.equal((await lsr.batches(user2))[2], 1); 127 | await lsr.withdraw({from:user2}); 128 | await lsr.withdraw({from:user2}); 129 | await lsr.withdraw({from:user2}); 130 | assert.equal((await lsr.batches(user2))[2], 4); 131 | assert.equal(await lsr.withdrawLimit(user2), 4); 132 | // can't withdraw over limit 133 | await assertRevert(lsr.withdraw({from:user2})); 134 | }); 135 | 136 | it('withdraw limit maximum', async function() { 137 | // pass all rateUnits, and then some 138 | await increaseTime(rateUnit * 100); 139 | assert.equal(await lsr.withdrawLimit(user2), 8); 140 | }); 141 | 142 | it('escape hatch', async function() { 143 | // doesn't work too early 144 | await assertRevert(lsr.withdrawOverdue(user2, owner)); 145 | await increaseTime(10*365*24*60*60); 146 | 147 | // test that we can't withdraw to the deposit address. This is a 148 | // convenient way to verify the isContract check works correct. 149 | await assertRevert(lsr.withdrawOverdue(user2, deposit)); 150 | 151 | // works afterward 152 | await lsr.withdrawOverdue(user2, owner); 153 | assert.isTrue(await azimuth.isOwner(1024, owner)); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /test/TestPolls.js: -------------------------------------------------------------------------------- 1 | const Polls = artifacts.require('Polls'); 2 | 3 | const assertRevert = require('./helpers/assertRevert'); 4 | const increaseTime = require('./helpers/increaseTime'); 5 | 6 | const web3 = Polls.web3; 7 | 8 | contract('Polls', function([owner, user]) { 9 | let polls, duration, cooldown; 10 | const concrProp = '0x11cE09F4EbE9d12f6e3864D21A1E7Dde126F34eb'; 11 | const concrProp2 = '0x22ce09f4EbE9d12F6e3864d21a1e7dDE126F34eb'; 12 | const abstrProp = 13 | '0xabcde00000000000000000000000000000000000000000000000000000000000'; 14 | const abstrProp2 = 15 | '0xcdef100000000000000000000000000000000000000000000000000000000000'; 16 | 17 | before('setting up for tests', async function() { 18 | polls = await Polls.new(432111, 432222); 19 | duration = 432000; 20 | cooldown = 7776000; 21 | }); 22 | 23 | it('configuring polls', async function() { 24 | assert.equal(await polls.pollDuration(), 432111); 25 | assert.equal(await polls.pollCooldown(), 432222); 26 | assert.equal(await polls.totalVoters(), 0); 27 | await polls.reconfigure(duration, cooldown); 28 | for (var i = 0; i < 3; i++) { 29 | await polls.incrementTotalVoters(); 30 | } 31 | assert.equal(await polls.pollDuration(), duration); 32 | assert.equal(await polls.pollCooldown(), cooldown); 33 | assert.equal(await polls.totalVoters(), 3); 34 | // can't set too high or too low 35 | await assertRevert(polls.reconfigure(431999, cooldown)); 36 | await assertRevert(polls.reconfigure(7776001, cooldown)); 37 | await assertRevert(polls.reconfigure(duration, 431999)); 38 | await assertRevert(polls.reconfigure(duration, 7776001)); 39 | }); 40 | 41 | it('upgrade poll start & majority', async function() { 42 | assert.isFalse(await polls.upgradeHasAchievedMajority(concrProp)); 43 | // non-owner can't do this. 44 | await assertRevert(polls.startUpgradePoll(concrProp, {from:user})); 45 | await polls.startUpgradePoll(concrProp); 46 | let cPoll = await polls.upgradePolls(concrProp); 47 | assert.notEqual(cPoll[0], 0); 48 | assert.equal(await polls.getUpgradeProposalCount(), 1); 49 | assert.equal((await polls.getUpgradeProposals())[0], concrProp); 50 | // non-owner can't do this. 51 | await assertRevert(polls.castUpgradeVote(0, concrProp, true, {from:user})); 52 | // cast votes. 53 | // we use .call to check the result first, then actually transact. 54 | assert.isFalse(await polls.castUpgradeVote.call(0, concrProp, true)); 55 | await polls.castUpgradeVote(0, concrProp, true); 56 | assert.isTrue(await polls.hasVotedOnUpgradePoll(0, concrProp)); 57 | // can't vote twice. 58 | await assertRevert(polls.castUpgradeVote(0, concrProp, true)); 59 | assert.isFalse(await polls.castUpgradeVote.call(1, concrProp, false)); 60 | await polls.castUpgradeVote(1, concrProp, false); 61 | assert.isTrue(await polls.castUpgradeVote.call(2, concrProp, true)); 62 | await polls.castUpgradeVote(2, concrProp, true); 63 | assert.isTrue(await polls.upgradeHasAchievedMajority(concrProp)); 64 | cPoll = await polls.upgradePolls(concrProp); 65 | assert.equal(cPoll[1], 2); 66 | assert.equal(cPoll[2], 1); 67 | // can't vote on finished poll 68 | await assertRevert(polls.castUpgradeVote(3, concrProp, true)); 69 | }); 70 | 71 | it('upgrade poll minority & restart', async function() { 72 | // start poll and wait for it to time out 73 | await polls.startUpgradePoll(concrProp2); 74 | await polls.castUpgradeVote(0, concrProp2, false); 75 | await increaseTime(duration); 76 | // can't vote on finished poll 77 | await assertRevert(polls.castUpgradeVote(1, concrProp2, true)); 78 | // can't recreate right away. 79 | await assertRevert(polls.startUpgradePoll(concrProp2)); 80 | await increaseTime(cooldown + 5); 81 | // recreate poll. 82 | await polls.startUpgradePoll(concrProp2); 83 | let cPoll = await polls.upgradePolls(concrProp2); 84 | assert.equal(cPoll[1], 0); 85 | assert.equal(cPoll[2], 0); 86 | assert.isFalse(await polls.hasVotedOnUpgradePoll(0, concrProp2)); 87 | assert.equal(await polls.getUpgradeProposalCount(), 2); 88 | let props = await polls.getUpgradeProposals(); 89 | assert.equal(props[0], concrProp); 90 | assert.equal(props[1], concrProp2); 91 | // test timeout majority 92 | await polls.castUpgradeVote(0, concrProp2, true); 93 | assert.isTrue(await polls.hasVotedOnUpgradePoll(0, concrProp2)); 94 | await increaseTime(duration + 5); 95 | assert.isTrue(await polls.updateUpgradePoll.call(concrProp2)); 96 | await polls.updateUpgradePoll(concrProp2); 97 | assert.isTrue(await polls.upgradeHasAchievedMajority(concrProp2)); 98 | // can't recreate once majority happened 99 | await assertRevert(polls.startUpgradePoll(concrProp2)); 100 | }); 101 | 102 | it('document poll start & majority', async function() { 103 | assert.isFalse(await polls.documentHasAchievedMajority(abstrProp)); 104 | let mas = await polls.getDocumentMajorities(); 105 | assert.equal(mas.length, 0); 106 | // non-owner can't do this. 107 | await assertRevert(polls.startDocumentPoll(abstrProp, {from:user})); 108 | await polls.startDocumentPoll(abstrProp); 109 | let aPoll = await polls.documentPolls(abstrProp); 110 | assert.notEqual(aPoll[0], 0); 111 | assert.equal(await polls.getDocumentProposalCount(), 1); 112 | assert.equal((await polls.getDocumentProposals())[0], abstrProp); 113 | // non-owner can't do this. 114 | await assertRevert(polls.castDocumentVote(0, abstrProp, true, {from:user})); 115 | // cast votes. 116 | await polls.castDocumentVote(0, abstrProp, true); 117 | assert.isTrue(await polls.hasVotedOnDocumentPoll(0, abstrProp)); 118 | // can't vote twice. 119 | await assertRevert(polls.castDocumentVote(0, abstrProp, true)); 120 | await polls.castDocumentVote(1, abstrProp, false); 121 | await polls.castDocumentVote(2, abstrProp, true); 122 | assert.isTrue(await polls.documentHasAchievedMajority(abstrProp)); 123 | mas = await polls.getDocumentMajorities(); 124 | assert.equal(mas.length, 1); 125 | assert.equal(mas[0], abstrProp); 126 | aPoll = await polls.documentPolls(abstrProp); 127 | assert.equal(aPoll[1], 2); 128 | assert.equal(aPoll[2], 1); 129 | // can't vote on finished poll 130 | await assertRevert(polls.castDocumentVote(3, abstrProp, true)); 131 | // can't recreate once majority happened. 132 | await assertRevert(polls.startDocumentPoll(abstrProp)); 133 | }); 134 | 135 | it('document poll minority & restart', async function() { 136 | // start poll and wait for it to time out 137 | await polls.startDocumentPoll(abstrProp2); 138 | await polls.castDocumentVote(0, abstrProp2, false); 139 | await increaseTime(duration); 140 | // can't vote on finished poll 141 | await assertRevert(polls.castDocumentVote(1, abstrProp2, true)); 142 | // can't recreate right away. 143 | await assertRevert(polls.startDocumentPoll(abstrProp2)); 144 | await increaseTime(cooldown + 5); 145 | // recreate poll. 146 | await polls.startDocumentPoll(abstrProp2); 147 | let aPoll = await polls.documentPolls(abstrProp2); 148 | assert.equal(aPoll[1], 0); 149 | assert.equal(aPoll[2], 0); 150 | assert.isFalse(await polls.hasVotedOnDocumentPoll(0, abstrProp2)); 151 | assert.equal(await polls.getDocumentProposalCount(), 2); 152 | let props = await polls.getDocumentProposals(); 153 | assert.equal(props[0], abstrProp); 154 | assert.equal(props[1], abstrProp2); 155 | // test timeout majority 156 | await polls.castDocumentVote(0, abstrProp2, true); 157 | assert.isTrue(await polls.hasVotedOnDocumentPoll(0, abstrProp2)); 158 | await increaseTime(duration + 5); 159 | await polls.updateDocumentPoll(abstrProp2); 160 | assert.isTrue(await polls.documentHasAchievedMajority(abstrProp2)); 161 | }); 162 | 163 | it('strong minority case', async function() { 164 | // this test stretches the calculation in checkPollMajority to their limits 165 | let pox = await Polls.new(432111, 432222); 166 | for (let i = 0; i <= 255; i++) { 167 | await pox.incrementTotalVoters(); 168 | } 169 | await pox.startDocumentPoll(abstrProp2); 170 | for (let i = 0; i <= 255; i++) { 171 | await pox.castDocumentVote(i, abstrProp2, false); 172 | } 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /contracts/DelegatedSending.sol: -------------------------------------------------------------------------------- 1 | // simple planet invitation management contract 2 | // https://azimuth.network 3 | 4 | pragma solidity 0.4.24; 5 | 6 | import './Ecliptic.sol'; 7 | 8 | // DelegatedSending: invite-like point sending 9 | // 10 | // This contract allows planet owners to gift planets to their friends, 11 | // if a star has allowed it. 12 | // 13 | // Star owners can grant a number of "invites" to planets. An "invite" in 14 | // the context of this contract means a planet from the same parent star, 15 | // that can be sent to an Ethereum address that owns no points. 16 | // Planets that were sent as invites are also allowed to send invites, but 17 | // instead of adhering to a star-set limit, they will use up invites from 18 | // the same "pool" as their inviter. 19 | // 20 | // To allow planets to be sent by this contract, stars must set it as 21 | // their spawnProxy using the Ecliptic. 22 | // 23 | contract DelegatedSending is ReadsAzimuth 24 | { 25 | // Pool: :who was given their own pool by :prefix, of :size invites 26 | // 27 | event Pool(uint16 indexed prefix, uint32 indexed who, uint16 size); 28 | 29 | // Sent: :by sent :point 30 | // 31 | event Sent( uint16 indexed prefix, 32 | uint32 indexed fromPool, 33 | uint32 by, 34 | uint32 point, 35 | address to); 36 | 37 | // pools: per pool, the amount of planets that can still be given away 38 | // per star by the pool's planet itself or the ones it invited 39 | // 40 | // pools are associated with planets by number, 41 | // then with stars by number. 42 | // pool 0 does not exist, and is used symbolically by :fromPool. 43 | // 44 | mapping(uint32 => mapping(uint16 => uint16)) public pools; 45 | 46 | // fromPool: per planet, the pool from which they send invites 47 | // 48 | // when invited by planet n, the invitee sends from n's pool. 49 | // a pool of 0 means the planet has its own invite pool. 50 | // 51 | mapping(uint32 => uint32) public fromPool; 52 | 53 | // poolStars: per pool, the stars from which it has received invites 54 | // 55 | mapping(uint32 => uint16[]) public poolStars; 56 | 57 | // poolStarsRegistered: per pool, per star, whether or not it is in 58 | // the :poolStars array 59 | // 60 | mapping(uint32 => mapping(uint16 => bool)) public poolStarsRegistered; 61 | 62 | // inviters: points with their own pools, invite tree roots 63 | // 64 | uint32[] public inviters; 65 | 66 | // isInviter: whether or not a point is in the :inviters list 67 | // 68 | mapping(uint32 => bool) public isInviter; 69 | 70 | // invited: for each point, the points they invited 71 | // 72 | mapping(uint32 => uint32[]) public invited; 73 | 74 | // invitedBy: for each point, the point they were invited by 75 | // 76 | mapping(uint32 => uint32) public invitedBy; 77 | 78 | // constructor(): register the azimuth contract 79 | // 80 | constructor(Azimuth _azimuth) 81 | ReadsAzimuth(_azimuth) 82 | public 83 | { 84 | // 85 | } 86 | 87 | // setPoolSize(): give _for their own pool if they don't have one already, 88 | // and allow them to send _size points from _as 89 | // 90 | function setPoolSize(uint16 _as, uint32 _for, uint16 _size) 91 | external 92 | activePointOwner(_as) 93 | { 94 | fromPool[_for] = 0; 95 | pools[_for][_as] = _size; 96 | 97 | // register star as having given invites to pool, 98 | // if that hasn't happened yet 99 | // 100 | if (false == poolStarsRegistered[_for][_as]) { 101 | poolStars[_for].push(_as); 102 | poolStarsRegistered[_for][_as] = true; 103 | } 104 | 105 | // add _for as an invite tree root 106 | // 107 | if (false == isInviter[_for]) 108 | { 109 | isInviter[_for] = true; 110 | inviters.push(_for); 111 | } 112 | 113 | emit Pool(_as, _for, _size); 114 | } 115 | 116 | // sendPoint(): as the point _as, spawn the point _point to _to. 117 | // 118 | // Requirements: 119 | // - :msg.sender must be the owner of _as, 120 | // - _to must not be the :msg.sender, 121 | // - _as must be able to send the _point according to canSend() 122 | // 123 | function sendPoint(uint32 _as, uint32 _point, address _to) 124 | external 125 | activePointOwner(_as) 126 | { 127 | require(canSend(_as, _point)); 128 | 129 | // caller may not send to themselves 130 | // 131 | require(msg.sender != _to); 132 | 133 | // recipient must be eligible to receive a planet from this contract 134 | // 135 | require(canReceive(_to)); 136 | 137 | // remove an invite from _as' current pool 138 | // 139 | uint32 pool = getPool(_as); 140 | uint16 prefix = azimuth.getPrefix(_point); 141 | pools[pool][prefix]--; 142 | 143 | // associate the _point with this pool 144 | // 145 | fromPool[_point] = pool; 146 | 147 | // add _point to _as' invite tree 148 | // 149 | invited[_as].push(_point); 150 | invitedBy[_point] = _as; 151 | 152 | // spawn _point to _to, they still need to accept the transfer manually 153 | // 154 | Ecliptic(azimuth.owner()).spawn(_point, _to); 155 | 156 | emit Sent(prefix, pool, _as, _point, _to); 157 | } 158 | 159 | // canSend(): check whether current conditions allow _as to send _point 160 | // 161 | function canSend(uint32 _as, uint32 _point) 162 | public 163 | view 164 | returns (bool result) 165 | { 166 | uint16 prefix = azimuth.getPrefix(_point); 167 | uint32 pool = getPool(_as); 168 | return ( // _as' pool for this prefix must not have been exhausted yet 169 | // 170 | (0 < pools[pool][prefix]) && 171 | // 172 | // _point needs to not be (in the process of being) spawned 173 | // 174 | azimuth.isOwner(_point, 0x0) && 175 | // 176 | // this contract must have permission to spawn points 177 | // 178 | azimuth.isSpawnProxy(prefix, this) && 179 | // 180 | // the prefix must be linked 181 | // 182 | azimuth.hasBeenLinked(prefix) && 183 | // 184 | // the prefix must not have hit its spawn limit yet 185 | // 186 | ( azimuth.getSpawnCount(prefix) < 187 | Ecliptic(azimuth.owner()) 188 | .getSpawnLimit(prefix, block.timestamp) ) ); 189 | } 190 | 191 | // getPool(): get the invite pool _point belongs to 192 | // 193 | function getPool(uint32 _point) 194 | public 195 | view 196 | returns (uint32 pool) 197 | { 198 | pool = fromPool[_point]; 199 | 200 | // no pool explicitly registered means they have their own pool, 201 | // because they either were not invited by this contract, or have 202 | // been granted their own pool by their star. 203 | // 204 | if (0 == pool) 205 | { 206 | // send from the planet's own pool, see also :fromPool 207 | // 208 | return _point; 209 | } 210 | 211 | return pool; 212 | } 213 | 214 | // canReceive(): whether the _recipient is eligible to receive a planet 215 | // from this contract or not 216 | // 217 | // only those who don't own or are entitled to any points may receive 218 | // 219 | function canReceive(address _recipient) 220 | public 221 | view 222 | returns (bool result) 223 | { 224 | return ( 0 == azimuth.getOwnedPointCount(_recipient) && 225 | 0 == azimuth.getTransferringForCount(_recipient) ); 226 | } 227 | 228 | // getPoolStars(): returns a list of stars _who has pools for 229 | // 230 | function getPoolStars(uint32 _who) 231 | external 232 | view 233 | returns (uint16[] stars) 234 | { 235 | return poolStars[_who]; 236 | } 237 | 238 | // getInviters(): returns a list of all points with their own pools 239 | // 240 | function getInviters() 241 | external 242 | view 243 | returns (uint32[] invs) 244 | { 245 | return inviters; 246 | } 247 | 248 | // getInvited(): returns a list of points invited by _who 249 | // 250 | function getInvited(uint32 _who) 251 | external 252 | view 253 | returns (uint32[] invd) 254 | { 255 | return invited[_who]; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /contracts/PublicResolver.sol: -------------------------------------------------------------------------------- 1 | // https://github.com/ethereum/ens/blob/master/contracts/PublicResolver.sol 2 | 3 | pragma solidity ^0.4.18; 4 | 5 | import './interfaces/ENS.sol'; 6 | 7 | /** 8 | * A simple resolver anyone can use; only allows the owner of a node to set its 9 | * address. 10 | */ 11 | contract PublicResolver { 12 | 13 | bytes4 constant INTERFACE_META_ID = 0x01ffc9a7; 14 | bytes4 constant ADDR_INTERFACE_ID = 0x3b3b57de; 15 | bytes4 constant CONTENT_INTERFACE_ID = 0xd8389dc5; 16 | bytes4 constant NAME_INTERFACE_ID = 0x691f3431; 17 | bytes4 constant ABI_INTERFACE_ID = 0x2203ab56; 18 | bytes4 constant PUBKEY_INTERFACE_ID = 0xc8690233; 19 | bytes4 constant TEXT_INTERFACE_ID = 0x59d1d43c; 20 | bytes4 constant MULTIHASH_INTERFACE_ID = 0xe89401a1; 21 | 22 | event AddrChanged(bytes32 indexed node, address a); 23 | event ContentChanged(bytes32 indexed node, bytes32 hash); 24 | event NameChanged(bytes32 indexed node, string name); 25 | event ABIChanged(bytes32 indexed node, uint256 indexed contentType); 26 | event PubkeyChanged(bytes32 indexed node, bytes32 x, bytes32 y); 27 | event TextChanged(bytes32 indexed node, string indexedKey, string key); 28 | event MultihashChanged(bytes32 indexed node, bytes hash); 29 | 30 | struct PublicKey { 31 | bytes32 x; 32 | bytes32 y; 33 | } 34 | 35 | struct Record { 36 | address addr; 37 | bytes32 content; 38 | string name; 39 | PublicKey pubkey; 40 | mapping(string=>string) text; 41 | mapping(uint256=>bytes) abis; 42 | bytes multihash; 43 | } 44 | 45 | ENS ens; 46 | 47 | mapping (bytes32 => Record) records; 48 | 49 | modifier only_owner(bytes32 node) { 50 | require(ens.owner(node) == msg.sender); 51 | _; 52 | } 53 | 54 | /** 55 | * Constructor. 56 | * @param ensAddr The ENS registrar contract. 57 | */ 58 | function PublicResolver(ENS ensAddr) public { 59 | ens = ensAddr; 60 | } 61 | 62 | /** 63 | * Sets the address associated with an ENS node. 64 | * May only be called by the owner of that node in the ENS registry. 65 | * @param node The node to update. 66 | * @param addr The address to set. 67 | */ 68 | function setAddr(bytes32 node, address addr) public only_owner(node) { 69 | records[node].addr = addr; 70 | AddrChanged(node, addr); 71 | } 72 | 73 | /** 74 | * Sets the content hash associated with an ENS node. 75 | * May only be called by the owner of that node in the ENS registry. 76 | * Note that this resource type is not standardized, and will likely change 77 | * in future to a resource type based on multihash. 78 | * @param node The node to update. 79 | * @param hash The content hash to set 80 | */ 81 | function setContent(bytes32 node, bytes32 hash) public only_owner(node) { 82 | records[node].content = hash; 83 | ContentChanged(node, hash); 84 | } 85 | 86 | /** 87 | * Sets the multihash associated with an ENS node. 88 | * May only be called by the owner of that node in the ENS registry. 89 | * @param node The node to update. 90 | * @param hash The multihash to set 91 | */ 92 | function setMultihash(bytes32 node, bytes hash) public only_owner(node) { 93 | records[node].multihash = hash; 94 | MultihashChanged(node, hash); 95 | } 96 | 97 | /** 98 | * Sets the name associated with an ENS node, for reverse records. 99 | * May only be called by the owner of that node in the ENS registry. 100 | * @param node The node to update. 101 | * @param name The name to set. 102 | */ 103 | function setName(bytes32 node, string name) public only_owner(node) { 104 | records[node].name = name; 105 | NameChanged(node, name); 106 | } 107 | 108 | /** 109 | * Sets the ABI associated with an ENS node. 110 | * Nodes may have one ABI of each content type. To remove an ABI, set it to 111 | * the empty string. 112 | * @param node The node to update. 113 | * @param contentType The content type of the ABI 114 | * @param data The ABI data. 115 | */ 116 | function setABI(bytes32 node, uint256 contentType, bytes data) public only_owner(node) { 117 | // Content types must be powers of 2 118 | require(((contentType - 1) & contentType) == 0); 119 | 120 | records[node].abis[contentType] = data; 121 | ABIChanged(node, contentType); 122 | } 123 | 124 | /** 125 | * Sets the SECP256k1 public key associated with an ENS node. 126 | * @param node The ENS node to query 127 | * @param x the X coordinate of the curve point for the public key. 128 | * @param y the Y coordinate of the curve point for the public key. 129 | */ 130 | function setPubkey(bytes32 node, bytes32 x, bytes32 y) public only_owner(node) { 131 | records[node].pubkey = PublicKey(x, y); 132 | PubkeyChanged(node, x, y); 133 | } 134 | 135 | /** 136 | * Sets the text data associated with an ENS node and key. 137 | * May only be called by the owner of that node in the ENS registry. 138 | * @param node The node to update. 139 | * @param key The key to set. 140 | * @param value The text data value to set. 141 | */ 142 | function setText(bytes32 node, string key, string value) public only_owner(node) { 143 | records[node].text[key] = value; 144 | TextChanged(node, key, key); 145 | } 146 | 147 | /** 148 | * Returns the text data associated with an ENS node and key. 149 | * @param node The ENS node to query. 150 | * @param key The text data key to query. 151 | * @return The associated text data. 152 | */ 153 | function text(bytes32 node, string key) public view returns (string) { 154 | return records[node].text[key]; 155 | } 156 | 157 | /** 158 | * Returns the SECP256k1 public key associated with an ENS node. 159 | * Defined in EIP 619. 160 | * @param node The ENS node to query 161 | * @return x, y the X and Y coordinates of the curve point for the public key. 162 | */ 163 | function pubkey(bytes32 node) public view returns (bytes32 x, bytes32 y) { 164 | return (records[node].pubkey.x, records[node].pubkey.y); 165 | } 166 | 167 | /** 168 | * Returns the ABI associated with an ENS node. 169 | * Defined in EIP205. 170 | * @param node The ENS node to query 171 | * @param contentTypes A bitwise OR of the ABI formats accepted by the caller. 172 | * @return contentType The content type of the return value 173 | * @return data The ABI data 174 | */ 175 | function ABI(bytes32 node, uint256 contentTypes) public view returns (uint256 contentType, bytes data) { 176 | Record storage record = records[node]; 177 | for (contentType = 1; contentType <= contentTypes; contentType <<= 1) { 178 | if ((contentType & contentTypes) != 0 && record.abis[contentType].length > 0) { 179 | data = record.abis[contentType]; 180 | return; 181 | } 182 | } 183 | contentType = 0; 184 | } 185 | 186 | /** 187 | * Returns the name associated with an ENS node, for reverse records. 188 | * Defined in EIP181. 189 | * @param node The ENS node to query. 190 | * @return The associated name. 191 | */ 192 | function name(bytes32 node) public view returns (string) { 193 | return records[node].name; 194 | } 195 | 196 | /** 197 | * Returns the content hash associated with an ENS node. 198 | * Note that this resource type is not standardized, and will likely change 199 | * in future to a resource type based on multihash. 200 | * @param node The ENS node to query. 201 | * @return The associated content hash. 202 | */ 203 | function content(bytes32 node) public view returns (bytes32) { 204 | return records[node].content; 205 | } 206 | 207 | /** 208 | * Returns the multihash associated with an ENS node. 209 | * @param node The ENS node to query. 210 | * @return The associated multihash. 211 | */ 212 | function multihash(bytes32 node) public view returns (bytes) { 213 | return records[node].multihash; 214 | } 215 | 216 | /** 217 | * Returns the address associated with an ENS node. 218 | * @param node The ENS node to query. 219 | * @return The associated address. 220 | */ 221 | function addr(bytes32 node) public view returns (address) { 222 | return records[node].addr; 223 | } 224 | 225 | /** 226 | * Returns true if the resolver implements the interface specified by the provided hash. 227 | * @param interfaceID The ID of the interface to check for. 228 | * @return True if the contract implements the requested interface. 229 | */ 230 | function supportsInterface(bytes4 interfaceID) public pure returns (bool) { 231 | return interfaceID == ADDR_INTERFACE_ID || 232 | interfaceID == CONTENT_INTERFACE_ID || 233 | interfaceID == NAME_INTERFACE_ID || 234 | interfaceID == ABI_INTERFACE_ID || 235 | interfaceID == PUBKEY_INTERFACE_ID || 236 | interfaceID == TEXT_INTERFACE_ID || 237 | interfaceID == MULTIHASH_INTERFACE_ID || 238 | interfaceID == INTERFACE_META_ID; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /contracts/LinearStarRelease.sol: -------------------------------------------------------------------------------- 1 | // linear star release 2 | // https://azimuth.network 3 | 4 | pragma solidity 0.4.24; 5 | 6 | import './Ecliptic.sol'; 7 | import './TakesPoints.sol'; 8 | 9 | import './SafeMath16.sol'; 10 | import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; 11 | 12 | // LinearStarRelease: batch transfer over time 13 | // 14 | // This contract allows its owner to transfer a batch of stars to a 15 | // recipient (also "participant") gradually, at a set rate of an 16 | // amount of stars per a period of time, after an optional waiting 17 | // period measured from the launch of this contract. 18 | // 19 | // The owner of this contract can register batches and deposit stars 20 | // into them. Participants can withdraw stars as they get released 21 | // and transfer ownership of their batch to another address. 22 | // If, ten years after the contract launch, any stars remain, the 23 | // contract owner is able to withdraw them. This saves address space from 24 | // being lost forever in case of key loss by participants. 25 | // 26 | contract LinearStarRelease is Ownable, TakesPoints 27 | { 28 | using SafeMath for uint256; 29 | using SafeMath16 for uint16; 30 | 31 | // escapeHatchTime: amount of time after the time of contract launch, after 32 | // which the contract owner can withdraw arbitrary stars 33 | // 34 | uint256 constant escapeHatchTime = 10 * 365 days; 35 | 36 | // start: global release start time 37 | // 38 | uint256 public start; 39 | 40 | // Batch: stars that unlock for a participant 41 | // 42 | // While the ordering of the struct members is semantically chaotic, 43 | // they are ordered to tightly pack them into Ethereum's 32-byte storage 44 | // slots, which reduces gas costs for some function calls. 45 | // The comment ticks indicate assumed slot boundaries. 46 | // 47 | struct Batch 48 | { 49 | // stars: specific stars assigned to this batch that have not yet 50 | // been withdrawn 51 | // 52 | uint16[] stars; 53 | // 54 | // windup: amount of time it takes for stars to start becoming 55 | // available for withdrawal (start unlocking), after the 56 | // release has started globally (:start) 57 | // 58 | uint256 windup; 59 | // 60 | // rateUnit: amount of time it takes for the next :rate stars to be 61 | // released/unlocked 62 | // 63 | uint256 rateUnit; 64 | // 65 | // withdrawn: number of stars withdrawn from this batch 66 | // 67 | uint16 withdrawn; 68 | 69 | // rate: number of stars released per :rateUnit 70 | // 71 | uint16 rate; 72 | 73 | // amount: promised amount of stars 74 | // 75 | uint16 amount; 76 | 77 | // approvedTransferTo: batch can be transferred to this address 78 | // 79 | address approvedTransferTo; 80 | } 81 | 82 | // batches: per participant, the registered star release 83 | // 84 | mapping(address => Batch) public batches; 85 | 86 | // constructor(): register azimuth contract 87 | // 88 | constructor(Azimuth _azimuth) 89 | TakesPoints(_azimuth) 90 | public 91 | { 92 | // 93 | } 94 | 95 | // 96 | // Functions for the contract owner 97 | // 98 | 99 | // register(): register a new star batch 100 | // 101 | function register( // _participant: address of the participant 102 | // _windup: time until first release 103 | // _amount: the promised amount of stars 104 | // _rate: number of stars that unlock per _rateUnit 105 | // _rateUnit: amount of time it takes for the next 106 | // _rate stars to unlock 107 | // 108 | address _participant, 109 | uint256 _windup, 110 | uint16 _amount, 111 | uint16 _rate, 112 | uint256 _rateUnit ) 113 | external 114 | onlyOwner 115 | { 116 | Batch storage batch = batches[_participant]; 117 | 118 | // make sure this participant doesn't already have a batch registered 119 | // 120 | require(0 == batch.amount); 121 | 122 | // make sure batch details are sane 123 | // 124 | require( (_rate > 0) && 125 | (_rateUnit > 0) && 126 | (_amount > 0) ); 127 | 128 | batch.windup = _windup; 129 | batch.amount = _amount; 130 | batch.rate = _rate; 131 | batch.rateUnit = _rateUnit; 132 | } 133 | 134 | // deposit(): deposit a star into this contract for later withdrawal 135 | // 136 | function deposit(address _participant, uint16 _star) 137 | external 138 | onlyOwner 139 | { 140 | Batch storage batch = batches[_participant]; 141 | 142 | // ensure we can only deposit stars, and that we can't deposit 143 | // more stars than necessary 144 | // 145 | require( (_star > 0xff) && 146 | (batch.stars.length < batch.amount.sub(batch.withdrawn)) ); 147 | 148 | // have the contract take ownership of the star if possible, 149 | // reverting if that fails. 150 | // 151 | require( takePoint(_star, true) ); 152 | 153 | // add _star to the participant's star balance 154 | // 155 | batch.stars.push(_star); 156 | } 157 | 158 | // startReleasing(): start the process of releasing stars 159 | // 160 | function startReleasing() 161 | external 162 | onlyOwner 163 | { 164 | // make sure we haven't started yet 165 | // 166 | require(0 == start); 167 | start = block.timestamp; 168 | } 169 | 170 | // withdrawOverdue(): withdraw arbitrary star from the contract 171 | // 172 | // this functions acts as an escape hatch in the case of key loss, 173 | // to prevent blocks of address space from being lost permanently. 174 | // 175 | function withdrawOverdue(address _participant, address _to) 176 | external 177 | onlyOwner 178 | { 179 | // this can only be done :escapeHatchTime after the release start 180 | // 181 | require( (0 < start) && 182 | (block.timestamp > start.add(escapeHatchTime)) ); 183 | 184 | // withdraw a star from the batch 185 | // 186 | performWithdraw(batches[_participant], _to, false); 187 | } 188 | 189 | // 190 | // Functions for participants 191 | // 192 | 193 | // approveBatchTransfer(): transfer the batch to another address 194 | // 195 | function approveBatchTransfer(address _to) 196 | external 197 | { 198 | // make sure the caller is a participant, 199 | // and that the target isn't 200 | // 201 | require( 0 != batches[msg.sender].amount && 202 | 0 == batches[_to].amount ); 203 | batches[msg.sender].approvedTransferTo = _to; 204 | } 205 | 206 | // transferBatch(): make an approved transfer of _from's batch 207 | // to the caller's address 208 | // 209 | function transferBatch(address _from) 210 | external 211 | { 212 | // make sure the :msg.sender is authorized to make this transfer 213 | // 214 | require(batches[_from].approvedTransferTo == msg.sender); 215 | 216 | // make sure the target isn't also a participant 217 | // 218 | require(0 == batches[msg.sender].amount); 219 | 220 | // copy the batch to the :msg.sender and clear _from's 221 | // 222 | Batch storage com = batches[_from]; 223 | batches[msg.sender] = com; 224 | batches[_from] = Batch(new uint16[](0), 0, 0, 0, 0, 0, 0x0); 225 | } 226 | 227 | // withdraw(): withdraw one star to the sender's address 228 | // 229 | function withdraw() 230 | external 231 | { 232 | withdraw(msg.sender); 233 | } 234 | 235 | // withdraw(): withdraw one star from the sender's batch to _to 236 | // 237 | function withdraw(address _to) 238 | public 239 | { 240 | Batch storage batch = batches[msg.sender]; 241 | 242 | // to withdraw, the participant must have a star balance 243 | // and be under their current withdrawal limit 244 | // 245 | require( (batch.stars.length > 0) && 246 | (batch.withdrawn < withdrawLimit(msg.sender)) ); 247 | 248 | // withdraw a star from the batch 249 | // 250 | performWithdraw(batch, _to, true); 251 | } 252 | 253 | // 254 | // Internal functions 255 | // 256 | 257 | // performWithdraw(): withdraw a star from _batch to _to 258 | // 259 | function performWithdraw(Batch storage _batch, address _to, bool _reset) 260 | internal 261 | { 262 | // star: star being withdrawn 263 | // 264 | uint16 star = _batch.stars[_batch.stars.length.sub(1)]; 265 | 266 | // remove the star from the batch 267 | // 268 | _batch.stars.length = _batch.stars.length.sub(1); 269 | _batch.withdrawn = _batch.withdrawn.add(1); 270 | 271 | // transfer :star 272 | // 273 | require( givePoint(star, _to, _reset) ); 274 | } 275 | 276 | // 277 | // Public operations and utilities 278 | // 279 | 280 | // withdrawLimit(): return the number of stars _participant can withdraw 281 | // at the current block timestamp 282 | // 283 | function withdrawLimit(address _participant) 284 | public 285 | view 286 | returns (uint16 limit) 287 | { 288 | // if we haven't started releasing yet, limit is always zero 289 | // 290 | if (0 == start) 291 | { 292 | return 0; 293 | } 294 | 295 | uint256 allowed = 0; 296 | Batch storage batch = batches[_participant]; 297 | 298 | // only do real calculations if the windup time is over 299 | // 300 | uint256 realStart = start.add(batch.windup); 301 | if ( block.timestamp > realStart ) 302 | { 303 | // calculate the amount of stars available from this batch by 304 | // multiplying the release rate (stars per :rateUnit) by the number 305 | // of :rateUnits that have passed since the windup period ended 306 | // 307 | allowed = uint256(batch.rate).mul( 308 | ( block.timestamp.sub(realStart) / 309 | batch.rateUnit ) ); 310 | } 311 | 312 | // allow at least one star 313 | // 314 | if ( allowed < 1 ) 315 | { 316 | return 1; 317 | } 318 | // 319 | // don't allow more than the promised amount 320 | // 321 | else if (allowed > batch.amount) 322 | { 323 | return batch.amount; 324 | } 325 | return uint16(allowed); 326 | } 327 | 328 | // verifyBalance: check the balance of _participant 329 | // 330 | // Note: for use by clients, to verify the contract owner 331 | // has deposited all the stars they're entitled to. 332 | // 333 | function verifyBalance(address _participant) 334 | external 335 | view 336 | returns (bool correct) 337 | { 338 | Batch storage batch = batches[_participant]; 339 | 340 | // return true if this contract holds as many stars as we'll ever 341 | // be entitled to withdraw 342 | // 343 | return ( batch.amount.sub(batch.withdrawn) == batch.stars.length ); 344 | } 345 | 346 | // getRemainingStars(): get the stars deposited into the batch 347 | // 348 | // Note: only useful for clients, as Solidity does not currently 349 | // support returning dynamic arrays. 350 | // 351 | function getRemainingStars(address _participant) 352 | external 353 | view 354 | returns (uint16[] stars) 355 | { 356 | return batches[_participant].stars; 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /test/TestConditionalStarRelease.js: -------------------------------------------------------------------------------- 1 | const Azimuth = artifacts.require('Azimuth'); 2 | const Polls = artifacts.require('Polls'); 3 | const Claims = artifacts.require('Claims'); 4 | const Ecliptic = artifacts.require('Ecliptic'); 5 | const CSR = artifacts.require('ConditionalStarRelease'); 6 | 7 | const assertRevert = require('./helpers/assertRevert'); 8 | const increaseTime = require('./helpers/increaseTime'); 9 | 10 | contract('Conditional Star Release', function([owner, user1, user2, user3]) { 11 | let azimuth, azimuth2, polls, claims, eclipt, eclipt2, csr, csr2, 12 | deadline1, deadline2, deadline3, deadline4, condit2, rateUnit, 13 | deadlineStep, escapeHatchTime, escapeHatchDate; 14 | 15 | function assertInvalid(error) { 16 | assert.isAbove(error.message.search('invalid opcode'), -1, 'Invalid opcode must be returned, but got ' + error); 17 | } 18 | 19 | function getChainTime() { 20 | return new Promise((resolve, reject) => { 21 | web3.currentProvider.send({ 22 | jsonrpc: '2.0', 23 | method: 'eth_getBlockByNumber', 24 | params: ['latest', false], 25 | id: 'csr-getting-time', 26 | }, (err, res) => { 27 | if (err) return reject(err); 28 | return resolve(res.result.timestamp); 29 | }); 30 | }); 31 | }; 32 | 33 | before('setting up for tests', async function() { 34 | deadlineStep = 100; 35 | condit2 = web3.utils.toHex(123456789); 36 | rateUnit = deadlineStep * 10; 37 | azimuth = await Azimuth.new(); 38 | azimuth2 = await Azimuth.new(); 39 | polls = await Polls.new(432000, 432000); 40 | claims = await Claims.new(azimuth.address); 41 | eclipt = await Ecliptic.new('0x0000000000000000000000000000000000000001', 42 | azimuth.address, 43 | polls.address, 44 | claims.address); 45 | eclipt2 = await Ecliptic.new('0x0000000000000000000000000000000000000000', 46 | azimuth2.address, 47 | polls.address, 48 | claims.address) 49 | await azimuth.transferOwnership(eclipt.address); 50 | await azimuth2.transferOwnership(eclipt2.address); 51 | await polls.transferOwnership(eclipt.address); 52 | await eclipt.createGalaxy(0, owner); 53 | await eclipt.configureKeys(web3.utils.toHex(0), 54 | web3.utils.toHex(1), 55 | web3.utils.toHex(2), 56 | web3.utils.toHex(1), 57 | false); 58 | await eclipt.spawn(256, owner); 59 | await eclipt.spawn(2560, owner); 60 | await eclipt.configureKeys(web3.utils.toHex(2560), 61 | web3.utils.toHex(1), 62 | web3.utils.toHex(2), 63 | web3.utils.toHex(1), 64 | false); 65 | deadline1 = web3.utils.toDecimal(await getChainTime()) + 10; 66 | deadline2 = deadline1 + deadlineStep; 67 | deadline3 = deadline2 + deadlineStep; 68 | deadline4 = deadline3 + deadlineStep; 69 | escapeHatchTime = deadlineStep * 100; 70 | escapeHatchDate = web3.utils.toDecimal(await getChainTime()) + escapeHatchTime; 71 | csr = await CSR.new( azimuth.address, [web3.utils.toHex(0), 72 | condit2, 73 | web3.utils.toHex("miss me"), 74 | web3.utils.toHex("too")], 75 | [0, 0, 0, 0], 76 | [deadline1, deadline2, deadline3, deadline4], 77 | escapeHatchDate ); 78 | csr2 = await CSR.new( azimuth2.address, [web3.utils.toHex(0), 79 | condit2, 80 | web3.utils.toHex("miss me"), 81 | web3.utils.toHex("too")], 82 | [0, 0, 0, 0], 83 | [deadline1, deadline2, deadline3, deadline4], 84 | escapeHatchDate ); 85 | await eclipt.setSpawnProxy(0, csr.address); 86 | await eclipt.setTransferProxy(256, csr.address); 87 | }); 88 | 89 | it('creation sanity check', async function() { 90 | // need as many deadlines as conditions 91 | await assertRevert(CSR.new(azimuth.address, [web3.utils.toHex(0), condit2], [0, 0], [0], 1)); 92 | await assertRevert(CSR.new(azimuth.address, [web3.utils.toHex(0), condit2], [0], [0, 0], 1)); 93 | // can't have too many conditions 94 | let many = [0, 0, 0, 0, 0, 0, 0, 0, 0]; 95 | await assertRevert(CSR.new(azimuth.address, many.map(n => web3.utils.toHex(n)), many, many, 1)); 96 | // can't have unfair escape hatch 97 | let few = [2, 2, 2]; 98 | await assertRevert(CSR.new(azimuth.address, few.map(n => web3.utils.toHex(n)), few, few, 1)); 99 | }); 100 | 101 | it('analyzing conditions', async function() { 102 | // first condition is zero, so might get automatically unlocked on-construct 103 | assert.notEqual(await csr.timestamps(0), 0); 104 | assert.equal(await csr2.timestamps(0), 0); 105 | // other conditions should not have timestamps yet. 106 | assert.equal(await csr.timestamps(3), 0); 107 | await csr.analyzeCondition(1); 108 | assert.equal(await csr.timestamps(1), 0); 109 | // fulfill condition 2 110 | await eclipt.startDocumentPoll(0, condit2); 111 | await eclipt.castDocumentVote(0, condit2, true); 112 | assert.isTrue(await polls.documentHasAchievedMajority(condit2)); 113 | await csr.analyzeCondition(1, {from:user1}); 114 | assert.notEqual(await csr.timestamps(1), 0); 115 | // can't analyze twice 116 | await assertRevert(csr.analyzeCondition(1, {from:user1})); 117 | // miss deadline for condition 3 118 | await increaseTime((deadlineStep * 2) + 10); 119 | await csr.analyzeCondition(2); 120 | assert.equal(await csr.timestamps(2), deadline3); 121 | // verify contract state getters work 122 | let { conds, lives, deads, times } = await csr.getConditionsState(); 123 | assert.equal(conds[3], 124 | // "too" 125 | "0x746f6f0000000000000000000000000000000000000000000000000000000000"); 126 | assert.equal(lives[3], 0); 127 | assert.equal(deads[3], deadline4); 128 | assert.equal(times[2], deadline3); 129 | assert.equal(times[3], 0); 130 | }); 131 | 132 | it('registering commitments', async function() { 133 | // only owner can do this 134 | await assertRevert(csr.register(user1, [4, 1, 2, 1], 1, rateUnit, {from:user1})); 135 | // need right amount of conditions 136 | await assertRevert(csr.register(user1, [4, 1, 2], 1, rateUnit)); 137 | // need a sane rate 138 | await assertRevert(csr.register(user1, [4, 1, 2, 1], 0, rateUnit)); 139 | // must contain stars 140 | await assertRevert(csr.register(user1, [0, 0, 0, 0], 1, rateUnit)); 141 | assert.isTrue(await csr.verifyBalance(user1)); 142 | await csr.register(user1, [4, 1, 2, 1], 1, rateUnit); 143 | await csr.register(user3, [0, 1, 2, 1], 1, rateUnit); 144 | // can't register twice 145 | await assertRevert(csr.register(user3, [4, 1, 2, 1], 1, rateUnit)); 146 | assert.equal((await csr.commitments(user1))[2], 8); 147 | assert.isFalse(await csr.verifyBalance(user1)); 148 | // can always withdraw at least one star from the first batch that has stars 149 | assert.equal(await csr.withdrawLimit(user1, 0), 1); 150 | assert.equal(await csr.withdrawLimit(user1, 1), 0); 151 | assert.equal(await csr.withdrawLimit(user3, 0), 0); 152 | assert.equal(await csr.withdrawLimit(user3, 1), 1); 153 | let batches = await csr.getBatches(user1); 154 | assert.equal(batches[0], 4); 155 | assert.equal(batches[1], 1); 156 | assert.equal(batches[2], 2); 157 | assert.equal(await csr.getBatch(user1, 2), 2); 158 | assert.equal(batches[3], 1); 159 | assert.equal(batches.length, 4); 160 | }); 161 | 162 | it('forfeiting early', async function() { 163 | // can't forfeit when deadline hasn't been missed 164 | await assertRevert(csr.forfeit(3, {from:user1})); 165 | }); 166 | 167 | it('withdraw limit', async function() { 168 | assert.equal(await csr.withdrawLimit(user1, 0), 1); 169 | assert.equal(await csr.withdrawLimit(user1, 3), 0); 170 | await increaseTime(rateUnit*2); 171 | assert.equal(await csr.withdrawLimit(user1, 0), 2); 172 | // unregistered address should not yet have a withdraw limit 173 | assert.equal(await csr.withdrawLimit(user2, 0), 0); 174 | }); 175 | 176 | it('depositing stars', async function() { 177 | // only owner can do this 178 | await assertRevert(csr.deposit(user1, 256, {from:user1})); 179 | // can't deposit a live star 180 | await assertRevert(csr.deposit(user1, 2560)); 181 | // deposit spawned star, as star owner 182 | await csr.deposit(user1, 256); 183 | // deposit unspawned stars, as galaxy owner 184 | for (var s = 2; s < 9; s++) { 185 | await csr.deposit(user1, s*256); 186 | } 187 | assert.equal((await csr.getRemainingStars(user1)).length, 8); 188 | assert.equal((await csr.getRemainingStars(user1))[7], 2048); 189 | assert.isTrue(await azimuth.isOwner(256, csr.address)); 190 | assert.isTrue(await csr.verifyBalance(user1)); 191 | // can't deposit too many 192 | await assertRevert(csr.deposit(user1, 2304)); 193 | }); 194 | 195 | it('withdrawing', async function() { 196 | await increaseTime(rateUnit); 197 | assert.equal(await csr.withdrawLimit(user1, 0), 3); 198 | // only commitment participant can do this 199 | await assertRevert(csr.withdrawToSelf(0, {from:owner})); 200 | await csr.withdrawToSelf(0, {from:user1}); 201 | assert.isTrue(await azimuth.isOwner(2048, user1)); 202 | assert.equal((await csr.getWithdrawn(user1))[0], 1); 203 | // can't withdraw over limit 204 | assert.equal(await csr.withdrawLimit(user1, 0), 3); 205 | await csr.withdraw(0, user1, {from:user1}); 206 | assert.isTrue(await azimuth.isOwner(1792, user1)); 207 | await csr.withdrawToSelf(0, {from:user1}); 208 | assert.equal(await csr.getWithdrawnFromBatch(user1, 0), 3); 209 | await assertRevert(csr.withdrawToSelf(0, {from:user1})); 210 | }); 211 | 212 | it('transferring commitment', async function() { 213 | assert.equal((await csr.commitments(user1))[1], 0); 214 | // can't transfer to other participant 215 | await assertRevert(csr.approveCommitmentTransfer(user3, {from:user1})); 216 | // can't transfer without permission 217 | await assertRevert(csr.transferCommitment(user1, {from:user2})); 218 | await csr.approveCommitmentTransfer(user2, {from:user1}); 219 | await csr.approveCommitmentTransfer(user2, {from:user3}); 220 | assert.equal((await csr.commitments(user1))[1], user2); 221 | await csr.transferCommitment(user1, {from:user2}); 222 | assert.notEqual(await csr.withdrawLimit(user2, 0), 0); 223 | // can't if we became a participant in the mean time 224 | await assertRevert(csr.transferCommitment(user3, {from:user2})); 225 | // unregistered address should no longer have batches, etc 226 | let batches = await csr.getBatches(user1); 227 | assert.equal(batches.length, 0); 228 | }); 229 | 230 | it('forfeiting and withdrawing', async function() { 231 | // owner can't withdraw if not forfeited 232 | assert.isFalse((await csr.getForfeited(user2))[2]); 233 | await assertRevert(csr.withdrawForfeited(user2, 2, owner)); 234 | // can't forfeit if no commitment 235 | await assertRevert(csr.forfeit(2, {from:user1})); 236 | await csr.forfeit(2, {from:user2}); 237 | assert.isTrue(await csr.hasForfeitedBatch(user2, 2)); 238 | // can't forfeit twice 239 | await assertRevert(csr.forfeit(2, {from:user2})); 240 | // can't withdraw because of forfeit 241 | await assertRevert(csr.withdrawToSelf(2, {from:user2})); 242 | // can't forfeit when we've withdrawn 243 | await csr.analyzeCondition(3); 244 | assert.equal(await csr.timestamps(3), deadline4); 245 | await csr.withdrawToSelf(3, {from:user2}); 246 | await assertRevert(csr.forfeit(3, {from:user2})); 247 | // only owner can still withdraw 248 | await assertRevert(csr.withdrawForfeited(user2, 2, user2, {from:user2})); 249 | await csr.withdrawForfeited(user2, 2, owner); 250 | assert.isTrue(await azimuth.isOwner(1024, owner)); 251 | }); 252 | 253 | it('escape hatch', async function() { 254 | await assertRevert(csr.withdrawOverdue(user2, owner)); 255 | await increaseTime(escapeHatchTime); 256 | await csr.withdrawOverdue(user2, owner); 257 | assert.isTrue(await azimuth.isOwner(2560, owner)); 258 | }); 259 | }); 260 | -------------------------------------------------------------------------------- /test-extras/TestERC721Ecliptic.js: -------------------------------------------------------------------------------- 1 | // adapted from: 2 | // https://github.com/0xcert/ethereum-erc721/blob/master/test/tokens/NFToken.test.js 3 | 4 | const Azimuth = artifacts.require('Azimuth'); 5 | const Polls = artifacts.require('Polls'); 6 | const Claims = artifacts.require('Claims'); 7 | const Ecliptic = artifacts.require('Ecliptic'); 8 | const TokenReceiverMock = artifacts.require('NFTokenReceiverTestMock'); 9 | 10 | // the below hacks around the fact that truffle doesn't play well with overloads 11 | 12 | const web3abi = require('web3-eth-abi'); 13 | const web3 = Ecliptic.web3; 14 | 15 | const overloadedSafeTransferFrom = { 16 | "constant": false, 17 | "inputs": [ 18 | { 19 | "name": "_from", 20 | "type": "address" 21 | }, 22 | { 23 | "name": "_to", 24 | "type": "address" 25 | }, 26 | { 27 | "name": "_tokenId", 28 | "type": "uint256" 29 | }, 30 | { 31 | "name": "_data", 32 | "type": "bytes" 33 | } 34 | ], 35 | "name": "safeTransferFrom", 36 | "outputs": [], 37 | "payable": false, 38 | "stateMutability": "nonpayable", 39 | "type": "function" 40 | }; 41 | 42 | const assertRevert = require('../test/helpers/assertRevert'); 43 | const seeEvents = require('../test/helpers/seeEvents'); 44 | 45 | contract('NFTokenMock', (accounts) => { 46 | let azimuth, polls, claims, nftoken; 47 | const id1 = 1; 48 | const id2 = 2; 49 | const id3 = 3; 50 | const id4 = 40; 51 | 52 | beforeEach(async () => { 53 | azimuth = await Azimuth.new(); 54 | polls = await Polls.new(432000, 432000); 55 | claims = await Claims.new(azimuth.address); 56 | nftoken = await Ecliptic.new('0x0000000000000000000000000000000000000000', 57 | azimuth.address, 58 | polls.address, 59 | claims.address); 60 | azimuth.transferOwnership(nftoken.address); 61 | polls.transferOwnership(nftoken.address); 62 | }); 63 | 64 | it('correctly checks all the supported interfaces', async () => { 65 | const nftokenInterface = await nftoken.supportsInterface('0x80ac58cd'); 66 | const nftokenNonExistingInterface = await nftoken.supportsInterface('0xffffffff'); 67 | assert.equal(nftokenInterface, true); 68 | assert.equal(nftokenNonExistingInterface, false); 69 | }); 70 | 71 | it('returns correct balanceOf after mint', async () => { 72 | await nftoken.createGalaxy(id1, accounts[0]); 73 | const count = await nftoken.balanceOf(accounts[0]); 74 | assert.equal(count.toNumber(), 1); 75 | }); 76 | 77 | it('correctly tells if NFT exists', async () => { 78 | await nftoken.createGalaxy(id1, accounts[0]); 79 | assert.isTrue(await nftoken.exists(id1)); 80 | assert.isFalse(await nftoken.exists(id2)); 81 | assert.isFalse(await nftoken.exists(id1+4294967296)); 82 | }); 83 | 84 | it('throws when trying to mint 2 NFTs with the same claim', async () => { 85 | await nftoken.createGalaxy(id2, accounts[0]); 86 | await assertRevert(nftoken.createGalaxy(id2, accounts[0])); 87 | }); 88 | 89 | it('throws when trying to mint NFT to 0x0 address ', async () => { 90 | await assertRevert(nftoken.createGalaxy(id3, '0x0000000000000000000000000000000000000000')); 91 | }); 92 | 93 | it('finds the correct amount of NFTs owned by account', async () => { 94 | await nftoken.createGalaxy(id2, accounts[1]); 95 | await nftoken.transferPoint(id2, accounts[1], false, {from:accounts[1]}); 96 | await nftoken.createGalaxy(id3, accounts[1]); 97 | await nftoken.transferPoint(id3, accounts[1], false, {from:accounts[1]}); 98 | const count = await nftoken.balanceOf(accounts[1]); 99 | assert.equal(count.toNumber(), 2); 100 | }); 101 | 102 | it('throws when trying to get count of NFTs owned by 0x0 address', async () => { 103 | await assertRevert(nftoken.balanceOf('0x0000000000000000000000000000000000000000')); 104 | }); 105 | 106 | it('finds the correct owner of NFToken id', async () => { 107 | await nftoken.createGalaxy(id2, accounts[1]); 108 | await nftoken.transferPoint(id2, accounts[1], false, {from:accounts[1]}); 109 | const address = await nftoken.ownerOf(id2); 110 | assert.equal(address, accounts[1]); 111 | }); 112 | 113 | it('throws when trying to find owner od non-existing NFT id', async () => { 114 | await assertRevert(nftoken.ownerOf(id4)); 115 | }); 116 | 117 | it('correctly approves account', async () => { 118 | await nftoken.createGalaxy(id2, accounts[0]); 119 | await nftoken.approve(accounts[1], id2); 120 | const address = await nftoken.getApproved(id2); 121 | assert.equal(address, accounts[1]); 122 | }); 123 | 124 | it('correctly cancels approval of account[1]', async () => { 125 | await nftoken.createGalaxy(id2, accounts[0]); 126 | await nftoken.approve(accounts[1], id2); 127 | await nftoken.approve('0x0000000000000000000000000000000000000000', 128 | id2); 129 | const address = await nftoken.getApproved(id2); 130 | assert.equal(address, 0); 131 | }); 132 | 133 | it('throws when trying to get approval of non-existing NFT id', async () => { 134 | await assertRevert(nftoken.getApproved(id4)); 135 | }); 136 | 137 | 138 | it('throws when trying to approve NFT ID which it does not own', async () => { 139 | await nftoken.createGalaxy(id2, accounts[1]); 140 | await nftoken.transferPoint(id2, accounts[1], false, {from:accounts[1]}); 141 | await assertRevert(nftoken.approve(accounts[2], id2, {from: accounts[2]})); 142 | const address = await nftoken.getApproved(id2); 143 | assert.equal(address, 0); 144 | }); 145 | 146 | it('throws when trying to approve NFT ID which it already owns', async () => { 147 | await nftoken.createGalaxy(id2, accounts[1]); 148 | await nftoken.transferPoint(id2, accounts[1], false, {from:accounts[1]}); 149 | await assertRevert(nftoken.approve(accounts[1], id2)); 150 | const address = await nftoken.getApproved(id2); 151 | assert.equal(address, 0); 152 | }); 153 | 154 | it('correctly sets an operator', async () => { 155 | await nftoken.createGalaxy(id2, accounts[0]); 156 | await seeEvents(nftoken.setApprovalForAll(accounts[6], true), 157 | ['ApprovalForAll']); 158 | const isApprovedForAll = await nftoken.isApprovedForAll(accounts[0], accounts[6]); 159 | assert.equal(isApprovedForAll, true); 160 | }); 161 | 162 | it('correctly sets then cancels an operator', async () => { 163 | await nftoken.createGalaxy(id2, accounts[0]); 164 | await nftoken.setApprovalForAll(accounts[6], true); 165 | await nftoken.setApprovalForAll(accounts[6], false); 166 | 167 | const isApprovedForAll = await nftoken.isApprovedForAll(accounts[0], accounts[6]); 168 | assert.equal(isApprovedForAll, false); 169 | }); 170 | 171 | it('throws when trying to set a zero address as operator', async () => { 172 | await assertRevert(nftoken.setApprovalForAll('0x0000000000000000000000000000000000000000', 173 | true)); 174 | }); 175 | 176 | it('correctly transfers NFT from owner', async () => { 177 | const sender = accounts[1]; 178 | const recipient = accounts[2]; 179 | 180 | await nftoken.createGalaxy(id2, sender); 181 | await nftoken.transferPoint(id2, sender, false, {from:sender}); 182 | await seeEvents(nftoken.transferFrom(sender, recipient, id2, {from: sender}), 183 | ['Transfer']); 184 | 185 | const senderBalance = await nftoken.balanceOf(sender); 186 | const recipientBalance = await nftoken.balanceOf(recipient); 187 | const ownerOfId2 = await nftoken.ownerOf(id2); 188 | 189 | assert.equal(senderBalance, 0); 190 | assert.equal(recipientBalance, 1); 191 | assert.equal(ownerOfId2, recipient); 192 | }); 193 | 194 | it('correctly transfers NFT from approved address', async () => { 195 | const sender = accounts[1]; 196 | const recipient = accounts[2]; 197 | const owner = accounts[3]; 198 | 199 | await nftoken.createGalaxy(id2, owner); 200 | await nftoken.transferPoint(id2, owner, false, {from:owner}); 201 | await nftoken.approve(sender, id2, {from: owner}); 202 | await seeEvents(nftoken.transferFrom(owner, recipient, id2, {from: sender}), 203 | ['Transfer']); 204 | 205 | const ownerBalance = await nftoken.balanceOf(owner); 206 | const recipientBalance = await nftoken.balanceOf(recipient); 207 | const ownerOfId2 = await nftoken.ownerOf(id2); 208 | 209 | assert.equal(ownerBalance, 0); 210 | assert.equal(recipientBalance, 1); 211 | assert.equal(ownerOfId2, recipient); 212 | }); 213 | 214 | it('corectly transfers NFT as operator', async () => { 215 | const sender = accounts[1]; 216 | const recipient = accounts[2]; 217 | const owner = accounts[3]; 218 | 219 | await nftoken.createGalaxy(id2, owner); 220 | await nftoken.transferPoint(id2, owner, false, {from:owner}); 221 | await nftoken.setApprovalForAll(sender, true, {from: owner}); 222 | await seeEvents(nftoken.transferFrom(owner, recipient, id2, {from: sender}), 223 | ['Transfer']); 224 | 225 | const ownerBalance = await nftoken.balanceOf(owner); 226 | const recipientBalance = await nftoken.balanceOf(recipient); 227 | const ownerOfId2 = await nftoken.ownerOf(id2); 228 | 229 | assert.equal(ownerBalance, 0); 230 | assert.equal(recipientBalance, 1); 231 | assert.equal(ownerOfId2, recipient); 232 | }); 233 | 234 | it('throws when trying to transfer NFT as an address that is not owner, approved or operator', async () => { 235 | const sender = accounts[1]; 236 | const recipient = accounts[2]; 237 | const owner = accounts[3]; 238 | 239 | await nftoken.createGalaxy(id2, owner); 240 | await nftoken.transferPoint(id2, owner, false, {from:owner}); 241 | await assertRevert(nftoken.transferFrom(owner, recipient, id2, {from: sender})); 242 | }); 243 | 244 | it('throws when trying to transfer NFT to a zero address', async () => { 245 | const owner = accounts[3]; 246 | 247 | await nftoken.createGalaxy(id2, owner); 248 | await nftoken.transferPoint(id2, owner, false, {from:owner}); 249 | await assertRevert(nftoken.transferFrom(owner, 250 | '0x0000000000000000000000000000000000000000', 251 | id2, 252 | {from: owner})); 253 | }); 254 | 255 | it('throws when trying to transfer a invalid NFT', async () => { 256 | const owner = accounts[3]; 257 | const recipient = accounts[2]; 258 | 259 | await nftoken.createGalaxy(id2, owner); 260 | await nftoken.transferPoint(id2, owner, false, {from:owner}); 261 | await assertRevert(nftoken.transferFrom(owner, recipient, id3, {from: owner})); 262 | }); 263 | 264 | it('correctly safe transfers NFT from owner', async () => { 265 | const sender = accounts[1]; 266 | const recipient = accounts[2]; 267 | 268 | await nftoken.createGalaxy(id2, sender); 269 | await nftoken.transferPoint(id2, sender, false, {from:sender}); 270 | await seeEvents(nftoken.safeTransferFrom(sender, recipient, id2, {from: 271 | sender}), ['Transfer']); 272 | 273 | const senderBalance = await nftoken.balanceOf(sender); 274 | const recipientBalance = await nftoken.balanceOf(recipient); 275 | const ownerOfId2 = await nftoken.ownerOf(id2); 276 | 277 | assert.equal(senderBalance, 0); 278 | assert.equal(recipientBalance, 1); 279 | assert.equal(ownerOfId2, recipient); 280 | }); 281 | 282 | it('throws when trying to safe transfer NFT from owner to a smart contract', async () => { 283 | const sender = accounts[1]; 284 | const recipient = nftoken.address; 285 | 286 | await nftoken.createGalaxy(id2, sender); 287 | await nftoken.transferPoint(id2, sender, false, {from:sender}); 288 | await assertRevert(nftoken.safeTransferFrom(sender, recipient, id2, {from: sender})); 289 | }); 290 | 291 | it('corectly safe transfers NFT from owner to smart contract that can recieve NFTs', async () => { 292 | const sender = accounts[1]; 293 | const tokenReceiverMock = await TokenReceiverMock.new(); 294 | const recipient = tokenReceiverMock.address; 295 | 296 | await nftoken.createGalaxy(id2, sender); 297 | await nftoken.transferPoint(id2, sender, false, {from:sender}); 298 | await seeEvents(nftoken.safeTransferFrom(sender, recipient, id2, {from: 299 | sender}), ['Transfer']); 300 | 301 | const senderBalance = await nftoken.balanceOf(sender); 302 | const recipientBalance = await nftoken.balanceOf(recipient); 303 | const ownerOfId2 = await nftoken.ownerOf(id2); 304 | 305 | assert.equal(senderBalance, 0); 306 | assert.equal(recipientBalance, 1); 307 | assert.equal(ownerOfId2, recipient); 308 | }); 309 | }); 310 | -------------------------------------------------------------------------------- /contracts/Polls.sol: -------------------------------------------------------------------------------- 1 | // the azimuth polls data store 2 | // https://azimuth.network 3 | 4 | pragma solidity 0.4.24; 5 | 6 | import './SafeMath8.sol'; 7 | import './SafeMath16.sol'; 8 | import 'openzeppelin-solidity/contracts/math/SafeMath.sol'; 9 | import 'openzeppelin-solidity/contracts/ownership/Ownable.sol'; 10 | 11 | // Polls: proposals & votes data contract 12 | // 13 | // This contract is used for storing all data related to the proposals 14 | // of the senate (galaxy owners) and their votes on those proposals. 15 | // It keeps track of votes and uses them to calculate whether a majority 16 | // is in favor of a proposal. 17 | // 18 | // Every galaxy can only vote on a proposal exactly once. Votes cannot 19 | // be changed. If a proposal fails to achieve majority within its 20 | // duration, it can be restarted after its cooldown period has passed. 21 | // 22 | // The requirements for a proposal to achieve majority are as follows: 23 | // - At least 1/4 of the currently active voters (rounded down) must have 24 | // voted in favor of the proposal, 25 | // - More than half of the votes cast must be in favor of the proposal, 26 | // and this can no longer change, either because 27 | // - the poll duration has passed, or 28 | // - not enough voters remain to take away the in-favor majority. 29 | // As soon as these conditions are met, no further interaction with 30 | // the proposal is possible. Achieving majority is permanent. 31 | // 32 | // Since data stores are difficult to upgrade, all of the logic unrelated 33 | // to the voting itself (that is, determining who is eligible to vote) 34 | // is expected to be implemented by this contract's owner. 35 | // 36 | // This contract will be owned by the Ecliptic contract. 37 | // 38 | contract Polls is Ownable 39 | { 40 | using SafeMath for uint256; 41 | using SafeMath16 for uint16; 42 | using SafeMath8 for uint8; 43 | 44 | // UpgradePollStarted: a poll on :proposal has opened 45 | // 46 | event UpgradePollStarted(address proposal); 47 | 48 | // DocumentPollStarted: a poll on :proposal has opened 49 | // 50 | event DocumentPollStarted(bytes32 proposal); 51 | 52 | // UpgradeMajority: :proposal has achieved majority 53 | // 54 | event UpgradeMajority(address proposal); 55 | 56 | // DocumentMajority: :proposal has achieved majority 57 | // 58 | event DocumentMajority(bytes32 proposal); 59 | 60 | // Poll: full poll state 61 | // 62 | struct Poll 63 | { 64 | // start: the timestamp at which the poll was started 65 | // 66 | uint256 start; 67 | 68 | // voted: per galaxy, whether they have voted on this poll 69 | // 70 | bool[256] voted; 71 | 72 | // yesVotes: amount of votes in favor of the proposal 73 | // 74 | uint16 yesVotes; 75 | 76 | // noVotes: amount of votes against the proposal 77 | // 78 | uint16 noVotes; 79 | 80 | // duration: amount of time during which the poll can be voted on 81 | // 82 | uint256 duration; 83 | 84 | // cooldown: amount of time before the (non-majority) poll can be reopened 85 | // 86 | uint256 cooldown; 87 | } 88 | 89 | // pollDuration: duration set for new polls. see also Poll.duration above 90 | // 91 | uint256 public pollDuration; 92 | 93 | // pollCooldown: cooldown set for new polls. see also Poll.cooldown above 94 | // 95 | uint256 public pollCooldown; 96 | 97 | // totalVoters: amount of active galaxies 98 | // 99 | uint16 public totalVoters; 100 | 101 | // upgradeProposals: list of all upgrades ever proposed 102 | // 103 | // this allows clients to discover the existence of polls. 104 | // from there, they can do liveness checks on the polls themselves. 105 | // 106 | address[] public upgradeProposals; 107 | 108 | // upgradePolls: per address, poll held to determine if that address 109 | // will become the new ecliptic 110 | // 111 | mapping(address => Poll) public upgradePolls; 112 | 113 | // upgradeHasAchievedMajority: per address, whether that address 114 | // has ever achieved majority 115 | // 116 | // If we did not store this, we would have to look at old poll data 117 | // to see whether or not a proposal has ever achieved majority. 118 | // Since the outcome of a poll is calculated based on :totalVoters, 119 | // which may not be consistent across time, we need to store outcomes 120 | // explicitly instead of re-calculating them. This allows us to always 121 | // tell with certainty whether or not a majority was achieved, 122 | // regardless of the current :totalVoters. 123 | // 124 | mapping(address => bool) public upgradeHasAchievedMajority; 125 | 126 | // documentProposals: list of all documents ever proposed 127 | // 128 | // this allows clients to discover the existence of polls. 129 | // from there, they can do liveness checks on the polls themselves. 130 | // 131 | bytes32[] public documentProposals; 132 | 133 | // documentPolls: per hash, poll held to determine if the corresponding 134 | // document is accepted by the galactic senate 135 | // 136 | mapping(bytes32 => Poll) public documentPolls; 137 | 138 | // documentHasAchievedMajority: per hash, whether that hash has ever 139 | // achieved majority 140 | // 141 | // the note for upgradeHasAchievedMajority above applies here as well 142 | // 143 | mapping(bytes32 => bool) public documentHasAchievedMajority; 144 | 145 | // documentMajorities: all hashes that have achieved majority 146 | // 147 | bytes32[] public documentMajorities; 148 | 149 | // constructor(): initial contract configuration 150 | // 151 | constructor(uint256 _pollDuration, uint256 _pollCooldown) 152 | public 153 | { 154 | reconfigure(_pollDuration, _pollCooldown); 155 | } 156 | 157 | // reconfigure(): change poll duration and cooldown 158 | // 159 | function reconfigure(uint256 _pollDuration, uint256 _pollCooldown) 160 | public 161 | onlyOwner 162 | { 163 | require( (5 days <= _pollDuration) && (_pollDuration <= 90 days) && 164 | (5 days <= _pollCooldown) && (_pollCooldown <= 90 days) ); 165 | pollDuration = _pollDuration; 166 | pollCooldown = _pollCooldown; 167 | } 168 | 169 | // incrementTotalVoters(): increase the amount of registered voters 170 | // 171 | function incrementTotalVoters() 172 | external 173 | onlyOwner 174 | { 175 | require(totalVoters < 256); 176 | totalVoters = totalVoters.add(1); 177 | } 178 | 179 | // getAllUpgradeProposals(): return array of all upgrade proposals ever made 180 | // 181 | // Note: only useful for clients, as Solidity does not currently 182 | // support returning dynamic arrays. 183 | // 184 | function getUpgradeProposals() 185 | external 186 | view 187 | returns (address[] proposals) 188 | { 189 | return upgradeProposals; 190 | } 191 | 192 | // getUpgradeProposalCount(): get the number of unique proposed upgrades 193 | // 194 | function getUpgradeProposalCount() 195 | external 196 | view 197 | returns (uint256 count) 198 | { 199 | return upgradeProposals.length; 200 | } 201 | 202 | // getAllDocumentProposals(): return array of all upgrade proposals ever made 203 | // 204 | // Note: only useful for clients, as Solidity does not currently 205 | // support returning dynamic arrays. 206 | // 207 | function getDocumentProposals() 208 | external 209 | view 210 | returns (bytes32[] proposals) 211 | { 212 | return documentProposals; 213 | } 214 | 215 | // getDocumentProposalCount(): get the number of unique proposed upgrades 216 | // 217 | function getDocumentProposalCount() 218 | external 219 | view 220 | returns (uint256 count) 221 | { 222 | return documentProposals.length; 223 | } 224 | 225 | // getDocumentMajorities(): return array of all document majorities 226 | // 227 | // Note: only useful for clients, as Solidity does not currently 228 | // support returning dynamic arrays. 229 | // 230 | function getDocumentMajorities() 231 | external 232 | view 233 | returns (bytes32[] majorities) 234 | { 235 | return documentMajorities; 236 | } 237 | 238 | // hasVotedOnUpgradePoll(): returns true if _galaxy has voted 239 | // on the _proposal 240 | // 241 | function hasVotedOnUpgradePoll(uint8 _galaxy, address _proposal) 242 | external 243 | view 244 | returns (bool result) 245 | { 246 | return upgradePolls[_proposal].voted[_galaxy]; 247 | } 248 | 249 | // hasVotedOnDocumentPoll(): returns true if _galaxy has voted 250 | // on the _proposal 251 | // 252 | function hasVotedOnDocumentPoll(uint8 _galaxy, bytes32 _proposal) 253 | external 254 | view 255 | returns (bool result) 256 | { 257 | return documentPolls[_proposal].voted[_galaxy]; 258 | } 259 | 260 | // startUpgradePoll(): open a poll on making _proposal the new ecliptic 261 | // 262 | function startUpgradePoll(address _proposal) 263 | external 264 | onlyOwner 265 | { 266 | // _proposal must not have achieved majority before 267 | // 268 | require(!upgradeHasAchievedMajority[_proposal]); 269 | 270 | Poll storage poll = upgradePolls[_proposal]; 271 | 272 | // if the proposal is being made for the first time, register it. 273 | // 274 | if (0 == poll.start) 275 | { 276 | upgradeProposals.push(_proposal); 277 | } 278 | 279 | startPoll(poll); 280 | emit UpgradePollStarted(_proposal); 281 | } 282 | 283 | // startDocumentPoll(): open a poll on accepting the document 284 | // whose hash is _proposal 285 | // 286 | function startDocumentPoll(bytes32 _proposal) 287 | external 288 | onlyOwner 289 | { 290 | // _proposal must not have achieved majority before 291 | // 292 | require(!documentHasAchievedMajority[_proposal]); 293 | 294 | Poll storage poll = documentPolls[_proposal]; 295 | 296 | // if the proposal is being made for the first time, register it. 297 | // 298 | if (0 == poll.start) 299 | { 300 | documentProposals.push(_proposal); 301 | } 302 | 303 | startPoll(poll); 304 | emit DocumentPollStarted(_proposal); 305 | } 306 | 307 | // startPoll(): open a new poll, or re-open an old one 308 | // 309 | function startPoll(Poll storage _poll) 310 | internal 311 | { 312 | // check that the poll has cooled down enough to be started again 313 | // 314 | // for completely new polls, the values used will be zero 315 | // 316 | require( block.timestamp > ( _poll.start.add( 317 | _poll.duration.add( 318 | _poll.cooldown )) ) ); 319 | 320 | // set started poll state 321 | // 322 | _poll.start = block.timestamp; 323 | delete _poll.voted; 324 | _poll.yesVotes = 0; 325 | _poll.noVotes = 0; 326 | _poll.duration = pollDuration; 327 | _poll.cooldown = pollCooldown; 328 | } 329 | 330 | // castUpgradeVote(): as galaxy _as, cast a vote on the _proposal 331 | // 332 | // _vote is true when in favor of the proposal, false otherwise 333 | // 334 | function castUpgradeVote(uint8 _as, address _proposal, bool _vote) 335 | external 336 | onlyOwner 337 | returns (bool majority) 338 | { 339 | Poll storage poll = upgradePolls[_proposal]; 340 | processVote(poll, _as, _vote); 341 | return updateUpgradePoll(_proposal); 342 | } 343 | 344 | // castDocumentVote(): as galaxy _as, cast a vote on the _proposal 345 | // 346 | // _vote is true when in favor of the proposal, false otherwise 347 | // 348 | function castDocumentVote(uint8 _as, bytes32 _proposal, bool _vote) 349 | external 350 | onlyOwner 351 | returns (bool majority) 352 | { 353 | Poll storage poll = documentPolls[_proposal]; 354 | processVote(poll, _as, _vote); 355 | return updateDocumentPoll(_proposal); 356 | } 357 | 358 | // processVote(): record a vote from _as on the _poll 359 | // 360 | function processVote(Poll storage _poll, uint8 _as, bool _vote) 361 | internal 362 | { 363 | // assist symbolic execution tools 364 | // 365 | assert(block.timestamp >= _poll.start); 366 | 367 | require( // may only vote once 368 | // 369 | !_poll.voted[_as] && 370 | // 371 | // may only vote when the poll is open 372 | // 373 | (block.timestamp < _poll.start.add(_poll.duration)) ); 374 | 375 | // update poll state to account for the new vote 376 | // 377 | _poll.voted[_as] = true; 378 | if (_vote) 379 | { 380 | _poll.yesVotes = _poll.yesVotes.add(1); 381 | } 382 | else 383 | { 384 | _poll.noVotes = _poll.noVotes.add(1); 385 | } 386 | } 387 | 388 | // updateUpgradePoll(): check whether the _proposal has achieved 389 | // majority, updating state, sending an event, 390 | // and returning true if it has 391 | // 392 | function updateUpgradePoll(address _proposal) 393 | public 394 | onlyOwner 395 | returns (bool majority) 396 | { 397 | // _proposal must not have achieved majority before 398 | // 399 | require(!upgradeHasAchievedMajority[_proposal]); 400 | 401 | // check for majority in the poll 402 | // 403 | Poll storage poll = upgradePolls[_proposal]; 404 | majority = checkPollMajority(poll); 405 | 406 | // if majority was achieved, update the state and send an event 407 | // 408 | if (majority) 409 | { 410 | upgradeHasAchievedMajority[_proposal] = true; 411 | emit UpgradeMajority(_proposal); 412 | } 413 | return majority; 414 | } 415 | 416 | // updateDocumentPoll(): check whether the _proposal has achieved majority, 417 | // updating the state and sending an event if it has 418 | // 419 | // this can be called by anyone, because the ecliptic does not 420 | // need to be aware of the result 421 | // 422 | function updateDocumentPoll(bytes32 _proposal) 423 | public 424 | returns (bool majority) 425 | { 426 | // _proposal must not have achieved majority before 427 | // 428 | require(!documentHasAchievedMajority[_proposal]); 429 | 430 | // check for majority in the poll 431 | // 432 | Poll storage poll = documentPolls[_proposal]; 433 | majority = checkPollMajority(poll); 434 | 435 | // if majority was achieved, update state and send an event 436 | // 437 | if (majority) 438 | { 439 | documentHasAchievedMajority[_proposal] = true; 440 | documentMajorities.push(_proposal); 441 | emit DocumentMajority(_proposal); 442 | } 443 | return majority; 444 | } 445 | 446 | // checkPollMajority(): returns true if the majority is in favor of 447 | // the subject of the poll 448 | // 449 | function checkPollMajority(Poll _poll) 450 | internal 451 | view 452 | returns (bool majority) 453 | { 454 | return ( // poll must have at least the minimum required yes-votes 455 | // 456 | (_poll.yesVotes >= (totalVoters / 4)) && 457 | // 458 | // and have a majority... 459 | // 460 | (_poll.yesVotes > _poll.noVotes) && 461 | // 462 | // ...that is indisputable 463 | // 464 | ( // either because the poll has ended 465 | // 466 | (block.timestamp > _poll.start.add(_poll.duration)) || 467 | // 468 | // or there are more yes votes than there can be no votes 469 | // 470 | ( _poll.yesVotes > totalVoters.sub(_poll.yesVotes) ) ) ); 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /test/TestAzimuth.js: -------------------------------------------------------------------------------- 1 | const Azimuth = artifacts.require('Azimuth'); 2 | 3 | const assertRevert = require('./helpers/assertRevert'); 4 | const seeEvents = require('./helpers/seeEvents'); 5 | 6 | const web3abi = require('web3-eth-abi'); 7 | const web3 = Azimuth.web3; 8 | 9 | contract('Azimuth', function([owner, user, user2, user3]) { 10 | let azimuth; 11 | 12 | before('setting up for tests', async function() { 13 | azimuth = await Azimuth.new(); 14 | }); 15 | 16 | it('getting prefix', async function() { 17 | // galaxies 18 | assert.equal(await azimuth.getPrefix(0), 0); 19 | assert.equal(await azimuth.getPrefix(255), 255); 20 | // stars 21 | assert.equal(await azimuth.getPrefix(256), 0); 22 | assert.equal(await azimuth.getPrefix(65535), 255); 23 | // planets 24 | assert.equal(await azimuth.getPrefix(1245952), 768); 25 | }); 26 | 27 | it('getting size', async function() { 28 | // galaxies 29 | assert.equal(await azimuth.getPointSize(0), 0); 30 | assert.equal(await azimuth.getPointSize(255), 0); 31 | // stars 32 | assert.equal(await azimuth.getPointSize(256), 1); 33 | assert.equal(await azimuth.getPointSize(65535), 1); 34 | // planets 35 | assert.equal(await azimuth.getPointSize(1245952), 2); 36 | }); 37 | 38 | it('setting dns domain', async function() { 39 | // only owner can do this. 40 | await assertRevert(azimuth.setDnsDomains("new1", "new2", "new3", {from:user})); 41 | await azimuth.setDnsDomains("new1", "new2", "new3"); 42 | assert.equal(await azimuth.dnsDomains(0), "new1"); 43 | assert.equal(await azimuth.dnsDomains(1), "new2"); 44 | assert.equal(await azimuth.dnsDomains(2), "new3"); 45 | }); 46 | 47 | it('getting and setting the point owner', async function() { 48 | assert.equal(await azimuth.getOwner(0), 0); 49 | // only owner can do this. 50 | await assertRevert(azimuth.setOwner(0, user, {from:user})); 51 | await seeEvents(azimuth.setOwner(0, user), ['OwnerChanged']); 52 | assert.isTrue(await azimuth.isOwner(0, user), true); 53 | assert.isFalse(await azimuth.isOwner(0, owner), false); 54 | // setting to the same owner is a no-op, shouldn't emit event 55 | await seeEvents(azimuth.setOwner(0, user), []); 56 | }); 57 | 58 | it('getting owned points', async function() { 59 | await azimuth.setOwner(1, user); 60 | await azimuth.setOwner(2, user); 61 | let owned = await azimuth.getOwnedPoints(user); 62 | assert.equal(owned[0], 0); 63 | assert.equal(owned[1], 1); 64 | assert.equal(owned[2], 2); 65 | assert.equal(owned.length, 3); 66 | assert.equal(await azimuth.getOwnedPointAtIndex(user, 2), 2); 67 | await assertRevert(azimuth.getOwnedPointAtIndex(user, 3)); 68 | await azimuth.setOwner(0, owner); 69 | owned = await azimuth.getOwnedPoints(user); 70 | assert.equal(owned[0].toNumber(), 2); 71 | assert.equal(owned[1].toNumber(), 1); 72 | assert.equal(owned.length, 2); 73 | // interact with points that got moved in the array 74 | await azimuth.setOwner(2, owner); 75 | owned = await azimuth.getOwnedPoints(user); 76 | assert.equal(owned[0].toNumber(), 1); 77 | assert.equal(owned.length, 1); 78 | }); 79 | 80 | it('activating', async function() { 81 | assert.isFalse(await azimuth.isActive(0)); 82 | assert.isFalse(await azimuth.isActive(257)); 83 | // only owner can do this. 84 | await assertRevert(azimuth.activatePoint(0, {from:user})); 85 | await azimuth.activatePoint(0); 86 | await azimuth.activatePoint(257); 87 | assert.isTrue(await azimuth.isActive(0)); 88 | assert.isTrue(await azimuth.isActive(257)); 89 | assert.equal(await azimuth.getSponsor(257), 1); 90 | assert.isTrue(await azimuth.hasSponsor(257)); 91 | assert.isTrue(await azimuth.isSponsor(257, 1)); 92 | // can't do it twice. 93 | await assertRevert(azimuth.activatePoint(0)); 94 | await azimuth.activatePoint(513); 95 | await azimuth.activatePoint(769); 96 | }); 97 | 98 | it('spawning and spawn count', async function() { 99 | assert.equal(await azimuth.getSpawnCount(1), 0); 100 | // only owner can do this. 101 | await assertRevert(azimuth.registerSpawned(0, {from:user})); 102 | await azimuth.registerSpawned(257); 103 | assert.equal(await azimuth.getSpawnCount(1), 1); 104 | let spawned = await azimuth.getSpawned(1); 105 | assert.equal(spawned.length, 1); 106 | assert.equal(spawned[0], 257); 107 | // registering galaxy spawns is a no-op 108 | await azimuth.registerSpawned(1); 109 | assert.equal(await azimuth.getSpawnCount(1), 1); 110 | spawned = await azimuth.getSpawned(1); 111 | assert.equal(spawned.length, 1); 112 | }); 113 | 114 | it('losing sponsor, setting, canceling, and doing escape', async function() { 115 | // reverse lookup is being kept correctly 116 | assert.equal(await azimuth.getSponsoringCount(1), 3); 117 | assert.equal(await azimuth.sponsoringIndexes(1, 257), 1); 118 | let spo = await azimuth.getSponsoring(1); 119 | assert.equal(spo[0], 257); 120 | assert.equal(spo[1], 513); 121 | assert.equal(spo[2], 769); 122 | // only owner can do this. 123 | await assertRevert(azimuth.loseSponsor(257, {from:user})); 124 | await seeEvents(azimuth.loseSponsor(257), ['LostSponsor']); 125 | assert.isFalse(await azimuth.hasSponsor(257)); 126 | assert.isFalse(await azimuth.isSponsor(257, 1)); 127 | assert.equal(await azimuth.getSponsor(257), 1); 128 | // won't emit events for subsequent calls. 129 | await seeEvents(azimuth.loseSponsor(257), []); 130 | assert.isFalse(await azimuth.isEscaping(257)); 131 | // reverse lookup is being kept correctly 132 | assert.equal(await azimuth.getSponsoringCount(1), 2); 133 | assert.equal(await azimuth.sponsoringIndexes(1, 257), 0); 134 | spo = await azimuth.getSponsoring(1); 135 | assert.equal(spo[0], 769); 136 | assert.equal(spo[1], 513); 137 | // can still interact with points that got shuffled around in array 138 | await azimuth.loseSponsor(769); 139 | // 140 | // only owner can do this. 141 | await assertRevert(azimuth.setEscapeRequest(257, 2, {from:user})); 142 | // only owner can do this. 143 | await assertRevert(azimuth.cancelEscape(257, {from:user})); 144 | await seeEvents(azimuth.setEscapeRequest(257, 2), ['EscapeRequested']); 145 | assert.isTrue(await azimuth.isRequestingEscapeTo(257, 2)); 146 | assert.isTrue(await azimuth.isEscaping(257)); 147 | assert.equal(await azimuth.getEscapeRequest(257), 2); 148 | // setting to the same request is a no-op, shouldn't emit event 149 | await seeEvents(azimuth.setEscapeRequest(257, 2), []); 150 | // reverse lookup is being kept correctly 151 | await azimuth.setEscapeRequest(513, 2); 152 | await azimuth.setEscapeRequest(769, 2); 153 | assert.equal(await azimuth.getEscapeRequestsCount(2), 3); 154 | assert.equal(await azimuth.escapeRequestsIndexes(2, 257), 1); 155 | let esr = await azimuth.getEscapeRequests(2); 156 | assert.equal(esr[0], 257); 157 | assert.equal(esr[1], 513); 158 | assert.equal(esr[2], 769); 159 | // cancelling the escape 160 | await seeEvents(azimuth.cancelEscape(257), ['EscapeCanceled']); 161 | assert.isFalse(await azimuth.isRequestingEscapeTo(257, 2)); 162 | assert.isFalse(await azimuth.isEscaping(257)); 163 | // cancelling a non-escaping point is a no-op, shouldn't emit event 164 | await seeEvents(azimuth.cancelEscape(257), []); 165 | // reverse lookup is being kept correctly 166 | assert.equal(await azimuth.getEscapeRequestsCount(2), 2); 167 | assert.equal(await azimuth.escapeRequestsIndexes(2, 257), 0); 168 | esr = await azimuth.getEscapeRequests(2); 169 | assert.equal(esr[0], 769); 170 | assert.equal(esr[1], 513); 171 | // can still interact with points that got shuffled around in array 172 | await azimuth.cancelEscape(769); 173 | // 174 | // only owner can do this. 175 | await assertRevert(azimuth.doEscape(257, {from:user})); 176 | // can't do if not escaping. 177 | await assertRevert(azimuth.doEscape(257)); 178 | await azimuth.setEscapeRequest(257, 2); 179 | await seeEvents(azimuth.doEscape(257), ['EscapeAccepted']); 180 | assert.isFalse(await azimuth.isRequestingEscapeTo(257, 2)); 181 | assert.equal(await azimuth.getSponsor(257), 2); 182 | }); 183 | 184 | it('setting keys', async function() { 185 | let { crypt, auth, suite, revision } = await azimuth.getKeys(0); 186 | assert.equal(crypt, 187 | '0x0000000000000000000000000000000000000000000000000000000000000000'); 188 | assert.equal(auth, 189 | '0x0000000000000000000000000000000000000000000000000000000000000000'); 190 | assert.equal(suite, 0); 191 | assert.equal(revision, 0); 192 | assert.equal(await azimuth.getKeyRevisionNumber(0), 0); 193 | assert.isFalse(await azimuth.isLive(0)); 194 | // only owner can do this. 195 | await assertRevert(azimuth.setKeys(web3.utils.toHex(0), 196 | web3.utils.toHex(10), 197 | web3.utils.toHex(11), 198 | web3.utils.toHex(2), 199 | {from:user})); 200 | await seeEvents(azimuth.setKeys(web3.utils.toHex(0), 201 | web3.utils.toHex(10), 202 | web3.utils.toHex(11), 203 | web3.utils.toHex(2)), ['ChangedKeys']); 204 | await seeEvents(azimuth.setKeys(web3.utils.toHex(0), 205 | web3.utils.toHex(10), 206 | web3.utils.toHex(11), 207 | web3.utils.toHex(2)), []); 208 | let ks = await azimuth.getKeys(web3.utils.toHex(0)); 209 | crypt = ks.crypt; 210 | auth = ks.auth; 211 | suite = ks.suite; 212 | revision = ks.revision; 213 | assert.equal(crypt, 214 | '0x0a00000000000000000000000000000000000000000000000000000000000000'); 215 | assert.equal(auth, 216 | '0x0b00000000000000000000000000000000000000000000000000000000000000'); 217 | assert.equal(suite, 2); 218 | assert.equal(revision, 1); 219 | assert.equal(await azimuth.getKeyRevisionNumber(0), 1); 220 | assert.equal(await azimuth.getContinuityNumber(0), 0); 221 | assert.isTrue(await azimuth.isLive(0)); 222 | await azimuth.setKeys(web3.utils.toHex(0), 223 | web3.utils.toHex(1), 224 | web3.utils.toHex(0), 225 | web3.utils.toHex(1)); 226 | assert.isFalse(await azimuth.isLive(0)); 227 | // only owner can do this 228 | await assertRevert(azimuth.incrementContinuityNumber(0, {from:user})); 229 | await azimuth.incrementContinuityNumber(0); 230 | assert.equal(await azimuth.getContinuityNumber(0), 1); 231 | }); 232 | 233 | it('setting management proxy', async function() { 234 | assert.isFalse(await azimuth.isManagementProxy(0, owner)); 235 | assert.equal(await azimuth.getManagerForCount(owner), 0); 236 | // only owner can do this. 237 | await assertRevert(azimuth.setManagementProxy(0, owner, {from:user})); 238 | await seeEvents(azimuth.setManagementProxy(0, owner), ['ChangedManagementProxy']); 239 | // won't emit event when nothing changes 240 | await seeEvents(azimuth.setManagementProxy(0, owner), []); 241 | await azimuth.setManagementProxy(1, owner); 242 | await azimuth.setManagementProxy(2, owner); 243 | assert.equal(await azimuth.getManagementProxy(0), owner); 244 | assert.isTrue(await azimuth.isManagementProxy(0, owner)); 245 | assert.equal(await azimuth.getManagerForCount(owner), 3); 246 | assert.equal(await azimuth.managerForIndexes(owner, 0), 1); 247 | let stt = await azimuth.getManagerFor(owner); 248 | assert.equal(stt[0], 0); 249 | assert.equal(stt[1], 1); 250 | assert.equal(stt[2], 2); 251 | await azimuth.setManagementProxy(0, '0x0000000000000000000000000000000000000000'); 252 | assert.isFalse(await azimuth.isManagementProxy(0, owner)); 253 | assert.equal(await azimuth.getManagerForCount(owner), 2); 254 | assert.equal(await azimuth.managerForIndexes(owner, 0), 0); 255 | stt = await azimuth.getManagerFor(owner); 256 | assert.equal(stt[0], 2); 257 | assert.equal(stt[1], 1); 258 | // can still interact with points that got shuffled around in array 259 | await azimuth.setManagementProxy(2, '0x0000000000000000000000000000000000000000'); 260 | }); 261 | 262 | it('setting voting proxy', async function() { 263 | assert.isFalse(await azimuth.isVotingProxy(0, owner)); 264 | assert.equal(await azimuth.getVotingForCount(owner), 0); 265 | // only owner can do this. 266 | await assertRevert(azimuth.setVotingProxy(0, owner, {from:user})); 267 | await seeEvents(azimuth.setVotingProxy(0, owner), ['ChangedVotingProxy']); 268 | // won't emit event when nothing changes 269 | await seeEvents(azimuth.setVotingProxy(0, owner), []); 270 | await azimuth.setVotingProxy(1, owner); 271 | await azimuth.setVotingProxy(2, owner); 272 | assert.equal(await azimuth.getVotingProxy(0), owner); 273 | assert.isTrue(await azimuth.isVotingProxy(0, owner)); 274 | assert.equal(await azimuth.getVotingForCount(owner), 3); 275 | assert.equal(await azimuth.votingForIndexes(owner, 0), 1); 276 | let stt = await azimuth.getVotingFor(owner); 277 | assert.equal(stt[0], 0); 278 | assert.equal(stt[1], 1); 279 | assert.equal(stt[2], 2); 280 | await azimuth.setVotingProxy(0, '0x0000000000000000000000000000000000000000'); 281 | assert.isFalse(await azimuth.isVotingProxy(0, owner)); 282 | assert.equal(await azimuth.getVotingForCount(owner), 2); 283 | assert.equal(await azimuth.votingForIndexes(owner, 0), 0); 284 | stt = await azimuth.getVotingFor(owner); 285 | assert.equal(stt[0], 2); 286 | assert.equal(stt[1], 1); 287 | // can still interact with points that got shuffled around in array 288 | await azimuth.setVotingProxy(2, '0x0000000000000000000000000000000000000000'); 289 | }); 290 | 291 | it('setting spawn proxy', async function() { 292 | assert.isFalse(await azimuth.isSpawnProxy(0, owner)); 293 | assert.equal(await azimuth.getSpawningForCount(owner), 0); 294 | // only owner can do this. 295 | await assertRevert(azimuth.setSpawnProxy(0, owner, {from:user})); 296 | await seeEvents(azimuth.setSpawnProxy(0, owner), ['ChangedSpawnProxy']); 297 | // won't emit event when nothing changes 298 | await seeEvents(azimuth.setSpawnProxy(0, owner), []); 299 | await azimuth.setSpawnProxy(1, owner); 300 | await azimuth.setSpawnProxy(2, owner); 301 | assert.equal(await azimuth.getSpawnProxy(0), owner); 302 | assert.isTrue(await azimuth.isSpawnProxy(0, owner)); 303 | assert.equal(await azimuth.getSpawningForCount(owner), 3); 304 | assert.equal(await azimuth.spawningForIndexes(owner, 0), 1); 305 | let stt = await azimuth.getSpawningFor(owner); 306 | assert.equal(stt[0], 0); 307 | assert.equal(stt[1], 1); 308 | assert.equal(stt[2], 2); 309 | await azimuth.setSpawnProxy(0, '0x0000000000000000000000000000000000000000'); 310 | assert.isFalse(await azimuth.isSpawnProxy(0, owner)); 311 | assert.equal(await azimuth.getSpawningForCount(owner), 2); 312 | assert.equal(await azimuth.spawningForIndexes(owner, 0), 0); 313 | stt = await azimuth.getSpawningFor(owner); 314 | assert.equal(stt[0], 2); 315 | assert.equal(stt[1], 1); 316 | // can still interact with points that got shuffled around in array 317 | await azimuth.setSpawnProxy(2, '0x0000000000000000000000000000000000000000'); 318 | }); 319 | 320 | it('setting transfer proxy', async function() { 321 | assert.isFalse(await azimuth.isTransferProxy(0, owner)); 322 | assert.equal(await azimuth.getTransferringForCount(owner), 0); 323 | // only owner can do this. 324 | await assertRevert(azimuth.setTransferProxy(0, owner, {from:user})); 325 | await seeEvents(azimuth.setTransferProxy(0, owner), ['ChangedTransferProxy']); 326 | // won't emit event when nothing changes 327 | await seeEvents(azimuth.setTransferProxy(0, owner), []); 328 | await azimuth.setTransferProxy(1, owner); 329 | await azimuth.setTransferProxy(2, owner); 330 | assert.equal(await azimuth.getTransferProxy(0), owner); 331 | assert.isTrue(await azimuth.isTransferProxy(0, owner)); 332 | assert.equal(await azimuth.getTransferringForCount(owner), 3); 333 | assert.equal(await azimuth.transferringForIndexes(owner, 0), 1); 334 | let stt = await azimuth.getTransferringFor(owner); 335 | assert.equal(stt[0], 0); 336 | assert.equal(stt[1], 1); 337 | assert.equal(stt[2], 2); 338 | await azimuth.setTransferProxy(0, '0x0000000000000000000000000000000000000000'); 339 | assert.isFalse(await azimuth.isTransferProxy(0, owner)); 340 | assert.equal(await azimuth.getTransferringForCount(owner), 2); 341 | assert.equal(await azimuth.transferringForIndexes(owner, 0), 0); 342 | stt = await azimuth.getTransferringFor(owner); 343 | assert.equal(stt[0], 2); 344 | assert.equal(stt[1], 1); 345 | // can still interact with points that got shuffled around in array 346 | await azimuth.setTransferProxy(2, '0x0000000000000000000000000000000000000000'); 347 | }); 348 | }); 349 | -------------------------------------------------------------------------------- /rollup/tiny.hoon: -------------------------------------------------------------------------------- 1 | !. 2 | => %a50 3 | ~% %a.50 ~ ~ 4 | |% 5 | :: Types 6 | :: 7 | +$ ship @p 8 | +$ life @ud 9 | +$ rift @ud 10 | +$ pass @ 11 | +$ bloq @ 12 | +$ step _`@u`1 13 | +$ bite $@(bloq [=bloq =step]) 14 | +$ octs [p=@ud q=@] 15 | +$ mold $~(* $-(* *)) 16 | ++ unit |$ [item] $@(~ [~ u=item]) 17 | ++ list |$ [item] $@(~ [i=item t=(list item)]) 18 | ++ lest |$ [item] [i=item t=(list item)] 19 | ++ tree |$ [node] $@(~ [n=node l=(tree node) r=(tree node)]) 20 | ++ pair |$ [head tail] [p=head q=tail] 21 | ++ map 22 | |$ [key value] 23 | $| (tree (pair key value)) 24 | |=(a=(tree (pair)) ?:(=(~ a) & ~(apt by a))) 25 | :: 26 | ++ set 27 | |$ [item] 28 | $| (tree item) 29 | |=(a=(tree) ?:(=(~ a) & ~(apt in a))) 30 | :: 31 | ++ jug |$ [key value] (map key (set value)) 32 | :: 33 | :: Bits 34 | :: 35 | ++ dec :: decrement 36 | ~/ %dec 37 | |= a=@ 38 | ~_ leaf+"decrement-underflow" 39 | ?< =(0 a) 40 | =+ b=0 41 | |- ^- @ 42 | ?: =(a +(b)) b 43 | $(b +(b)) 44 | :: 45 | ++ add :: plus 46 | ~/ %add 47 | |= [a=@ b=@] 48 | ^- @ 49 | ?: =(0 a) b 50 | $(a (dec a), b +(b)) 51 | :: 52 | ++ sub :: subtract 53 | ~/ %sub 54 | |= [a=@ b=@] 55 | ~_ leaf+"subtract-underflow" 56 | :: difference 57 | ^- @ 58 | ?: =(0 b) a 59 | $(a (dec a), b (dec b)) 60 | :: 61 | ++ mul :: multiply 62 | ~/ %mul 63 | |: [a=`@`1 b=`@`1] 64 | ^- @ 65 | =+ c=0 66 | |- 67 | ?: =(0 a) c 68 | $(a (dec a), c (add b c)) 69 | :: 70 | ++ div :: divide 71 | ~/ %div 72 | |: [a=`@`1 b=`@`1] 73 | ^- @ 74 | ~_ leaf+"divide-by-zero" 75 | ?< =(0 b) 76 | =+ c=0 77 | |- 78 | ?: (lth a b) c 79 | $(a (sub a b), c +(c)) 80 | :: 81 | ++ dvr :: divide w/remainder 82 | ~/ %dvr 83 | |: [a=`@`1 b=`@`1] 84 | ^- [p=@ q=@] 85 | [(div a b) (mod a b)] 86 | :: 87 | ++ mod :: modulus 88 | ~/ %mod 89 | |: [a=`@`1 b=`@`1] 90 | ^- @ 91 | ?< =(0 b) 92 | (sub a (mul b (div a b))) 93 | :: 94 | ++ bex :: binary exponent 95 | ~/ %bex 96 | |= a=bloq 97 | ^- @ 98 | ?: =(0 a) 1 99 | (mul 2 $(a (dec a))) 100 | :: 101 | ++ lsh :: left-shift 102 | ~/ %lsh 103 | |= [a=bite b=@] 104 | =/ [=bloq =step] ?^(a a [a *step]) 105 | (mul b (bex (mul (bex bloq) step))) 106 | :: 107 | ++ rsh :: right-shift 108 | ~/ %rsh 109 | |= [a=bite b=@] 110 | =/ [=bloq =step] ?^(a a [a *step]) 111 | (div b (bex (mul (bex bloq) step))) 112 | :: 113 | ++ con :: binary or 114 | ~/ %con 115 | |= [a=@ b=@] 116 | =+ [c=0 d=0] 117 | |- ^- @ 118 | ?: ?&(=(0 a) =(0 b)) d 119 | %= $ 120 | a (rsh 0 a) 121 | b (rsh 0 b) 122 | c +(c) 123 | d %+ add d 124 | %+ lsh [0 c] 125 | ?& =(0 (end 0 a)) 126 | =(0 (end 0 b)) 127 | == 128 | == 129 | :: 130 | ++ dis :: binary and 131 | ~/ %dis 132 | |= [a=@ b=@] 133 | =| [c=@ d=@] 134 | |- ^- @ 135 | ?: ?|(=(0 a) =(0 b)) d 136 | %= $ 137 | a (rsh 0 a) 138 | b (rsh 0 b) 139 | c +(c) 140 | d %+ add d 141 | %+ lsh [0 c] 142 | ?| =(0 (end 0 a)) 143 | =(0 (end 0 b)) 144 | == 145 | == 146 | :: 147 | ++ mix :: binary xor 148 | ~/ %mix 149 | |= [a=@ b=@] 150 | ^- @ 151 | =+ [c=0 d=0] 152 | |- 153 | ?: ?&(=(0 a) =(0 b)) d 154 | %= $ 155 | a (rsh 0 a) 156 | b (rsh 0 b) 157 | c +(c) 158 | d (add d (lsh [0 c] =((end 0 a) (end 0 b)))) 159 | == 160 | :: 161 | ++ lth :: less 162 | ~/ %lth 163 | |= [a=@ b=@] 164 | ^- ? 165 | ?& !=(a b) 166 | |- 167 | ?| =(0 a) 168 | ?& !=(0 b) 169 | $(a (dec a), b (dec b)) 170 | == == == 171 | :: 172 | ++ lte :: less or equal 173 | ~/ %lte 174 | |= [a=@ b=@] 175 | |(=(a b) (lth a b)) 176 | :: 177 | ++ gte :: greater or equal 178 | ~/ %gte 179 | |= [a=@ b=@] 180 | ^- ? 181 | !(lth a b) 182 | :: 183 | ++ gth :: greater 184 | ~/ %gth 185 | |= [a=@ b=@] 186 | ^- ? 187 | !(lte a b) 188 | :: 189 | ++ swp :: naive rev bloq order 190 | ~/ %swp 191 | |= [a=bloq b=@] 192 | (rep a (flop (rip a b))) 193 | :: 194 | ++ met :: measure 195 | ~/ %met 196 | |= [a=bloq b=@] 197 | ^- @ 198 | =+ c=0 199 | |- 200 | ?: =(0 b) c 201 | $(b (rsh a b), c +(c)) 202 | :: 203 | ++ end :: tail 204 | ~/ %end 205 | |= [a=bite b=@] 206 | =/ [=bloq =step] ?^(a a [a *step]) 207 | (mod b (bex (mul (bex bloq) step))) 208 | :: 209 | ++ cat :: concatenate 210 | ~/ %cat 211 | |= [a=bloq b=@ c=@] 212 | (add (lsh [a (met a b)] c) b) 213 | :: 214 | ++ cut :: slice 215 | ~/ %cut 216 | |= [a=bloq [b=step c=step] d=@] 217 | (end [a c] (rsh [a b] d)) 218 | :: 219 | ++ can :: assemble 220 | ~/ %can 221 | |= [a=bloq b=(list [p=step q=@])] 222 | ^- @ 223 | ?~ b 0 224 | (add (end [a p.i.b] q.i.b) (lsh [a p.i.b] $(b t.b))) 225 | :: 226 | ++ cad :: assemble specific 227 | ~/ %cad 228 | |= [a=bloq b=(list [p=step q=@])] 229 | ^- [=step @] 230 | :_ (can a b) 231 | |- 232 | ?~ b 233 | 0 234 | (add p.i.b $(b t.b)) 235 | :: 236 | ++ rep :: assemble fixed 237 | ~/ %rep 238 | |= [a=bite b=(list @)] 239 | =/ [=bloq =step] ?^(a a [a *step]) 240 | =| i=@ud 241 | |- ^- @ 242 | ?~ b 0 243 | %+ add $(i +(i), b t.b) 244 | (lsh [bloq (mul step i)] (end [bloq step] i.b)) 245 | :: 246 | ++ rip :: disassemble 247 | ~/ %rip 248 | |= [a=bite b=@] 249 | ^- (list @) 250 | ?: =(0 b) ~ 251 | [(end a b) $(b (rsh a b))] 252 | :: 253 | :: 254 | :: Lists 255 | :: 256 | ++ lent :: length 257 | ~/ %lent 258 | |= a=(list) 259 | ^- @ 260 | =+ b=0 261 | |- 262 | ?~ a b 263 | $(a t.a, b +(b)) 264 | :: 265 | ++ slag :: suffix 266 | ~/ %slag 267 | |* [a=@ b=(list)] 268 | |- ^+ b 269 | ?: =(0 a) b 270 | ?~ b ~ 271 | $(b t.b, a (dec a)) 272 | :: 273 | ++ snag :: index 274 | ~/ %snag 275 | |* [a=@ b=(list)] 276 | |- ^+ ?>(?=(^ b) i.b) 277 | ?~ b 278 | ~_ leaf+"snag-fail" 279 | !! 280 | ?: =(0 a) i.b 281 | $(b t.b, a (dec a)) 282 | :: 283 | ++ homo :: homogenize 284 | |* a=(list) 285 | ^+ =< $ 286 | |@ ++ $ ?:(*? ~ [i=(snag 0 a) t=$]) 287 | -- 288 | a 289 | :: 290 | ++ flop :: reverse 291 | ~/ %flop 292 | |* a=(list) 293 | => .(a (homo a)) 294 | ^+ a 295 | =+ b=`_a`~ 296 | |- 297 | ?~ a b 298 | $(a t.a, b [i.a b]) 299 | :: 300 | ++ welp :: concatenate 301 | ~/ %welp 302 | =| [* *] 303 | |@ 304 | ++ $ 305 | ?~ +<- 306 | +<-(. +<+) 307 | +<-(+ $(+<- +<->)) 308 | -- 309 | :: 310 | ++ reap :: replicate 311 | ~/ %reap 312 | |* [a=@ b=*] 313 | |- ^- (list _b) 314 | ?~ a ~ 315 | [b $(a (dec a))] 316 | :: 317 | :: Modular arithmetic 318 | :: 319 | ++ fe :: modulo bloq 320 | |_ a=bloq 321 | ++ rol |= [b=bloq c=@ d=@] ^- @ :: roll left 322 | =+ e=(sit d) 323 | =+ f=(bex (sub a b)) 324 | =+ g=(mod c f) 325 | (sit (con (lsh [b g] e) (rsh [b (sub f g)] e))) 326 | ++ sum |=([b=@ c=@] (sit (add b c))) :: wrapping add 327 | ++ sit |=(b=@ (end a b)) :: enforce modulo 328 | -- 329 | :: 330 | :: Hashes 331 | :: 332 | ++ muk :: standard murmur3 333 | ~% %muk ..muk ~ 334 | =+ ~(. fe 5) 335 | |= [syd=@ len=@ key=@] 336 | =. syd (end 5 syd) 337 | =/ pad (sub len (met 3 key)) 338 | =/ data (welp (rip 3 key) (reap pad 0)) 339 | =/ nblocks (div len 4) :: intentionally off-by-one 340 | =/ h1 syd 341 | =+ [c1=0xcc9e.2d51 c2=0x1b87.3593] 342 | =/ blocks (rip 5 key) 343 | =/ i nblocks 344 | =. h1 =/ hi h1 |- 345 | ?: =(0 i) hi 346 | =/ k1 (snag (sub nblocks i) blocks) :: negative array index 347 | =. k1 (sit (mul k1 c1)) 348 | =. k1 (rol 0 15 k1) 349 | =. k1 (sit (mul k1 c2)) 350 | =. hi (mix hi k1) 351 | =. hi (rol 0 13 hi) 352 | =. hi (sum (sit (mul hi 5)) 0xe654.6b64) 353 | $(i (dec i)) 354 | =/ tail (slag (mul 4 nblocks) data) 355 | =/ k1 0 356 | =/ tlen (dis len 3) 357 | =. h1 358 | ?+ tlen h1 :: fallthrough switch 359 | %3 =. k1 (mix k1 (lsh [0 16] (snag 2 tail))) 360 | =. k1 (mix k1 (lsh [0 8] (snag 1 tail))) 361 | =. k1 (mix k1 (snag 0 tail)) 362 | =. k1 (sit (mul k1 c1)) 363 | =. k1 (rol 0 15 k1) 364 | =. k1 (sit (mul k1 c2)) 365 | (mix h1 k1) 366 | %2 =. k1 (mix k1 (lsh [0 8] (snag 1 tail))) 367 | =. k1 (mix k1 (snag 0 tail)) 368 | =. k1 (sit (mul k1 c1)) 369 | =. k1 (rol 0 15 k1) 370 | =. k1 (sit (mul k1 c2)) 371 | (mix h1 k1) 372 | %1 =. k1 (mix k1 (snag 0 tail)) 373 | =. k1 (sit (mul k1 c1)) 374 | =. k1 (rol 0 15 k1) 375 | =. k1 (sit (mul k1 c2)) 376 | (mix h1 k1) 377 | == 378 | =. h1 (mix h1 len) 379 | |^ (fmix32 h1) 380 | ++ fmix32 381 | |= h=@ 382 | =. h (mix h (rsh [0 16] h)) 383 | =. h (sit (mul h 0x85eb.ca6b)) 384 | =. h (mix h (rsh [0 13] h)) 385 | =. h (sit (mul h 0xc2b2.ae35)) 386 | =. h (mix h (rsh [0 16] h)) 387 | h 388 | -- 389 | :: 390 | ++ mug :: mug with murmur3 391 | ~/ %mug 392 | |= a=* 393 | |^ ?@ a (mum 0xcafe.babe 0x7fff a) 394 | =/ b (cat 5 $(a -.a) $(a +.a)) 395 | (mum 0xdead.beef 0xfffe b) 396 | :: 397 | ++ mum 398 | |= [syd=@uxF fal=@F key=@] 399 | =/ wyd (met 3 key) 400 | =| i=@ud 401 | |- ^- @F 402 | ?: =(8 i) fal 403 | =/ haz=@F (muk syd wyd key) 404 | =/ ham=@F (mix (rsh [0 31] haz) (end [0 31] haz)) 405 | ?.(=(0 ham) ham $(i +(i), syd +(syd))) 406 | -- 407 | :: 408 | ++ gor :: mug order 409 | ~/ %gor 410 | |= [a=* b=*] 411 | ^- ? 412 | =+ [c=(mug a) d=(mug b)] 413 | ?: =(c d) 414 | (dor a b) 415 | (lth c d) 416 | :: 417 | ++ mor :: more mug order 418 | ~/ %mor 419 | |= [a=* b=*] 420 | ^- ? 421 | =+ [c=(mug (mug a)) d=(mug (mug b))] 422 | ?: =(c d) 423 | (dor a b) 424 | (lth c d) 425 | :: 426 | ++ dor :: tree order 427 | ~/ %dor 428 | |= [a=* b=*] 429 | ^- ? 430 | ?: =(a b) & 431 | ?. ?=(@ a) 432 | ?: ?=(@ b) | 433 | ?: =(-.a -.b) 434 | $(a +.a, b +.b) 435 | $(a -.a, b -.b) 436 | ?. ?=(@ b) & 437 | (lth a b) 438 | :: 439 | ++ por :: parent order 440 | ~/ %por 441 | |= [a=@p b=@p] 442 | ^- ? 443 | ?: =(a b) & 444 | =| i=@ 445 | |- 446 | ?: =(i 2) 447 | :: second two bytes 448 | (lte a b) 449 | :: first two bytes 450 | =+ [c=(end 3 a) d=(end 3 b)] 451 | ?: =(c d) 452 | $(a (rsh 3 a), b (rsh 3 b), i +(i)) 453 | (lth c d) 454 | :: 455 | :: Maps 456 | :: 457 | ++ by 458 | ~/ %by 459 | =| a=(tree (pair)) :: (map) 460 | =* node ?>(?=(^ a) n.a) 461 | |@ 462 | ++ get 463 | ~/ %get 464 | |* b=* 465 | => .(b `_?>(?=(^ a) p.n.a)`b) 466 | |- ^- (unit _?>(?=(^ a) q.n.a)) 467 | ?~ a 468 | ~ 469 | ?: =(b p.n.a) 470 | `q.n.a 471 | ?: (gor b p.n.a) 472 | $(a l.a) 473 | $(a r.a) 474 | :: 475 | ++ put 476 | ~/ %put 477 | |* [b=* c=*] 478 | |- ^+ a 479 | ?~ a 480 | [[b c] ~ ~] 481 | ?: =(b p.n.a) 482 | ?: =(c q.n.a) 483 | a 484 | a(n [b c]) 485 | ?: (gor b p.n.a) 486 | =+ d=$(a l.a) 487 | ?> ?=(^ d) 488 | ?: (mor p.n.a p.n.d) 489 | a(l d) 490 | d(r a(l r.d)) 491 | =+ d=$(a r.a) 492 | ?> ?=(^ d) 493 | ?: (mor p.n.a p.n.d) 494 | a(r d) 495 | d(l a(r l.d)) 496 | :: 497 | ++ del 498 | ~/ %del 499 | |* b=* 500 | |- ^+ a 501 | ?~ a 502 | ~ 503 | ?. =(b p.n.a) 504 | ?: (gor b p.n.a) 505 | a(l $(a l.a)) 506 | a(r $(a r.a)) 507 | |- ^- [$?(~ _a)] 508 | ?~ l.a r.a 509 | ?~ r.a l.a 510 | ?: (mor p.n.l.a p.n.r.a) 511 | l.a(r $(l.a r.l.a)) 512 | r.a(l $(r.a l.r.a)) 513 | :: 514 | ++ apt 515 | =< $ 516 | ~/ %apt 517 | =| [l=(unit) r=(unit)] 518 | |. ^- ? 519 | ?~ a & 520 | ?& ?~(l & &((gor p.n.a u.l) !=(p.n.a u.l))) 521 | ?~(r & &((gor u.r p.n.a) !=(u.r p.n.a))) 522 | ?~ l.a & 523 | &((mor p.n.a p.n.l.a) !=(p.n.a p.n.l.a) $(a l.a, l `p.n.a)) 524 | ?~ r.a & 525 | &((mor p.n.a p.n.r.a) !=(p.n.a p.n.r.a) $(a r.a, r `p.n.a)) 526 | == 527 | -- 528 | :: 529 | ++ on :: ordered map 530 | ~/ %on 531 | |* [key=mold val=mold] 532 | => |% 533 | +$ item [key=key val=val] 534 | -- 535 | :: 536 | ~% %comp +>+ ~ 537 | |= compare=$-([key key] ?) 538 | ~% %core + ~ 539 | |% 540 | :: 541 | ++ apt 542 | ~/ %apt 543 | |= a=(tree item) 544 | =| [l=(unit key) r=(unit key)] 545 | |- ^- ? 546 | ?~ a %.y 547 | ?& ?~(l %.y (compare key.n.a u.l)) 548 | ?~(r %.y (compare u.r key.n.a)) 549 | ?~(l.a %.y &((mor key.n.a key.n.l.a) $(a l.a, l `key.n.a))) 550 | ?~(r.a %.y &((mor key.n.a key.n.r.a) $(a r.a, r `key.n.a))) 551 | == 552 | :: 553 | ++ get 554 | ~/ %get 555 | |= [a=(tree item) b=key] 556 | ^- (unit val) 557 | ?~ a ~ 558 | ?: =(b key.n.a) 559 | `val.n.a 560 | ?: (compare b key.n.a) 561 | $(a l.a) 562 | $(a r.a) 563 | :: 564 | ++ has 565 | ~/ %has 566 | |= [a=(tree item) b=key] 567 | ^- ? 568 | !=(~ (get a b)) 569 | :: 570 | ++ put 571 | ~/ %put 572 | |= [a=(tree item) =key =val] 573 | ^- (tree item) 574 | ?~ a [n=[key val] l=~ r=~] 575 | ?: =(key.n.a key) a(val.n val) 576 | ?: (compare key key.n.a) 577 | =/ l $(a l.a) 578 | ?> ?=(^ l) 579 | ?: (mor key.n.a key.n.l) 580 | a(l l) 581 | l(r a(l r.l)) 582 | =/ r $(a r.a) 583 | ?> ?=(^ r) 584 | ?: (mor key.n.a key.n.r) 585 | a(r r) 586 | r(l a(r l.r)) 587 | -- 588 | :: 589 | :: Sets 590 | :: 591 | ++ in 592 | ~/ %in 593 | =| a=(tree) :: (set) 594 | |@ 595 | ++ put 596 | ~/ %put 597 | |* b=* 598 | |- ^+ a 599 | ?~ a 600 | [b ~ ~] 601 | ?: =(b n.a) 602 | a 603 | ?: (gor b n.a) 604 | =+ c=$(a l.a) 605 | ?> ?=(^ c) 606 | ?: (mor n.a n.c) 607 | a(l c) 608 | c(r a(l r.c)) 609 | =+ c=$(a r.a) 610 | ?> ?=(^ c) 611 | ?: (mor n.a n.c) 612 | a(r c) 613 | c(l a(r l.c)) 614 | :: 615 | ++ del 616 | ~/ %del 617 | |* b=* 618 | |- ^+ a 619 | ?~ a 620 | ~ 621 | ?. =(b n.a) 622 | ?: (gor b n.a) 623 | a(l $(a l.a)) 624 | a(r $(a r.a)) 625 | |- ^- [$?(~ _a)] 626 | ?~ l.a r.a 627 | ?~ r.a l.a 628 | ?: (mor n.l.a n.r.a) 629 | l.a(r $(l.a r.l.a)) 630 | r.a(l $(r.a l.r.a)) 631 | :: 632 | ++ apt 633 | =< $ 634 | ~/ %apt 635 | =| [l=(unit) r=(unit)] 636 | |. ^- ? 637 | ?~ a & 638 | ?& ?~(l & (gor n.a u.l)) 639 | ?~(r & (gor u.r n.a)) 640 | ?~(l.a & ?&((mor n.a n.l.a) $(a l.a, l `n.a))) 641 | ?~(r.a & ?&((mor n.a n.r.a) $(a r.a, r `n.a))) 642 | == 643 | -- 644 | :: 645 | :: Jugs 646 | :: 647 | ++ ju 648 | =| a=(tree (pair * (tree))) :: (jug) 649 | |@ 650 | ++ get 651 | |* b=* 652 | =+ c=(~(get by a) b) 653 | ?~(c ~ u.c) 654 | :: 655 | ++ del 656 | |* [b=* c=*] 657 | ^+ a 658 | =+ d=(get b) 659 | =+ e=(~(del in d) c) 660 | ?~ e 661 | (~(del by a) b) 662 | (~(put by a) b e) 663 | :: 664 | ++ put 665 | |* [b=* c=*] 666 | ^+ a 667 | =+ d=(get b) 668 | (~(put by a) b (~(put in d) c)) 669 | -- 670 | -- 671 | -------------------------------------------------------------------------------- /test/TestEcliptic.js: -------------------------------------------------------------------------------- 1 | const Azimuth = artifacts.require('Azimuth'); 2 | const Polls = artifacts.require('Polls'); 3 | const Claims = artifacts.require('Claims'); 4 | const Ecliptic = artifacts.require('Ecliptic'); 5 | const ENSRegistry = artifacts.require('ENSRegistry'); 6 | const PublicResolver = artifacts.require('PublicResolver'); 7 | 8 | const assertRevert = require('./helpers/assertRevert'); 9 | const increaseTime = require('./helpers/increaseTime'); 10 | const seeEvents = require('./helpers/seeEvents'); 11 | 12 | const deposit = '0x1111111111111111111111111111111111111111'; 13 | const zero = '0x0000000000000000000000000000000000000000'; 14 | const zero64 = 15 | '0x0000000000000000000000000000000000000000000000000000000000000000'; 16 | 17 | contract('Ecliptic', function([owner, user1, user2]) { 18 | let azimuth, polls, claims, ens, resolver, eclipt, eclipt2, pollTime; 19 | 20 | before('setting up for tests', async function() { 21 | pollTime = 432000; 22 | azimuth = await Azimuth.new(); 23 | polls = await Polls.new(pollTime, pollTime); 24 | claims = await Claims.new(azimuth.address); 25 | eclipt = await Ecliptic.new(zero, 26 | azimuth.address, 27 | polls.address, 28 | claims.address); 29 | await azimuth.transferOwnership(eclipt.address); 30 | await polls.transferOwnership(eclipt.address); 31 | }); 32 | 33 | it('setting dns domains', async function() { 34 | // can only be done by owner 35 | await assertRevert(eclipt.setDnsDomains("1", "2", "3", {from:user1})); 36 | await eclipt.setDnsDomains("1", "2", "3"); 37 | assert.equal(await azimuth.dnsDomains(2), "3"); 38 | }); 39 | 40 | it('creating galaxies', async function() { 41 | // create. 42 | await eclipt.createGalaxy(0, user1); 43 | assert.isFalse(await azimuth.isActive(0)); 44 | assert.isTrue(await azimuth.isOwner(0, owner)); 45 | assert.isTrue(await azimuth.isTransferProxy(0, user1)); 46 | // can't create twice. 47 | await assertRevert(eclipt.createGalaxy(0, owner)); 48 | // non-owner can't create. 49 | await assertRevert(eclipt.createGalaxy(1, user1, {from:user1})); 50 | // prep for next tests. 51 | await eclipt.transferPoint(0, user1, false, {from:user1}); 52 | await eclipt.createGalaxy(1, user1); 53 | await eclipt.transferPoint(1, user1, false, {from:user1}); 54 | await eclipt.createGalaxy(2, owner); 55 | assert.isTrue(await azimuth.isActive(2)); 56 | assert.isTrue(await azimuth.isOwner(2, owner)); 57 | assert.equal(await polls.totalVoters(), 3); 58 | await eclipt.transferPoint(2, user1, false); 59 | }); 60 | 61 | it('spawning points', async function() { 62 | // can't spawn if prefix not live. 63 | await assertRevert(eclipt.spawn(256, user1, {from:user1})); 64 | await eclipt.configureKeys(web3.utils.toHex(0), 65 | web3.utils.toHex(1), 66 | web3.utils.toHex(2), 67 | web3.utils.toHex(1), 68 | false, 69 | {from:user1}); 70 | // can't spawn if not prefix owner. 71 | await assertRevert(eclipt.spawn(256, user1, {from:user2})); 72 | // can only spawn size directly below prefix 73 | await assertRevert(eclipt.spawn(65536, user1), {from:user1}); 74 | // spawn child to self, directly 75 | assert.isFalse(await azimuth.isOwner(256, user1)); 76 | await seeEvents(eclipt.spawn(256, user1, {from:user1}), 77 | ['Transfer']); 78 | assert.equal(await azimuth.getSpawnCount(0), 1); 79 | assert.isTrue(await azimuth.isOwner(256, user1)); 80 | assert.isTrue(await azimuth.isActive(256)); 81 | // can't spawn same point twice. 82 | await assertRevert(eclipt.spawn(256, user1, {from:user1})); 83 | // spawn child to other, via withdraw pattern 84 | await seeEvents(eclipt.spawn(512, user2, {from:user1}), 85 | ['Transfer', 'Approval']); 86 | assert.equal(await azimuth.getSpawnCount(0), 2); 87 | assert.isTrue(await azimuth.isOwner(512, user1)); 88 | assert.isFalse(await azimuth.isActive(512)); 89 | assert.isTrue(await azimuth.isTransferProxy(512, user2)); 90 | await eclipt.transferPoint(512, user2, true, {from:user2}); 91 | assert.isTrue(await azimuth.isOwner(512, user2)); 92 | assert.isTrue(await azimuth.isActive(512)); 93 | await eclipt.transferPoint(512, user1, true, {from:user2}); 94 | // check the spawn limits. 95 | assert.equal(await eclipt.getSpawnLimit(0, 0), 255); 96 | assert.equal(await eclipt.getSpawnLimit(123455, 0), 0); 97 | assert.equal(await eclipt.getSpawnLimit(512, new Date('2019-01-01 UTC').valueOf() / 1000), 1024); 98 | assert.equal(await eclipt.getSpawnLimit(512, new Date('2019-12-31 UTC').valueOf() / 1000), 1024); 99 | assert.equal(await eclipt.getSpawnLimit(512, new Date('2020-01-01 UTC').valueOf() / 1000), 2048); 100 | assert.equal(await eclipt.getSpawnLimit(512, new Date('2024-01-01 UTC').valueOf() / 1000), 32768); 101 | assert.equal(await eclipt.getSpawnLimit(512, new Date('2025-01-01 UTC').valueOf() / 1000), 65535); 102 | assert.equal(await eclipt.getSpawnLimit(512, new Date('2026-01-01 UTC').valueOf() / 1000), 65535); 103 | }); 104 | 105 | it('setting spawn proxy', async function() { 106 | // should not be spawner by default. 107 | assert.isFalse(await azimuth.isSpawnProxy(0, user2)); 108 | // can't do if not owner. 109 | await assertRevert(eclipt.setSpawnProxy(0, user2, {from:user2})); 110 | // set up for working spawn. 111 | await eclipt.setSpawnProxy(0, owner, {from:user1}); 112 | // can do as proxy itself 113 | await eclipt.setSpawnProxy(0, user2, {from:owner}) 114 | assert.isTrue(await azimuth.isSpawnProxy(0, user2)); 115 | // spawn as launcher, then test revoking of rights. 116 | await eclipt.spawn(768, user1, {from:user2}); 117 | await eclipt.setSpawnProxy(0, zero, {from:user1}); 118 | assert.isFalse(await azimuth.isSpawnProxy(0, user2)); 119 | }); 120 | 121 | it('transfering ownership directly', async function() { 122 | assert.equal(await azimuth.getContinuityNumber(0), 0); 123 | // set values that should be cleared on-transfer. 124 | await eclipt.setManagementProxy(0, owner, {from:user1}); 125 | await eclipt.setVotingProxy(0, owner, {from:user1}); 126 | await eclipt.setSpawnProxy(0, owner, {from:user1}); 127 | await eclipt.setTransferProxy(0, owner, {from:user1}); 128 | await claims.addClaim(0, "protocol", "claim", web3.utils.toHex("proof"), {from:user1}); 129 | // can't do if not owner. 130 | await assertRevert(eclipt.transferPoint(0, user2, true, {from:user2})); 131 | // transfer as owner, resetting the point. 132 | await seeEvents(eclipt.transferPoint(0, user2, true, {from:user1}), 133 | ['Transfer']); 134 | assert.isTrue(await azimuth.isOwner(0, user2)); 135 | let { crypt, auth } = await azimuth.getKeys(0); 136 | assert.equal(crypt, zero64); 137 | assert.equal(auth, zero64); 138 | assert.equal(await azimuth.getKeyRevisionNumber(0), 2); 139 | assert.equal(await azimuth.getContinuityNumber(0), 1); 140 | assert.isTrue(await azimuth.isManagementProxy(0, zero)); 141 | assert.isTrue(await azimuth.isVotingProxy(0, zero)); 142 | assert.isTrue(await azimuth.isSpawnProxy(0, zero)); 143 | assert.isTrue(await azimuth.isTransferProxy(0, zero)); 144 | let claim = await claims.claims(0, 0); 145 | assert.equal(claim[0], ""); 146 | // for unlinked points, keys/continuity aren't incremented 147 | assert.equal(await azimuth.getKeyRevisionNumber(2), 0); 148 | assert.equal(await azimuth.getContinuityNumber(2), 0); 149 | await eclipt.transferPoint(2, user2, true, {from:user1}); 150 | assert.equal(await azimuth.getKeyRevisionNumber(2), 0); 151 | assert.equal(await azimuth.getContinuityNumber(2), 0); 152 | // transfer to self as temporary owner of spawned point 153 | assert.isTrue(await azimuth.isTransferProxy(768, user1)); 154 | // this shouldn't have emitted a transfer event 155 | await seeEvents(eclipt.transferPoint(768, user1, true, {from:user1}), []); 156 | // but still reset proxy because we asked 157 | assert.isTrue(await azimuth.isTransferProxy(768, zero)); 158 | }); 159 | 160 | it('allowing transfer of ownership', async function() { 161 | // can't do if not owner. 162 | await assertRevert(eclipt.setTransferProxy(0, user1, {from:user1})); 163 | // allow as owner. 164 | await seeEvents(eclipt.setTransferProxy(0, user1, {from:user2}), 165 | ['Approval']); 166 | await eclipt.setSpawnProxy(0, user1, {from:user2}); 167 | assert.isTrue(await azimuth.isTransferProxy(0, user1)); 168 | // transfer as transferrer, but don't reset. 169 | await eclipt.transferPoint(0, user1, false, {from:user1}); 170 | assert.isTrue(await azimuth.isOwner(0, user1)); 171 | assert.isTrue(await azimuth.isSpawnProxy(0, user1)); 172 | // transferrer always reset on-transfer, as per erc721 173 | assert.isFalse(await azimuth.isTransferProxy(0, user1)); 174 | }); 175 | 176 | it('rekeying a point', async function() { 177 | // can't do if not owner. 178 | await assertRevert(eclipt.configureKeys(web3.utils.toHex(0), 179 | web3.utils.toHex(9), 180 | web3.utils.toHex(8), 181 | web3.utils.toHex(1), 182 | false, 183 | {from:user2})); 184 | // can't do if point not active. 185 | await assertRevert(eclipt.configureKeys(web3.utils.toHex(100), 186 | web3.utils.toHex(9), 187 | web3.utils.toHex(8), 188 | web3.utils.toHex(1), 189 | false)); 190 | // rekey as owner. 191 | await eclipt.configureKeys(web3.utils.toHex(0), 192 | web3.utils.toHex(9), 193 | web3.utils.toHex(8), 194 | web3.utils.toHex(1), 195 | false, 196 | {from:user1}); 197 | let { crypt, auth, suite, revision } = await azimuth.getKeys(0); 198 | assert.equal(crypt, 199 | '0x0900000000000000000000000000000000000000000000000000000000000000'); 200 | assert.equal(auth, 201 | '0x0800000000000000000000000000000000000000000000000000000000000000'); 202 | assert.equal(suite, 1); 203 | assert.equal(revision, 3); 204 | assert.equal(await azimuth.getKeyRevisionNumber(0), 3); 205 | await eclipt.configureKeys(web3.utils.toHex(0), 206 | web3.utils.toHex(9), 207 | web3.utils.toHex(8), 208 | web3.utils.toHex(1), 209 | true, 210 | {from:user1}); 211 | assert.equal(await azimuth.getContinuityNumber(0), 2); 212 | }); 213 | 214 | it('setting management proxy', async function() { 215 | assert.equal(await azimuth.getManagementProxy(0), 0); 216 | await assertRevert(eclipt.setManagementProxy(0, owner, {from:user2})); 217 | await eclipt.setManagementProxy(0, user2, {from:user1}); 218 | await eclipt.setManagementProxy(0, owner, {from:user2}); 219 | assert.equal(await azimuth.getManagementProxy(0), owner); 220 | // manager can do things like configure keys 221 | await eclipt.configureKeys(web3.utils.toHex(0), 222 | web3.utils.toHex(9), 223 | web3.utils.toHex(9), 224 | web3.utils.toHex(1), 225 | false, 226 | {from:owner}); 227 | }); 228 | 229 | it('setting and canceling an escape', async function() { 230 | // can't if chosen sponsor not active. 231 | await assertRevert(eclipt.escape(257, 1, {from:user1})); 232 | await eclipt.configureKeys(web3.utils.toHex(1), 233 | web3.utils.toHex(8), 234 | web3.utils.toHex(9), 235 | web3.utils.toHex(1), 236 | false, 237 | {from:user1}); 238 | // can't if not owner of point. 239 | await assertRevert(eclipt.escape(256, 1, {from:user2})); 240 | await assertRevert(eclipt.cancelEscape(256, {from:user2})); 241 | // galaxies can't escape. 242 | await assertRevert(eclipt.escape(0, 1, {from:user1})); 243 | // set escape as owner. 244 | await eclipt.escape(256, 1, {from:user1}); 245 | assert.isTrue(await azimuth.isRequestingEscapeTo(256, 1)); 246 | await eclipt.cancelEscape(256, {from:user1}); 247 | assert.isFalse(await azimuth.isRequestingEscapeTo(256, 1)); 248 | await eclipt.escape(256, 1, {from:user1}); 249 | await eclipt.escape(512, 1, {from:user1}); 250 | // try out peer sponsorship. 251 | await eclipt.configureKeys(web3.utils.toHex(256), 252 | web3.utils.toHex(1), 253 | web3.utils.toHex(2), 254 | web3.utils.toHex(1), 255 | false, 256 | {from:user1}); 257 | await eclipt.spawn(65792, owner, {from:user1}); 258 | await eclipt.transferPoint(65792, owner, true); 259 | await eclipt.spawn(131328, owner, {from:user1}); 260 | await eclipt.transferPoint(131328, owner, true); 261 | assert.isFalse(await eclipt.canEscapeTo(131328, 65792)); 262 | await eclipt.configureKeys(web3.utils.toHex(65792), 263 | web3.utils.toHex(1), 264 | web3.utils.toHex(2), 265 | web3.utils.toHex(1), 266 | false); 267 | assert.isTrue(await eclipt.canEscapeTo(131328, 65792)); 268 | await eclipt.configureKeys(web3.utils.toHex(131328), 269 | web3.utils.toHex(3), 270 | web3.utils.toHex(4), 271 | web3.utils.toHex(1), 272 | false); 273 | assert.isFalse(await eclipt.canEscapeTo(131328, 65792)); 274 | }); 275 | 276 | it('adopting or reject an escaping point', async function() { 277 | // can't if not owner of sponsor. 278 | await assertRevert(eclipt.adopt(256, {from:user2})); 279 | await assertRevert(eclipt.reject(512, {from:user2})); 280 | // can't if target is not escaping to sponsor. 281 | await assertRevert(eclipt.adopt(258, {from:user1})); 282 | await assertRevert(eclipt.reject(258, {from:user1})); 283 | // adopt as sponsor owner. 284 | await eclipt.adopt(256, {from:user1}); 285 | assert.isFalse(await azimuth.isRequestingEscapeTo(256, 1)); 286 | assert.equal(await azimuth.getSponsor(256), 1); 287 | assert.isTrue(await azimuth.isSponsor(256, 1)); 288 | // reject as sponsor owner. 289 | await eclipt.reject(512, {from:user1}); 290 | assert.isFalse(await azimuth.isRequestingEscapeTo(512, 1)); 291 | assert.equal(await azimuth.getSponsor(512), 0); 292 | }); 293 | 294 | it('detaching sponsorship', async function() { 295 | // can't if not owner of sponsor 296 | await assertRevert(eclipt.detach(256, {from:user2})); 297 | await eclipt.detach(256, {from:user1}); 298 | assert.isFalse(await azimuth.isSponsor(256, 1)); 299 | assert.equal(await azimuth.getSponsor(256), 1); 300 | }); 301 | 302 | it('setting voting proxy', async function() { 303 | assert.equal(await azimuth.getVotingProxy(0), 0); 304 | await assertRevert(eclipt.setVotingProxy(0, owner, {from:user2})); 305 | await eclipt.setVotingProxy(0, user2, {from:user1}); 306 | await eclipt.setVotingProxy(0, owner, {from:user2}); 307 | assert.equal(await azimuth.getVotingProxy(0), owner); 308 | }); 309 | 310 | it('cannot spawn or change spawn proxy if on L2', async function() { 311 | // Deposit ~binzod to L2 312 | await eclipt.setSpawnProxy(512, user2, {from:user1}); 313 | assert.equal(await azimuth.getSpawnProxy(512), user2); 314 | await eclipt.configureKeys(512, '0x1', '0x2', 3, false, {from: user1}); 315 | assert.equal(await azimuth.getContinuityNumber(512), 0); 316 | await eclipt.spawn(0x10200, user2, {from:user1}); 317 | assert.equal(await azimuth.getTransferProxy(0x10200), user2); 318 | await eclipt.setSpawnProxy(512, deposit, {from:user1}); 319 | assert.equal(await azimuth.getSpawnProxy(512), deposit); 320 | 321 | // Can't change spawn proxy 322 | await assertRevert(eclipt.setSpawnProxy(512, user2, {from:user1})); 323 | assert.equal(await azimuth.getSpawnProxy(512), deposit); 324 | 325 | // Can't spawn 326 | await assertRevert(eclipt.spawn(0x20200, user2, {from:user1})); 327 | assert.equal(await azimuth.getOwner(0x20200), 0); 328 | 329 | // transferPoint with reset doesn't clear spawn rights 330 | await eclipt.transferPoint(512, user2, true, {from:user1}); 331 | assert.equal(await azimuth.getContinuityNumber(512), 1); 332 | assert.equal(await azimuth.getSpawnProxy(512), deposit); 333 | await eclipt.transferPoint(512, user1, false, {from:user2}); 334 | }); 335 | 336 | it('cannot deposit galaxy to L2', async function() { 337 | await eclipt.transferPoint(1, user2, false, {from:user1}); 338 | await assertRevert(eclipt.transferPoint(1, deposit, false, {from:user2})); 339 | assert.equal(await azimuth.getOwner(1), user2); 340 | await eclipt.transferPoint(1, user1, false, {from:user2}); 341 | }); 342 | 343 | it('clears correct data on deposit, regardless of reset', async function() { 344 | // without reset 345 | await eclipt.transferPoint(512, deposit, false, {from: user1}); 346 | assert.equal(await azimuth.getOwner(512), deposit) 347 | let { crypt, auth } = await azimuth.getKeys(512); 348 | assert.equal(crypt, zero64); 349 | assert.equal(auth, zero64); 350 | assert.equal(await azimuth.getKeyRevisionNumber(512), 2); 351 | assert.equal(await azimuth.getContinuityNumber(512), 1); 352 | assert.isTrue(await azimuth.isManagementProxy(512, zero)); 353 | assert.isTrue(await azimuth.isVotingProxy(512, zero)); 354 | assert.isTrue(await azimuth.isSpawnProxy(512, zero)); 355 | assert.isTrue(await azimuth.isTransferProxy(512, zero)); 356 | 357 | // with reset 358 | await eclipt.transferPoint(0x10200, user1, false, {from: user1}); 359 | await eclipt.setManagementProxy(0x10200, user2, {from:user1}); 360 | assert.isTrue(await azimuth.isManagementProxy(0x10200, user2)); 361 | await eclipt.transferPoint(0x10200, deposit, true, {from: user1}); 362 | assert.equal(await azimuth.getOwner(0x10200), deposit) 363 | let res = await azimuth.getKeys(0x10200); 364 | assert.equal(res.crypt, zero64); 365 | assert.equal(res.auth, zero64); 366 | assert.equal(await azimuth.getKeyRevisionNumber(0x10200), 0); 367 | assert.equal(await azimuth.getContinuityNumber(0x10200), 0); 368 | assert.isTrue(await azimuth.isManagementProxy(0x10200, zero)); 369 | assert.isTrue(await azimuth.isVotingProxy(0x10200, zero)); 370 | assert.isTrue(await azimuth.isSpawnProxy(0x10200, zero)); 371 | assert.isTrue(await azimuth.isTransferProxy(0x10200, zero)); 372 | }); 373 | 374 | it('cannot escape to L2 sponsor on L1', async function() { 375 | // 0x10100 == 65792 376 | await eclipt.configureKeys(768, '0x1', '0x2', 3, false, {from: user1}); 377 | await eclipt.escape(0x10100, 768, {from:owner}); 378 | assert.equal(await azimuth.getEscapeRequest(0x10100), 768); 379 | await assertRevert(eclipt.escape(65792, 512, {from:owner})); 380 | assert.equal(await azimuth.getEscapeRequest(0x10100), 768); 381 | }); 382 | 383 | it('[not implemented] cannot deposit contract-owned ship', async function() { 384 | // XXX: not sure how to test this? I guess we need to deploy a 385 | // contract which can control a ship and try to deposit it from 386 | // either that contract or a transfer proxy or operator 387 | }); 388 | 389 | it('voting on and updating document poll', async function() { 390 | // can't if not galaxy owner. 391 | await assertRevert(eclipt.startDocumentPoll(0, web3.utils.toHex(10), {from:user2})); 392 | await assertRevert(eclipt.castDocumentVote(0, web3.utils.toHex(10), true, {from:user2})); 393 | await eclipt.startDocumentPoll(0, web3.utils.toHex(10), {from:user1}); 394 | // can do voting operations as delegate 395 | await eclipt.castDocumentVote(0, web3.utils.toHex(10), true); 396 | assert.isTrue(await polls.hasVotedOnDocumentPoll(0, web3.utils.toHex(10))); 397 | await increaseTime(pollTime + 5); 398 | await eclipt.updateDocumentPoll(web3.utils.toHex(10)); 399 | assert.isTrue(await polls.documentHasAchievedMajority(web3.utils.toHex(10))); 400 | }); 401 | 402 | it('voting on upgrade poll', async function() { 403 | let ecliptx = await Ecliptic.new(zero, 404 | azimuth.address, 405 | polls.address, 406 | claims.address); 407 | eclipt2 = await Ecliptic.new(eclipt.address, 408 | azimuth.address, 409 | polls.address, 410 | claims.address); 411 | // can't if upgrade path not correct 412 | await assertRevert(eclipt.startUpgradePoll(0, ecliptx.address, {from:user1})); 413 | // can't start if not galaxy owner. 414 | await assertRevert(eclipt.startUpgradePoll(0, eclipt2.address, {from:user2})); 415 | await eclipt.startUpgradePoll(0, eclipt2.address, {from:user1}); 416 | // can't vote if not galaxy owner 417 | await assertRevert(eclipt.castUpgradeVote(0, eclipt2.address, true, {from:user2})); 418 | await eclipt.castUpgradeVote(0, eclipt2.address, true, {from:user1}); 419 | await eclipt.castUpgradeVote(1, eclipt2.address, true, {from:user1}); 420 | assert.equal(await azimuth.owner(), eclipt2.address); 421 | assert.equal(await polls.owner(), eclipt2.address); 422 | }); 423 | 424 | it('updating upgrade poll', async function() { 425 | let eclipt3 = await Ecliptic.new(eclipt2.address, 426 | azimuth.address, 427 | polls.address, 428 | claims.address); 429 | // onUpgrade can only be called by previous ecliptic 430 | await assertRevert(eclipt3.onUpgrade({from:user2})); 431 | assert.equal(await azimuth.owner(), eclipt2.address); 432 | await eclipt2.startUpgradePoll(0, eclipt3.address, {from:user1}); 433 | await eclipt2.castUpgradeVote(0, eclipt3.address, true, {from:user1}); 434 | await seeEvents(eclipt2.updateUpgradePoll(eclipt3.address), []); 435 | assert.equal(await azimuth.owner(), eclipt2.address); 436 | await increaseTime(pollTime + 5); 437 | await seeEvents(eclipt2.updateUpgradePoll(eclipt3.address), [ 438 | 'OwnershipTransferred', 439 | 'OwnershipTransferred', 440 | 'Upgraded' 441 | ]); 442 | assert.equal(await azimuth.owner(), eclipt3.address); 443 | assert.equal(await polls.owner(), eclipt3.address); 444 | }); 445 | }); 446 | --------------------------------------------------------------------------------