├── .gitignore ├── .babelrc ├── test ├── SmartToken.js ├── helpers │ ├── assert.js │ ├── MultiSigWalletMock.sol │ ├── StoxSmartTokenSaleMock.sol │ ├── expectThrow.js │ ├── time.js │ └── SaferMathMock.sol ├── StoxSmartToken.js ├── Ownable.js ├── SaferMath.js ├── Trustee.js ├── StoxSmartTokenSale.js └── MultiSigWallet.js ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── contracts ├── StoxSmartToken.sol ├── Migrations.sol ├── SaferMath.sol ├── Ownable.sol ├── Trustee.sol ├── StoxSmartTokenSale.sol └── MultiSigWallet.sol ├── truffle.js ├── package.json ├── unify.sh ├── LICENSE └── testrpc.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache 3 | /build/ 4 | /node_modules/ 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "stage-3"] 3 | } 4 | -------------------------------------------------------------------------------- /test/SmartToken.js: -------------------------------------------------------------------------------- 1 | import 'bancor-contracts/solidity/test/SmartToken.js'; 2 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require('./Migrations.sol'); 2 | 3 | module.exports = (deployer) => { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /test/helpers/assert.js: -------------------------------------------------------------------------------- 1 | let around = (a, b, diff) => { 2 | let abs = Math.abs(a - b); 3 | if (abs > diff) { 4 | throw new Error(`Assertion failed: ${a} is not ${diff} around ${b}`); 5 | } 6 | }; 7 | 8 | export default { around }; 9 | 10 | -------------------------------------------------------------------------------- /contracts/StoxSmartToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | import 'bancor-contracts/solidity/contracts/SmartToken.sol'; 4 | 5 | /// @title Stox Smart Token 6 | contract StoxSmartToken is SmartToken { 7 | function StoxSmartToken() SmartToken('Stox', 'STX', 18) { 8 | disableTransfers(true); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const SaferMath = artifacts.require('./SaferMath.sol'); 2 | const Ownable = artifacts.require('./Ownable.sol'); 3 | 4 | const StoxSmartToken = artifacts.require('./StoxSmartToken.sol'); 5 | 6 | module.exports = (deployer) => { 7 | deployer.deploy(Ownable); 8 | deployer.deploy(SaferMath); 9 | 10 | deployer.link(Ownable, StoxSmartToken); 11 | deployer.link(SaferMath, StoxSmartToken); 12 | 13 | deployer.deploy(StoxSmartToken); 14 | }; 15 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('babel-polyfill'); 3 | 4 | let provider; 5 | 6 | module.exports = { 7 | networks: { 8 | development: { 9 | host: 'localhost', 10 | port: 8545, 11 | network_id: '*' // Match any network id 12 | }, 13 | ropsten: { 14 | provider: provider, 15 | network_id: 3 // official id of the ropsten network 16 | } 17 | }, 18 | mocha: { 19 | useColors: true, 20 | slow: 30000 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /test/helpers/MultiSigWalletMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | import '../../contracts/MultiSigWallet.sol'; 4 | 5 | contract MultiSigWalletMock is MultiSigWallet { 6 | uint256 public transactionId; 7 | 8 | function MultiSigWalletMock(address[] _owners, uint _required) MultiSigWallet(_owners, _required) { 9 | } 10 | 11 | function submitTransaction(address destination, uint value, bytes data) public returns (uint transactionId) { 12 | transactionId = super.submitTransaction(destination, value, data); 13 | return transactionId; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/helpers/StoxSmartTokenSaleMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | import '../../contracts/StoxSmartTokenSale.sol'; 4 | 5 | contract StoxSmartTokenSaleMock is StoxSmartTokenSale { 6 | function StoxSmartTokenSaleMock(address _stox, address _fundingRecipient, uint256 _startTime) 7 | StoxSmartTokenSale(_stox, _fundingRecipient, _startTime) { 8 | } 9 | 10 | function setTokensSold(uint256 _tokensSold) { 11 | tokensSold = _tokensSold; 12 | } 13 | 14 | function setFinalized(bool state) { 15 | isFinalized = state; 16 | } 17 | 18 | function setDistributed(bool state) { 19 | isDistributed = state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public lastCompletedMigration; 6 | 7 | modifier restricted() { 8 | if (msg.sender != owner) { 9 | throw; 10 | } 11 | 12 | _; 13 | } 14 | 15 | function Migrations() { 16 | owner = msg.sender; 17 | } 18 | 19 | function setCompleted(uint completed) restricted { 20 | lastCompletedMigration = completed; 21 | } 22 | 23 | function upgrade(address new_address) restricted { 24 | Migrations upgraded = Migrations(new_address); 25 | upgraded.setCompleted(lastCompletedMigration); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/helpers/expectThrow.js: -------------------------------------------------------------------------------- 1 | export default async (promise) => { 2 | try { 3 | await promise; 4 | } catch (error) { 5 | // TODO: Check jump destination to destinguish between a throw and an actual invalid jump. 6 | const invalidOpcode = error.message.search('invalid opcode') > -1; 7 | 8 | // TODO: When we contract A calls contract B, and B throws, instead of an 'invalid jump', we get an 'out of gas' 9 | // error. How do we distinguish this from an actual out of gas event? The testrpc log actually show an "invalid 10 | // jump" event). 11 | const outOfGas = error.message.search('out of gas') > -1; 12 | 13 | assert(invalidOpcode || outOfGas, `Expected throw, got ${error} instead`); 14 | 15 | return; 16 | } 17 | 18 | assert(false, "Expected throw wasn't received"); 19 | }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stox-token", 3 | "version": "1.0.0", 4 | "repository": "git@github.com:stx-technologies/stox-token.git", 5 | "license": "MIT", 6 | "dependencies": { 7 | "bancor-contracts": "^0.2.0", 8 | "truffle-hdwallet-provider": "0.0.3" 9 | }, 10 | "devDependencies": { 11 | "babel-polyfill": "^6.23.0", 12 | "babel-preset-es2015": "^6.24.1", 13 | "babel-preset-stage-2": "^6.24.1", 14 | "babel-preset-stage-3": "^6.24.1", 15 | "babel-register": "^6.24.1", 16 | "bignumber.js": "^4.0.2", 17 | "ethereumjs-testrpc": "^4.0.1", 18 | "lerna": "^2.0.0", 19 | "truffle": "^3.4.5", 20 | "web3": "github:MichalZalecki/web3.js#04dffef5c24fd9aebfc3aa8224a4f9d38ca69d68", 21 | "yargs": "^8.0.2" 22 | }, 23 | "scripts": { 24 | "testrpc": "./testrpc.sh", 25 | "test": "./testrpc.sh > /dev/null & sleep 5 && truffle test" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/helpers/time.js: -------------------------------------------------------------------------------- 1 | const increaseTime = (time) => { 2 | return new Promise((resolve, reject) => { 3 | web3.currentProvider.sendAsync({ 4 | jsonrpc: '2.0', 5 | method: 'evm_increaseTime', 6 | params: [time], // Time increase param. 7 | id: new Date().getTime() 8 | }, (err) => { 9 | if (err) { 10 | return reject(err); 11 | } 12 | 13 | resolve(); 14 | }); 15 | }); 16 | }; 17 | 18 | const mine = () => { 19 | return new Promise((resolve, reject) => { 20 | web3.currentProvider.sendAsync({ 21 | jsonrpc: '2.0', 22 | method: 'evm_mine', 23 | params: [], 24 | id: new Date().getTime() 25 | }, (err) => { 26 | if (err) { 27 | return reject(err); 28 | } 29 | 30 | resolve(); 31 | }); 32 | }); 33 | }; 34 | 35 | export default { increaseTime, mine }; 36 | -------------------------------------------------------------------------------- /test/helpers/SaferMathMock.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | import '../../contracts/SaferMath.sol'; 4 | 5 | contract SaferMathMock { 6 | uint public result; 7 | 8 | function multiply(uint a, uint b) { 9 | result = SaferMath.mul(a, b); 10 | } 11 | 12 | function subtract(uint a, uint b) { 13 | result = SaferMath.sub(a, b); 14 | } 15 | 16 | function add(uint a, uint b) { 17 | result = SaferMath.add(a, b); 18 | } 19 | 20 | function divide(uint a, uint b) { 21 | result = SaferMath.div(a, b); 22 | } 23 | 24 | function max64(uint64 a, uint64 b) { 25 | result = SaferMath.max64(a, b); 26 | } 27 | 28 | function min64(uint64 a, uint64 b) { 29 | result = SaferMath.min64(a, b); 30 | } 31 | 32 | function max256(uint256 a, uint256 b) { 33 | result = SaferMath.max256(a, b); 34 | } 35 | 36 | function min256(uint256 a, uint256 b) { 37 | result = SaferMath.min256(a, b); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /unify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | function unify() { 4 | grep -v '^[pragma|import]' $1 >> Unified.sol 5 | } 6 | 7 | echo "pragma solidity ^0.4.11;" > Unified.sol 8 | 9 | # Bancor 10 | unify node_modules/bancor-contracts/solidity/contracts/IOwned.sol 11 | unify node_modules/bancor-contracts/solidity/contracts/IERC20Token.sol 12 | unify node_modules/bancor-contracts/solidity/contracts/ITokenHolder.sol 13 | unify node_modules/bancor-contracts/solidity/contracts/ISmartToken.sol 14 | unify node_modules/bancor-contracts/solidity/contracts/SafeMath.sol 15 | unify node_modules/bancor-contracts/solidity/contracts/ERC20Token.sol 16 | unify node_modules/bancor-contracts/solidity/contracts/Owned.sol 17 | unify node_modules/bancor-contracts/solidity/contracts/TokenHolder.sol 18 | unify node_modules/bancor-contracts/solidity/contracts/SmartToken.sol 19 | 20 | # Stox 21 | unify contracts/Ownable.sol 22 | unify contracts/SaferMath.sol 23 | unify contracts/StoxSmartToken.sol 24 | unify contracts/Trustee.sol 25 | unify contracts/StoxSmartTokenSale.sol 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 stx-technologies 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 | -------------------------------------------------------------------------------- /test/StoxSmartToken.js: -------------------------------------------------------------------------------- 1 | import expectThrow from './helpers/expectThrow'; 2 | 3 | const StoxSmartToken = artifacts.require('../contracts/StoxSmartToken.sol'); 4 | 5 | contract('StoxSmartToken', (accounts) => { 6 | let token; 7 | let owner = accounts[0]; 8 | 9 | beforeEach(async () => { 10 | token = await StoxSmartToken.new(); 11 | }); 12 | 13 | describe('construction', async () => { 14 | it('should be ownable', async () => { 15 | assert.equal(await token.owner(), owner); 16 | }); 17 | 18 | it('should return correct name after construction', async () => { 19 | assert.equal(await token.name(), 'Stox'); 20 | }); 21 | 22 | it('should return correct symbol after construction', async () => { 23 | assert.equal(await token.symbol(), 'STX'); 24 | }); 25 | 26 | it('should return correct decimal points after construction', async () => { 27 | assert.equal(await token.decimals(), 18); 28 | }); 29 | 30 | it('should be initialized as not transferable', async () => { 31 | assert.equal(await token.transfersEnabled(), false); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /testrpc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | pgrep node ./node_modules/.bin/testrpc | xargs kill 4 | 5 | ./node_modules/.bin/testrpc -u 0 -u 1 \ 6 | --account="0x59fc744f0a948ab6a0cf7651d15a0c4cdbcd9447d647ad094d7e67e443675779,10000000000000000000000000" \ 7 | --account="0xb07ca59544df2aa2c388d48a64b724bab2fbc9800f50196c2be90c6b6f75be94,10000000000000000000000000" \ 8 | --account="0xd3ab26e5752c6664478f6d5d43f51df9238419a11e408696cd34acbed8670ba0,10000000000000000000000000" \ 9 | --account="0xd683b11aef820a478280a8f31d9fc07a84a4965d7c47d16ee5dd378f0026ca5b,10000000000000000000000000" \ 10 | --account="0xa0596c5ddec1f254f890d38d40107c1f8f37adc8f0fc58ea4fadc2b7b5998eab,10000000000000000000000000" \ 11 | --account="0x580d6c304def110bb2472ec9acd958e541f016a1b16617251bded2414cb73af2,10000000000000000000000000" \ 12 | --account="0x001d655d5b0814b3191f382e0831c3649010b1670c4039bd57bafc5c78c4a7b5,10000000000000000000000000" \ 13 | --account="0x6f8654b9ae76dee5cfc64a4bfcaaf0fed7687b0adc18185e37be7571e5b29e33,10000000000000000000000000" \ 14 | --account="0x7e77e8830c5eeeeabad5782dd469c47b1f5ede44a165acba644dec2c5d4fe280,10000000000000000000000000" \ 15 | --account="0x19a8bf692f721f5a8dac9c6c3eff4b0850e0a510d716cbf83030d6d4b3bb60d2,10000000000000000000000000" 16 | -------------------------------------------------------------------------------- /contracts/SaferMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | /// @title Math operations with safety checks 4 | library SaferMath { 5 | function mul(uint256 a, uint256 b) internal returns (uint256) { 6 | uint256 c = a * b; 7 | assert(a == 0 || c / a == b); 8 | return c; 9 | } 10 | 11 | function div(uint256 a, uint256 b) internal returns (uint256) { 12 | // assert(b > 0); // Solidity automatically throws when dividing by 0 13 | uint256 c = a / b; 14 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 15 | return c; 16 | } 17 | 18 | function sub(uint256 a, uint256 b) internal returns (uint256) { 19 | assert(b <= a); 20 | return a - b; 21 | } 22 | 23 | function add(uint256 a, uint256 b) internal returns (uint256) { 24 | uint256 c = a + b; 25 | assert(c >= a); 26 | return c; 27 | } 28 | 29 | function max64(uint64 a, uint64 b) internal constant returns (uint64) { 30 | return a >= b ? a : b; 31 | } 32 | 33 | function min64(uint64 a, uint64 b) internal constant returns (uint64) { 34 | return a < b ? a : b; 35 | } 36 | 37 | function max256(uint256 a, uint256 b) internal constant returns (uint256) { 38 | return a >= b ? a : b; 39 | } 40 | 41 | function min256(uint256 a, uint256 b) internal constant returns (uint256) { 42 | return a < b ? a : b; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /contracts/Ownable.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | /// @title Ownable 4 | /// @dev The Ownable contract has an owner address, and provides basic authorization control functions, this simplifies 5 | /// & the implementation of "user permissions". 6 | contract Ownable { 7 | address public owner; 8 | address public newOwnerCandidate; 9 | 10 | event OwnershipRequested(address indexed _by, address indexed _to); 11 | event OwnershipTransferred(address indexed _from, address indexed _to); 12 | 13 | /// @dev The Ownable constructor sets the original `owner` of the contract to the sender account. 14 | function Ownable() { 15 | owner = msg.sender; 16 | } 17 | 18 | /// @dev Throws if called by any account other than the owner. 19 | modifier onlyOwner() { 20 | if (msg.sender != owner) { 21 | throw; 22 | } 23 | 24 | _; 25 | } 26 | 27 | /// @dev Proposes to transfer control of the contract to a newOwnerCandidate. 28 | /// @param _newOwnerCandidate address The address to transfer ownership to. 29 | function transferOwnership(address _newOwnerCandidate) onlyOwner { 30 | require(_newOwnerCandidate != address(0)); 31 | 32 | newOwnerCandidate = _newOwnerCandidate; 33 | 34 | OwnershipRequested(msg.sender, newOwnerCandidate); 35 | } 36 | 37 | /// @dev Accept ownership transfer. This method needs to be called by the perviously proposed owner. 38 | function acceptOwnership() { 39 | if (msg.sender == newOwnerCandidate) { 40 | owner = newOwnerCandidate; 41 | newOwnerCandidate = address(0); 42 | 43 | OwnershipTransferred(owner, newOwnerCandidate); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/Ownable.js: -------------------------------------------------------------------------------- 1 | import expectThrow from './helpers/expectThrow'; 2 | const Ownable = artifacts.require('../contracts/Ownable.sol'); 3 | 4 | contract('Ownable', (accounts) => { 5 | let ownable; 6 | 7 | let owner = accounts[0]; 8 | let newOwner = accounts[1]; 9 | let stranger = accounts[2]; 10 | 11 | beforeEach(async () => { 12 | ownable = await Ownable.new(); 13 | }); 14 | 15 | describe('construction', async () => { 16 | it('should have an owner', async () => { 17 | assert.equal(await ownable.owner(), owner); 18 | }); 19 | 20 | it('should not have a newOwnerCandidate', async () => { 21 | assert.equal(await ownable.newOwnerCandidate(), 0); 22 | }); 23 | }); 24 | 25 | describe('ownership transfer', async () => { 26 | it('should change newOwnerCandidate', async () => { 27 | await ownable.transferOwnership(newOwner); 28 | 29 | assert.equal(await ownable.newOwnerCandidate(), newOwner); 30 | }); 31 | 32 | it('should not change owner without approving the new owner', async () => { 33 | await ownable.transferOwnership(newOwner); 34 | 35 | assert.equal(await ownable.owner(), owner); 36 | }); 37 | 38 | it('should change owner after transfer and approval', async () => { 39 | await ownable.transferOwnership(newOwner); 40 | await ownable.acceptOwnership({from: newOwner}); 41 | 42 | assert.equal(await ownable.owner(), newOwner); 43 | assert.equal(await ownable.newOwnerCandidate(), 0); 44 | }); 45 | 46 | it('should prevent non-owners from transfering ownership', async () => { 47 | assert((await ownable.owner()) != stranger); 48 | 49 | await expectThrow(ownable.transferOwnership(newOwner, {from: stranger})); 50 | }); 51 | 52 | it('should prevent transferring ownership to null or 0 address', async () => { 53 | await expectThrow(ownable.transferOwnership(null, {from: owner})); 54 | await expectThrow(ownable.transferOwnership(0, {from: owner})); 55 | 56 | assert.equal(owner, await ownable.owner()); 57 | }); 58 | 59 | it('should prevent strangers from accepting ownership', async () => { 60 | await ownable.transferOwnership(newOwner); 61 | assert.equal(await ownable.newOwnerCandidate(), newOwner); 62 | 63 | await ownable.acceptOwnership({from: stranger}); 64 | assert.equal(await ownable.newOwnerCandidate(), newOwner); 65 | assert.equal(await ownable.owner(), owner); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/SaferMath.js: -------------------------------------------------------------------------------- 1 | import expectThrow from './helpers/expectThrow'; 2 | const SaferMathMock = artifacts.require('./helpers/SaferMathMock.sol'); 3 | 4 | contract('SaferMath', (accounts) => { 5 | let saferMath; 6 | 7 | beforeEach(async () => { 8 | saferMath = await SaferMathMock.new(); 9 | }); 10 | 11 | describe('mul', async () => { 12 | [ 13 | [5678, 1234], 14 | [2, 0], 15 | [575689, 123] 16 | ].forEach((pair) => { 17 | it(`multiplies ${pair[0]} and ${pair[1]} correctly`, async () => { 18 | let a = pair[0]; 19 | let b = pair[1]; 20 | await saferMath.multiply(a, b); 21 | let result = await saferMath.result(); 22 | assert.equal(result, a * b); 23 | }); 24 | }); 25 | 26 | it('should throw an error on multiplication overflow', async () => { 27 | let a = 115792089237316195423570985008687907853269984665640564039457584007913129639933; 28 | let b = 2; 29 | 30 | await expectThrow(saferMath.multiply(a, b)); 31 | }); 32 | }); 33 | 34 | describe('add', async () => { 35 | [ 36 | [5678, 1234], 37 | [2, 0], 38 | [123, 575689] 39 | ].forEach((pair) => { 40 | it(`adds ${pair[0]} and ${pair[1]} correctly`, async () => { 41 | let a = pair[0]; 42 | let b = pair[1]; 43 | await saferMath.add(a, b); 44 | let result = await saferMath.result(); 45 | 46 | assert.equal(result, a + b); 47 | }); 48 | }); 49 | 50 | it('should throw an error on addition overflow', async () => { 51 | let a = 115792089237316195423570985008687907853269984665640564039457584007913129639935; 52 | let b = 1; 53 | 54 | await expectThrow(saferMath.add(a, b)); 55 | }); 56 | }); 57 | 58 | describe('sub', async () => { 59 | [ 60 | [5678, 1234], 61 | [2, 0], 62 | [575689, 123] 63 | ].forEach((pair) => { 64 | it(`subtracts ${pair[0]} and ${pair[1]} correctly`, async () => { 65 | let a = pair[0]; 66 | let b = pair[1]; 67 | await saferMath.subtract(a, b); 68 | let result = await saferMath.result(); 69 | 70 | assert.equal(result, a - b); 71 | }); 72 | }); 73 | 74 | it('should throw an error if subtraction result would be negative', async () => { 75 | let a = 1234; 76 | let b = 5678; 77 | 78 | await expectThrow(saferMath.subtract(a, b)); 79 | }); 80 | }); 81 | 82 | describe('div', () => { 83 | [ 84 | [5678, 1234], 85 | [2, 1], 86 | [123, 575689] 87 | ].forEach((pair) => { 88 | it(`divides ${pair[0]} and ${pair[1]} correctly`, async () => { 89 | let a = pair[0]; 90 | let b = pair[1]; 91 | await saferMath.divide(a, b); 92 | let result = await saferMath.result(); 93 | 94 | assert.equal(result, Math.floor(a / b)); 95 | }); 96 | }); 97 | 98 | it('should throw an error on division by 0', async () => { 99 | let a = 100; 100 | let b = 0; 101 | 102 | await expectThrow(saferMath.divide(a, b)); 103 | }); 104 | }); 105 | 106 | describe('max64', () => { 107 | [ 108 | [5678, 1234], 109 | [2, 1], 110 | [123, 575689] 111 | ].forEach((pair) => { 112 | it(`get the max64 of ${pair[0]} and ${pair[1]} correctly`, async () => { 113 | let a = pair[0]; 114 | let b = pair[1]; 115 | await saferMath.max64(a, b); 116 | let result = await saferMath.result(); 117 | 118 | assert.equal(result, Math.max(a, b)); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('min64', () => { 124 | [ 125 | [5678, 1234], 126 | [2, 1], 127 | [123, 575689] 128 | ].forEach((pair) => { 129 | it(`get the min64 of ${pair[0]} and ${pair[1]} correctly`, async () => { 130 | let a = pair[0]; 131 | let b = pair[1]; 132 | await saferMath.min64(a, b); 133 | let result = await saferMath.result(); 134 | 135 | assert.equal(result, Math.min(a, b)); 136 | }); 137 | }); 138 | }); 139 | 140 | describe('max256', () => { 141 | [ 142 | [5678, 1234], 143 | [2, 1], 144 | [123, 575689] 145 | ].forEach((pair) => { 146 | it(`get the max256 of ${pair[0]} and ${pair[1]} correctly`, async () => { 147 | let a = pair[0]; 148 | let b = pair[1]; 149 | await saferMath.max256(a, b); 150 | let result = await saferMath.result(); 151 | 152 | assert.equal(result, Math.max(a, b)); 153 | }); 154 | }); 155 | }); 156 | 157 | describe('min256', () => { 158 | [ 159 | [5678, 1234], 160 | [2, 1], 161 | [123, 575689] 162 | ].forEach((pair) => { 163 | it(`get the min256 of ${pair[0]} and ${pair[1]} correctly`, async () => { 164 | let a = pair[0]; 165 | let b = pair[1]; 166 | await saferMath.min256(a, b); 167 | let result = await saferMath.result(); 168 | 169 | assert.equal(result, Math.min(a, b)); 170 | }); 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /contracts/Trustee.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | import './SaferMath.sol'; 4 | import './Ownable.sol'; 5 | import './StoxSmartToken.sol'; 6 | 7 | /// @title Vesting trustee 8 | contract Trustee is Ownable { 9 | using SaferMath for uint256; 10 | 11 | // The address of the STX ERC20 token. 12 | StoxSmartToken public stox; 13 | 14 | struct Grant { 15 | uint256 value; 16 | uint256 start; 17 | uint256 cliff; 18 | uint256 end; 19 | uint256 transferred; 20 | bool revokable; 21 | } 22 | 23 | // Grants holder. 24 | mapping (address => Grant) public grants; 25 | 26 | // Total tokens available for vesting. 27 | uint256 public totalVesting; 28 | 29 | event NewGrant(address indexed _from, address indexed _to, uint256 _value); 30 | event UnlockGrant(address indexed _holder, uint256 _value); 31 | event RevokeGrant(address indexed _holder, uint256 _refund); 32 | 33 | /// @dev Constructor that initializes the address of the StoxSmartToken contract. 34 | /// @param _stox StoxSmartToken The address of the previously deployed StoxSmartToken smart contract. 35 | function Trustee(StoxSmartToken _stox) { 36 | require(_stox != address(0)); 37 | 38 | stox = _stox; 39 | } 40 | 41 | /// @dev Grant tokens to a specified address. 42 | /// @param _to address The address to grant tokens to. 43 | /// @param _value uint256 The amount of tokens to be granted. 44 | /// @param _start uint256 The beginning of the vesting period. 45 | /// @param _cliff uint256 Duration of the cliff period. 46 | /// @param _end uint256 The end of the vesting period. 47 | /// @param _revokable bool Whether the grant is revokable or not. 48 | function grant(address _to, uint256 _value, uint256 _start, uint256 _cliff, uint256 _end, bool _revokable) 49 | public onlyOwner { 50 | require(_to != address(0)); 51 | require(_value > 0); 52 | 53 | // Make sure that a single address can be granted tokens only once. 54 | require(grants[_to].value == 0); 55 | 56 | // Check for date inconsistencies that may cause unexpected behavior. 57 | require(_start <= _cliff && _cliff <= _end); 58 | 59 | // Check that this grant doesn't exceed the total amount of tokens currently available for vesting. 60 | require(totalVesting.add(_value) <= stox.balanceOf(address(this))); 61 | 62 | // Assign a new grant. 63 | grants[_to] = Grant({ 64 | value: _value, 65 | start: _start, 66 | cliff: _cliff, 67 | end: _end, 68 | transferred: 0, 69 | revokable: _revokable 70 | }); 71 | 72 | // Tokens granted, reduce the total amount available for vesting. 73 | totalVesting = totalVesting.add(_value); 74 | 75 | NewGrant(msg.sender, _to, _value); 76 | } 77 | 78 | /// @dev Revoke the grant of tokens of a specifed address. 79 | /// @param _holder The address which will have its tokens revoked. 80 | function revoke(address _holder) public onlyOwner { 81 | Grant grant = grants[_holder]; 82 | 83 | require(grant.revokable); 84 | 85 | // Send the remaining STX back to the owner. 86 | uint256 refund = grant.value.sub(grant.transferred); 87 | 88 | // Remove the grant. 89 | delete grants[_holder]; 90 | 91 | totalVesting = totalVesting.sub(refund); 92 | stox.transfer(msg.sender, refund); 93 | 94 | RevokeGrant(_holder, refund); 95 | } 96 | 97 | /// @dev Calculate the total amount of vested tokens of a holder at a given time. 98 | /// @param _holder address The address of the holder. 99 | /// @param _time uint256 The specific time. 100 | /// @return a uint256 representing a holder's total amount of vested tokens. 101 | function vestedTokens(address _holder, uint256 _time) public constant returns (uint256) { 102 | Grant grant = grants[_holder]; 103 | if (grant.value == 0) { 104 | return 0; 105 | } 106 | 107 | return calculateVestedTokens(grant, _time); 108 | } 109 | 110 | /// @dev Calculate amount of vested tokens at a specifc time. 111 | /// @param _grant Grant The vesting grant. 112 | /// @param _time uint256 The time to be checked 113 | /// @return An uint256 representing the amount of vested tokens of a specific grant. 114 | /// | _/-------- vestedTokens rect 115 | /// | _/ 116 | /// | _/ 117 | /// | _/ 118 | /// | _/ 119 | /// | / 120 | /// | .| 121 | /// | . | 122 | /// | . | 123 | /// | . | 124 | /// | . | 125 | /// | . | 126 | /// +===+===========+---------+----------> time 127 | /// Start Cliff End 128 | function calculateVestedTokens(Grant _grant, uint256 _time) private constant returns (uint256) { 129 | // If we're before the cliff, then nothing is vested. 130 | if (_time < _grant.cliff) { 131 | return 0; 132 | } 133 | 134 | // If we're after the end of the vesting period - everything is vested; 135 | if (_time >= _grant.end) { 136 | return _grant.value; 137 | } 138 | 139 | // Interpolate all vested tokens: vestedTokens = tokens/// (time - start) / (end - start) 140 | return _grant.value.mul(_time.sub(_grant.start)).div(_grant.end.sub(_grant.start)); 141 | } 142 | 143 | /// @dev Unlock vested tokens and transfer them to their holder. 144 | /// @return a uint256 representing the amount of vested tokens transferred to their holder. 145 | function unlockVestedTokens() public { 146 | Grant grant = grants[msg.sender]; 147 | require(grant.value != 0); 148 | 149 | // Get the total amount of vested tokens, acccording to grant. 150 | uint256 vested = calculateVestedTokens(grant, now); 151 | if (vested == 0) { 152 | return; 153 | } 154 | 155 | // Make sure the holder doesn't transfer more than what he already has. 156 | uint256 transferable = vested.sub(grant.transferred); 157 | if (transferable == 0) { 158 | return; 159 | } 160 | 161 | grant.transferred = grant.transferred.add(transferable); 162 | totalVesting = totalVesting.sub(transferable); 163 | stox.transfer(msg.sender, transferable); 164 | 165 | UnlockGrant(msg.sender, transferable); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /contracts/StoxSmartTokenSale.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | 3 | import './SaferMath.sol'; 4 | import './Ownable.sol'; 5 | import './StoxSmartToken.sol'; 6 | import './Trustee.sol'; 7 | 8 | /// @title Stox Smart Token sale 9 | contract StoxSmartTokenSale is Ownable { 10 | using SaferMath for uint256; 11 | 12 | uint256 public constant DURATION = 14 days; 13 | 14 | bool public isFinalized = false; 15 | bool public isDistributed = false; 16 | 17 | // The address of the STX ERC20 token. 18 | StoxSmartToken public stox; 19 | 20 | // The address of the token allocation trustee; 21 | Trustee public trustee; 22 | 23 | uint256 public startTime = 0; 24 | uint256 public endTime = 0; 25 | address public fundingRecipient; 26 | 27 | uint256 public tokensSold = 0; 28 | 29 | // TODO: update to the correct values. 30 | uint256 public constant ETH_CAP = 148000; 31 | uint256 public constant EXCHANGE_RATE = 200; // 200 STX for ETH 32 | uint256 public constant TOKEN_SALE_CAP = ETH_CAP * EXCHANGE_RATE * 10 ** 18; 33 | 34 | event TokensIssued(address indexed _to, uint256 _tokens); 35 | 36 | /// @dev Throws if called when not during sale. 37 | modifier onlyDuringSale() { 38 | if (tokensSold >= TOKEN_SALE_CAP || now < startTime || now >= endTime) { 39 | throw; 40 | } 41 | 42 | _; 43 | } 44 | 45 | /// @dev Throws if called before sale ends. 46 | modifier onlyAfterSale() { 47 | if (!(tokensSold >= TOKEN_SALE_CAP || now >= endTime)) { 48 | throw; 49 | } 50 | 51 | _; 52 | } 53 | 54 | /// @dev Constructor that initializes the sale conditions. 55 | /// @param _fundingRecipient address The address of the funding recipient. 56 | /// @param _startTime uint256 The start time of the token sale. 57 | function StoxSmartTokenSale(address _stox, address _fundingRecipient, uint256 _startTime) { 58 | require(_stox != address(0)); 59 | require(_fundingRecipient != address(0)); 60 | require(_startTime > now); 61 | 62 | stox = StoxSmartToken(_stox); 63 | 64 | fundingRecipient = _fundingRecipient; 65 | startTime = _startTime; 66 | endTime = startTime + DURATION; 67 | } 68 | 69 | /// @dev Distributed tokens to the partners who have participated during the pre-sale. 70 | function distributePartnerTokens() external onlyOwner { 71 | require(!isDistributed); 72 | 73 | assert(tokensSold == 0); 74 | assert(stox.totalSupply() == 0); 75 | 76 | // Distribute strategic tokens to partners. Please note, that this address doesn't represent a single entity or 77 | // person and will be only used to distribute tokens to 30~ partners. 78 | // 79 | // Please expect to see token transfers from this address in the first 24 hours after the token sale ends. 80 | issueTokens(0x9065260ef6830f6372F1Bde408DeC57Fe3150530, 14800000 * 10 ** 18); 81 | 82 | isDistributed = true; 83 | } 84 | 85 | /// @dev Finalizes the token sale event. 86 | function finalize() external onlyAfterSale { 87 | if (isFinalized) { 88 | throw; 89 | } 90 | 91 | // Grant vesting grants. 92 | // 93 | // TODO: use real addresses. 94 | trustee = new Trustee(stox); 95 | 96 | // Since only 50% of the tokens will be sold, we will automatically issue the same amount of sold STX to the 97 | // trustee. 98 | uint256 unsoldTokens = tokensSold; 99 | 100 | // Issue 55% of the remaining tokens (== 27.5%) go to strategic parternships. 101 | uint256 strategicPartnershipTokens = unsoldTokens.mul(55).div(100); 102 | 103 | // Note: we will substract the bonus tokens from this grant, since they were already issued for the pre-sale 104 | // strategic partners and should've been taken from this allocation. 105 | stox.issue(0xbC14105ccDdeAadB96Ba8dCE18b40C45b6bACf58, strategicPartnershipTokens); 106 | 107 | // Issue the remaining tokens as vesting grants: 108 | stox.issue(trustee, unsoldTokens.sub(strategicPartnershipTokens)); 109 | 110 | // 25% of the remaining tokens (== 12.5%) go to Invest.com, at uniform 12 months vesting schedule. 111 | trustee.grant(0xb54c6a870d4aD65e23d471Fb7941aD271D323f5E, unsoldTokens.mul(25).div(100), now, now, 112 | now.add(1 years), true); 113 | 114 | // 20% of the remaining tokens (== 10%) go to Stox team, at uniform 24 months vesting schedule. 115 | trustee.grant(0x4eB4Cd1D125d9d281709Ff38d65b99a6927b46c1, unsoldTokens.mul(20).div(100), now, now, 116 | now.add(2 years), true); 117 | 118 | // Re-enable transfers after the token sale. 119 | stox.disableTransfers(false); 120 | 121 | isFinalized = true; 122 | } 123 | 124 | /// @dev Create and sell tokens to the caller. 125 | /// @param _recipient address The address of the recipient. 126 | function create(address _recipient) public payable onlyDuringSale { 127 | require(_recipient != address(0)); 128 | require(msg.value > 0); 129 | 130 | assert(isDistributed); 131 | 132 | uint256 tokens = SaferMath.min256(msg.value.mul(EXCHANGE_RATE), TOKEN_SALE_CAP.sub(tokensSold)); 133 | uint256 contribution = tokens.div(EXCHANGE_RATE); 134 | 135 | issueTokens(_recipient, tokens); 136 | 137 | // Transfer the funds to the funding recipient. 138 | fundingRecipient.transfer(contribution); 139 | 140 | // Refund the msg.sender, in the case that not all of its ETH was used. This can happen only when selling the 141 | // last chunk of STX. 142 | uint256 refund = msg.value.sub(contribution); 143 | if (refund > 0) { 144 | msg.sender.transfer(refund); 145 | } 146 | } 147 | 148 | /// @dev Issues tokens for the recipient. 149 | /// @param _recipient address The address of the recipient. 150 | /// @param _tokens uint256 The amount of tokens to issue. 151 | function issueTokens(address _recipient, uint256 _tokens) private { 152 | // Update total sold tokens. 153 | tokensSold = tokensSold.add(_tokens); 154 | 155 | stox.issue(_recipient, _tokens); 156 | 157 | TokensIssued(_recipient, _tokens); 158 | } 159 | 160 | /// @dev Fallback function that will delegate the request to create. 161 | function () external payable onlyDuringSale { 162 | create(msg.sender); 163 | } 164 | 165 | /// @dev Proposes to transfer control of the StoxSmartToken contract to a new owner. 166 | /// @param _newOwnerCandidate address The address to transfer ownership to. 167 | /// 168 | /// Note that: 169 | /// 1. The new owner will need to call StoxSmartToken's acceptOwnership directly in order to accept the ownership. 170 | /// 2. Calling this method during the token sale will prevent the token sale to continue, since only the owner of 171 | /// the StoxSmartToken contract can issue new tokens. 172 | function transferSmartTokenOwnership(address _newOwnerCandidate) external onlyOwner { 173 | stox.transferOwnership(_newOwnerCandidate); 174 | } 175 | 176 | /// @dev Accepts new ownership on behalf of the StoxSmartToken contract. This can be used, by the token sale 177 | /// contract itself to claim back ownership of the StoxSmartToken contract. 178 | function acceptSmartTokenOwnership() external onlyOwner { 179 | stox.acceptOwnership(); 180 | } 181 | 182 | /// @dev Proposes to transfer control of the Trustee contract to a new owner. 183 | /// @param _newOwnerCandidate address The address to transfer ownership to. 184 | /// 185 | /// Note that: 186 | /// 1. The new owner will need to call Trustee's acceptOwnership directly in order to accept the ownership. 187 | /// 2. Calling this method during the token sale won't be possible, as the Trustee is only created after its 188 | /// finalization. 189 | function transferTrusteeOwnership(address _newOwnerCandidate) external onlyOwner { 190 | trustee.transferOwnership(_newOwnerCandidate); 191 | } 192 | 193 | /// @dev Accepts new ownership on behalf of the Trustee contract. This can be used, by the token sale 194 | /// contract itself to claim back ownership of the Trustee contract. 195 | function acceptTrusteeOwnership() external onlyOwner { 196 | trustee.acceptOwnership(); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /contracts/MultiSigWallet.sol: -------------------------------------------------------------------------------- 1 | /// This code was taken from: https://github.com/ConsenSys. Please do not change or refactor. 2 | 3 | pragma solidity 0.4.11; 4 | 5 | /// @title Multisignature wallet - Allows multiple parties to agree on transactions before execution. 6 | /// @author Stefan George - 7 | contract MultiSigWallet { 8 | 9 | uint constant public MAX_OWNER_COUNT = 50; 10 | 11 | event Confirmation(address indexed sender, uint indexed transactionId); 12 | event Revocation(address indexed sender, uint indexed transactionId); 13 | event Submission(uint indexed transactionId); 14 | event Execution(uint indexed transactionId); 15 | event ExecutionFailure(uint indexed transactionId); 16 | event Deposit(address indexed sender, uint value); 17 | event OwnerAddition(address indexed owner); 18 | event OwnerRemoval(address indexed owner); 19 | event RequirementChange(uint required); 20 | 21 | mapping (uint => Transaction) public transactions; 22 | mapping (uint => mapping (address => bool)) public confirmations; 23 | mapping (address => bool) public isOwner; 24 | address[] public owners; 25 | uint public required; 26 | uint public transactionCount; 27 | 28 | struct Transaction { 29 | address destination; 30 | uint value; 31 | bytes data; 32 | bool executed; 33 | } 34 | 35 | modifier onlyWallet() { 36 | if (msg.sender != address(this)) 37 | throw; 38 | _; 39 | } 40 | 41 | modifier ownerDoesNotExist(address owner) { 42 | if (isOwner[owner]) 43 | throw; 44 | _; 45 | } 46 | 47 | modifier ownerExists(address owner) { 48 | if (!isOwner[owner]) 49 | throw; 50 | _; 51 | } 52 | 53 | modifier transactionExists(uint transactionId) { 54 | if (transactions[transactionId].destination == 0) 55 | throw; 56 | _; 57 | } 58 | 59 | modifier confirmed(uint transactionId, address owner) { 60 | if (!confirmations[transactionId][owner]) 61 | throw; 62 | _; 63 | } 64 | 65 | modifier notConfirmed(uint transactionId, address owner) { 66 | if (confirmations[transactionId][owner]) 67 | throw; 68 | _; 69 | } 70 | 71 | modifier notExecuted(uint transactionId) { 72 | if (transactions[transactionId].executed) 73 | throw; 74 | _; 75 | } 76 | 77 | modifier notNull(address _address) { 78 | if (_address == 0) 79 | throw; 80 | _; 81 | } 82 | 83 | modifier validRequirement(uint ownerCount, uint _required) { 84 | if ( ownerCount > MAX_OWNER_COUNT 85 | || _required > ownerCount 86 | || _required == 0 87 | || ownerCount == 0) 88 | throw; 89 | _; 90 | } 91 | 92 | /// @dev Fallback function allows to deposit ether. 93 | function() 94 | payable 95 | { 96 | if (msg.value > 0) 97 | Deposit(msg.sender, msg.value); 98 | } 99 | 100 | /* 101 | * Public functions 102 | */ 103 | /// @dev Contract constructor sets initial owners and required number of confirmations. 104 | /// @param _owners List of initial owners. 105 | /// @param _required Number of required confirmations. 106 | function MultiSigWallet(address[] _owners, uint _required) 107 | public 108 | validRequirement(_owners.length, _required) 109 | { 110 | for (uint i=0; i<_owners.length; i++) { 111 | if (isOwner[_owners[i]] || _owners[i] == 0) 112 | throw; 113 | isOwner[_owners[i]] = true; 114 | } 115 | owners = _owners; 116 | required = _required; 117 | } 118 | 119 | /// @dev Allows to add a new owner. Transaction has to be sent by wallet. 120 | /// @param owner Address of new owner. 121 | function addOwner(address owner) 122 | public 123 | onlyWallet 124 | ownerDoesNotExist(owner) 125 | notNull(owner) 126 | validRequirement(owners.length + 1, required) 127 | { 128 | isOwner[owner] = true; 129 | owners.push(owner); 130 | OwnerAddition(owner); 131 | } 132 | 133 | /// @dev Allows to remove an owner. Transaction has to be sent by wallet. 134 | /// @param owner Address of owner. 135 | function removeOwner(address owner) 136 | public 137 | onlyWallet 138 | ownerExists(owner) 139 | { 140 | isOwner[owner] = false; 141 | for (uint i=0; i owners.length) 148 | changeRequirement(owners.length); 149 | OwnerRemoval(owner); 150 | } 151 | 152 | /// @dev Allows to replace an owner with a new owner. Transaction has to be sent by wallet. 153 | /// @param owner Address of owner to be replaced. 154 | /// @param owner Address of new owner. 155 | function replaceOwner(address owner, address newOwner) 156 | public 157 | onlyWallet 158 | ownerExists(owner) 159 | ownerDoesNotExist(newOwner) 160 | { 161 | for (uint i=0; i { 9 | const MINUTE = 60; 10 | const HOUR = 60 * MINUTE; 11 | const DAY = 24 * 60; 12 | const MONTH = 30 * DAY; 13 | const YEAR = 12 * MONTH; 14 | 15 | let now; 16 | let granter = accounts[0]; 17 | let token; 18 | let trustee; 19 | 20 | beforeEach(async () => { 21 | now = web3.eth.getBlock(web3.eth.blockNumber).timestamp; 22 | 23 | token = await StoxSmartToken.new(); 24 | await token.disableTransfers(false); 25 | 26 | trustee = await Trustee.new(token.address, {from: granter}); 27 | }); 28 | 29 | let getGrant = async (address) => { 30 | let grant = await trustee.grants(address); 31 | 32 | return { 33 | value: grant[0], 34 | start: grant[1], 35 | cliff: grant[2], 36 | end: grant[3], 37 | transferred: grant[4], 38 | revokable: grant[5] 39 | }; 40 | } 41 | 42 | describe('construction', async () => { 43 | it('should be initialized with a valid address', async () => { 44 | await expectThrow(Trustee.new()); 45 | }); 46 | 47 | it('should be ownable', async () => { 48 | assert.equal(await trustee.owner(), granter); 49 | }); 50 | 51 | it('should initially start with 0', async () => { 52 | let trusteeBalance = (await token.balanceOf(trustee.address)).toNumber(); 53 | assert.equal(trusteeBalance, 0); 54 | }); 55 | 56 | let balance = 1000; 57 | context(`with ${balance} tokens assigned to the trustee`, async () => { 58 | beforeEach(async () => { 59 | await token.issue(trustee.address, balance); 60 | }); 61 | 62 | it(`should equal to ${balance}`, async () => { 63 | let trusteeBalance = (await token.balanceOf(trustee.address)).toNumber(); 64 | assert.equal(trusteeBalance, balance); 65 | }); 66 | 67 | it('should be able to update', async () => { 68 | let value = 10; 69 | 70 | await token.issue(trustee.address, value); 71 | let trusteeBalance = (await token.balanceOf(trustee.address)).toNumber(); 72 | assert.equal(trusteeBalance, balance + value); 73 | }); 74 | }); 75 | }); 76 | 77 | describe('grant', async () => { 78 | let balance = 10000; 79 | 80 | context(`with ${balance} tokens assigned to the trustee`, async () => { 81 | beforeEach(async () => { 82 | await token.issue(trustee.address, balance); 83 | }); 84 | 85 | it('should initially have no grants', async () => { 86 | assert.equal((await trustee.totalVesting()).toNumber(), 0); 87 | }); 88 | 89 | it('should not allow granting to 0', async () => { 90 | await expectThrow(trustee.grant(0, 1000, now, now, now + 10 * YEAR, false)); 91 | }); 92 | 93 | it('should not allow granting 0 tokens', async () => { 94 | await expectThrow(trustee.grant(accounts[0], 0, now, now, now + 3 * YEAR, false)); 95 | }); 96 | 97 | it('should not allow granting with a cliff before the start', async () => { 98 | await expectThrow(trustee.grant(accounts[0], 0, now, now - 1, now + 10 * YEAR, false)); 99 | }); 100 | 101 | it('should not allow granting with a cliff after the vesting', async () => { 102 | await expectThrow(trustee.grant(accounts[0], 0, now, now + YEAR, now + MONTH, false)); 103 | }); 104 | 105 | it('should not allow granting tokens more than once', async () => { 106 | await trustee.grant(accounts[0], 1000, now, now, now + 10 * YEAR, false); 107 | 108 | await expectThrow(trustee.grant(accounts[0], 1000, now, now, now + 10 * YEAR, false)); 109 | }); 110 | 111 | it('should not allow granting from not an owner', async () => { 112 | await expectThrow(trustee.grant(accounts[0], 1000, now, now + MONTH, now + YEAR, false, 113 | {from: accounts[1]})); 114 | }); 115 | 116 | it('should not allow granting more than the balance in a single grant', async () => { 117 | await expectThrow(trustee.grant(accounts[0], balance + 1, now, now + MONTH, now + YEAR, false)); 118 | }); 119 | 120 | it('should not allow granting more than the balance in multiple grants', async () => { 121 | await trustee.grant(accounts[0], balance - 10, now, now + MONTH, now + YEAR, false); 122 | await trustee.grant(accounts[1], 7, now, now + MONTH, now + YEAR, false); 123 | await trustee.grant(accounts[2], 3, now, now + 5 * MONTH, now + YEAR, false); 124 | 125 | await expectThrow(trustee.grant(accounts[3], 1, now, now, now + YEAR, false)); 126 | }); 127 | 128 | it('should record a grant and increase grants count and total vesting', async () => { 129 | let totalVesting = (await trustee.totalVesting()).toNumber(); 130 | assert.equal(totalVesting, 0); 131 | 132 | let value = 1000; 133 | let start = now; 134 | let cliff = now + MONTH; 135 | let end = now + YEAR; 136 | await trustee.grant(accounts[0], value, start, cliff, end, false); 137 | 138 | assert.equal((await trustee.totalVesting()).toNumber(), totalVesting + value); 139 | let grant = await getGrant(accounts[0]); 140 | assert.equal(grant.value, value); 141 | assert.equal(grant.start, start); 142 | assert.equal(grant.cliff, cliff); 143 | assert.equal(grant.end, end); 144 | assert.equal(grant.transferred, 0); 145 | assert.equal(grant.revokable, false); 146 | 147 | let value2 = 2300; 148 | let start2 = now + 2 * MONTH; 149 | let cliff2 = now + 6 * MONTH; 150 | let end2 = now + YEAR; 151 | await trustee.grant(accounts[1], value2, start2, cliff2, end2, false); 152 | 153 | assert.equal((await trustee.totalVesting()).toNumber(), totalVesting + value + value2); 154 | let grant2 = await getGrant(accounts[1]); 155 | assert.equal(grant2.value, value2); 156 | assert.equal(grant2.start, start2); 157 | assert.equal(grant2.cliff, cliff2); 158 | assert.equal(grant2.end, end2); 159 | assert.equal(grant2.transferred, 0); 160 | assert.equal(grant2.revokable, false); 161 | }); 162 | }); 163 | }); 164 | 165 | describe('revoke', async () => { 166 | let grantee = accounts[1]; 167 | let notOwner = accounts[9]; 168 | let balance = 100000; 169 | 170 | context(`with ${balance} tokens assigned to the trustee`, async () => { 171 | beforeEach(async () => { 172 | await token.issue(trustee.address, balance); 173 | }); 174 | 175 | it('should throw an error when revoking a non-existing grant', async () => { 176 | await expectThrow(trustee.revoke(accounts[9])); 177 | }); 178 | 179 | it('should not be able to revoke a non-revokable grant', async () => { 180 | await trustee.grant(grantee, balance, now, now + MONTH, now + YEAR, false); 181 | 182 | await expectThrow(trustee.revoke(grantee)); 183 | }); 184 | 185 | it('should only allow revoking a grant by an owner', async () => { 186 | let grantee = accounts[1]; 187 | 188 | await trustee.grant(grantee, balance, now, now + MONTH, now + YEAR, true); 189 | await expectThrow(trustee.revoke(grantee, {from: accounts[9]})); 190 | 191 | await trustee.revoke(grantee, {from: granter}); 192 | }); 193 | 194 | [ 195 | { 196 | tokens: 1000, startOffset: 0, cliffOffset: MONTH, endOffset: YEAR, results: [ 197 | { diff: 0, unlocked: 0 }, 198 | // 1 day before the cliff. 199 | { diff: MONTH - DAY, unlocked: 0 }, 200 | // At the cliff. 201 | { diff: DAY, unlocked: 83 }, 202 | // 1 second after che cliff and previous unlock/withdraw. 203 | { diff: 1, unlocked: 0 }, 204 | // 1 month after the cliff. 205 | { diff: MONTH - 1, unlocked: 83 }, 206 | // At half of the vesting period. 207 | { diff: 4 * MONTH, unlocked: 1000 / 2 - 2 * 83 }, 208 | // At the end of the vesting period. 209 | { diff: 6 * MONTH, unlocked: 1000 / 2 }, 210 | // After the vesting period, with everything already unlocked and withdrawn. 211 | { diff: DAY, unlocked: 0 } 212 | ] 213 | }, 214 | { 215 | tokens: 1000, startOffset: 0, cliffOffset: MONTH, endOffset: YEAR, results: [ 216 | { diff: 0, unlocked: 0 }, 217 | // 1 day after the vesting period. 218 | { diff: YEAR + DAY, unlocked: 1000 }, 219 | // 1 year after the vesting period. 220 | { diff: YEAR - DAY, unlocked: 0 } 221 | ] 222 | }, 223 | { 224 | tokens: 1000000, startOffset: 0, cliffOffset: 0, endOffset: 4 * YEAR, results: [ 225 | { diff: 0, unlocked: 0 }, 226 | { diff: YEAR, unlocked: 1000000 / 4 }, 227 | { diff: YEAR, unlocked: 1000000 / 4 }, 228 | { diff: YEAR, unlocked: 1000000 / 4 }, 229 | { diff: YEAR, unlocked: 1000000 / 4 }, 230 | { diff: YEAR, unlocked: 0 } 231 | ] 232 | } 233 | ].forEach(async (grant) => { 234 | context(`grant: ${grant.tokens}, startOffset: ${grant.startOffset}, cliffOffset: ${grant.cliffOffset}, ` + 235 | `endOffset: ${grant.endOffset}`, async () => { 236 | // We'd allow (up to) 10 tokens vesting error, due to possible timing differences during the tests. 237 | const MAX_ERROR = 10; 238 | 239 | let holder = accounts[1]; 240 | 241 | for (let i = 0; i < grant.results.length; ++i) { 242 | it(`should revoke the grant and refund tokens after ${i + 1} transactions`, async () => { 243 | trustee = await Trustee.new(token.address, {from: granter}); 244 | await token.issue(trustee.address, grant.tokens); 245 | await trustee.grant(holder, grant.tokens, now + grant.startOffset, now + grant.cliffOffset, 246 | now + grant.endOffset, true); 247 | 248 | // Get previous state. 249 | let totalVesting = (await trustee.totalVesting()).toNumber(); 250 | let trusteeBalance = (await token.balanceOf(trustee.address)).toNumber(); 251 | let userBalance = (await token.balanceOf(holder)).toNumber(); 252 | let transferred = (await getGrant(holder)).transferred.toNumber(); 253 | let granterBalance = (await token.balanceOf(granter)).toNumber(); 254 | 255 | let totalUnlocked = 0; 256 | 257 | for (let j = 0; j <= i; ++j) { 258 | let res = grant.results[j]; 259 | 260 | // Jump forward in time by the requested diff. 261 | await time.increaseTime(res.diff); 262 | await trustee.unlockVestedTokens({from: holder}); 263 | 264 | totalUnlocked += res.unlocked; 265 | } 266 | 267 | // Verify the state after the multiple unlocks. 268 | let totalVesting2 = (await trustee.totalVesting()).toNumber(); 269 | let trusteeBalance2 = (await token.balanceOf(trustee.address)).toNumber(); 270 | let userBalance2 = (await token.balanceOf(holder)).toNumber(); 271 | let transferred2 = (await getGrant(holder)).transferred.toNumber(); 272 | 273 | assertHelper.around(totalVesting2, totalVesting - totalUnlocked, MAX_ERROR); 274 | assertHelper.around(trusteeBalance2, trusteeBalance - totalUnlocked, MAX_ERROR); 275 | assertHelper.around(userBalance2, userBalance + totalUnlocked, MAX_ERROR); 276 | assertHelper.around(transferred2, transferred + totalUnlocked, MAX_ERROR); 277 | 278 | let refundTokens = grant.tokens - totalUnlocked; 279 | 280 | console.log(`\texpecting ${refundTokens} tokens refunded after ${i + 1} transactions`); 281 | 282 | let vestingGrant = await getGrant(holder); 283 | assert.equal(vestingGrant.value, grant.tokens); 284 | 285 | await trustee.revoke(holder); 286 | 287 | let totalVesting3 = (await trustee.totalVesting()).toNumber(); 288 | let trusteeBalance3 = (await token.balanceOf(trustee.address)).toNumber(); 289 | let userBalance3 = (await token.balanceOf(holder)).toNumber(); 290 | let granterBalance2 = (await token.balanceOf(granter)).toNumber(); 291 | 292 | assertHelper.around(totalVesting3, totalVesting2 - refundTokens, MAX_ERROR); 293 | assertHelper.around(trusteeBalance3, trusteeBalance2 - refundTokens, MAX_ERROR); 294 | assert.equal(userBalance3, userBalance2); 295 | assertHelper.around(granterBalance2, granterBalance + refundTokens, MAX_ERROR); 296 | 297 | let vestingGrant2 = await getGrant(holder); 298 | assert.equal(vestingGrant2.tokens, undefined); 299 | }); 300 | } 301 | }); 302 | }); 303 | }); 304 | }); 305 | 306 | describe('vestedTokens', async () => { 307 | let balance = 10 ** 12; 308 | 309 | beforeEach(async () => { 310 | await token.issue(trustee.address, balance); 311 | }); 312 | 313 | it('should return 0 for non existing grant', async () => { 314 | let holder = accounts[5]; 315 | let grant = await getGrant(holder); 316 | 317 | assert.equal(grant.value, 0); 318 | assert.equal(grant.start, 0); 319 | assert.equal(grant.cliff, 0); 320 | assert.equal(grant.end, 0); 321 | 322 | assert.equal((await trustee.vestedTokens(holder, now + 100 * YEAR)).toNumber(), 0); 323 | }); 324 | 325 | [ 326 | { 327 | tokens: 1000, startOffset: 0, cliffOffset: MONTH, endOffset: YEAR, results: [ 328 | { offset: 0, vested: 0 }, 329 | { offset: MONTH - 1, vested: 0 }, 330 | { offset: MONTH, vested: Math.floor(1000 / 12) }, 331 | { offset: 2 * MONTH, vested: 2 * Math.floor(1000 / 12) }, 332 | { offset: 0.5 * YEAR, vested: 1000 / 2 }, 333 | { offset: YEAR, vested: 1000 }, 334 | { offset: YEAR + DAY, vested: 1000 } 335 | ] 336 | }, 337 | { 338 | tokens: 10000, startOffset: 0, cliffOffset: 0, endOffset: 4 * YEAR, results: [ 339 | { offset: 0, vested: 0 }, 340 | { offset: MONTH, vested: Math.floor(10000 / 12 / 4) }, 341 | { offset: 0.5 * YEAR, vested: 10000 / 8 }, 342 | { offset: YEAR, vested: 10000 / 4 }, 343 | { offset: 2 * YEAR, vested: 10000 / 2 }, 344 | { offset: 3 * YEAR, vested: 10000 * 0.75 }, 345 | { offset: 4 * YEAR, vested: 10000 }, 346 | { offset: 4 * YEAR + MONTH, vested: 10000 } 347 | ] 348 | }, 349 | { 350 | tokens: 10000, startOffset: 0, cliffOffset: YEAR, endOffset: 4 * YEAR, results: [ 351 | { offset: 0, vested: 0 }, 352 | { offset: MONTH, vested: 0 }, 353 | { offset: 0.5 * YEAR, vested: 0 }, 354 | { offset: YEAR, vested: 10000 / 4 }, 355 | { offset: 2 * YEAR, vested: 10000 / 2 }, 356 | { offset: 3 * YEAR, vested: 10000 * 0.75 }, 357 | { offset: 4 * YEAR, vested: 10000 }, 358 | { offset: 4 * YEAR + MONTH, vested: 10000 } 359 | ] 360 | }, 361 | { 362 | tokens: 100000000, startOffset: 0, cliffOffset: 0, endOffset: 2 * YEAR, results: [ 363 | { offset: 0, vested: 0 }, 364 | { offset: MONTH, vested: Math.floor(100000000 / 12 / 2) }, 365 | { offset: 0.5 * YEAR, vested: 100000000 / 4 }, 366 | { offset: YEAR, vested: 100000000 / 2 }, 367 | { offset: 2 * YEAR, vested: 100000000 }, 368 | { offset: 3 * YEAR, vested: 100000000 } 369 | ] 370 | }, 371 | ].forEach((grant) => { 372 | context(`grant: ${grant.tokens}, startOffset: ${grant.startOffset}, cliffOffset: ${grant.cliffOffset}, ` + 373 | `endOffset: ${grant.endOffset}`, async () => { 374 | 375 | beforeEach(async () => { 376 | await trustee.grant(accounts[2], grant.tokens, now + grant.startOffset, now + grant.cliffOffset, 377 | now + grant.endOffset, false); 378 | }); 379 | 380 | grant.results.forEach(async (res) => { 381 | it(`should vest ${res.vested} out of ${grant.tokens} at time offset ${res.offset}`, async () => { 382 | let result = (await trustee.vestedTokens(accounts[2], now + res.offset)).toNumber(); 383 | assert.equal(result, res.vested); 384 | }); 385 | }); 386 | }); 387 | }); 388 | }); 389 | 390 | describe('unlockVestedTokens', async () => { 391 | // We'd allow (up to) 10 tokens vesting error, due to possible timing differences during the tests. 392 | const MAX_ERROR = 10; 393 | 394 | let balance = 10 ** 12; 395 | 396 | beforeEach(async () => { 397 | await token.issue(trustee.address, balance); 398 | }); 399 | 400 | it('should not allow unlocking a non-existing grant', async () => { 401 | let holder = accounts[5]; 402 | let grant = await getGrant(holder); 403 | 404 | assert.equal(grant.value, 0); 405 | assert.equal(grant.start, 0); 406 | assert.equal(grant.cliff, 0); 407 | assert.equal(grant.end, 0); 408 | 409 | await expectThrow(trustee.unlockVestedTokens({from: holder})); 410 | }); 411 | 412 | it('should not allow unlocking a rovoked grant', async () => { 413 | let grantee = accounts[1]; 414 | 415 | await trustee.grant(grantee, balance, now, now + MONTH, now + YEAR, true); 416 | await trustee.revoke(grantee, {from: granter}); 417 | 418 | await expectThrow(trustee.unlockVestedTokens({from: granter})); 419 | }); 420 | 421 | [ 422 | { 423 | tokens: 1000, startOffset: 0, cliffOffset: MONTH, endOffset: YEAR, results: [ 424 | { diff: 0, unlocked: 0 }, 425 | // 1 day before the cliff. 426 | { diff: MONTH - DAY, unlocked: 0 }, 427 | // At the cliff. 428 | { diff: DAY, unlocked: 83 }, 429 | // 1 second after che cliff and previous unlock/withdraw. 430 | { diff: 1, unlocked: 0 }, 431 | // 1 month after the cliff. 432 | { diff: MONTH - 1, unlocked: 83 }, 433 | // At half of the vesting period. 434 | { diff: 4 * MONTH, unlocked: 1000 / 2 - 2 * 83 }, 435 | // At the end of the vesting period. 436 | { diff: 6 * MONTH, unlocked: 1000 / 2 }, 437 | // After the vesting period, with everything already unlocked and withdrawn. 438 | { diff: DAY, unlocked: 0 } 439 | ] 440 | }, 441 | { 442 | tokens: 1000, startOffset: 0, cliffOffset: MONTH, endOffset: YEAR, results: [ 443 | { diff: 0, unlocked: 0 }, 444 | // 1 day after the vesting period. 445 | { diff: YEAR + DAY, unlocked: 1000 }, 446 | // 1 year after the vesting period. 447 | { diff: YEAR - DAY, unlocked: 0 } 448 | ] 449 | }, 450 | { 451 | tokens: 1000000, startOffset: 0, cliffOffset: 0, endOffset: 4 * YEAR, results: [ 452 | { diff: 0, unlocked: 0 }, 453 | { diff: YEAR, unlocked: 1000000 / 4 }, 454 | { diff: YEAR, unlocked: 1000000 / 4 }, 455 | { diff: YEAR, unlocked: 1000000 / 4 }, 456 | { diff: YEAR, unlocked: 1000000 / 4 }, 457 | { diff: YEAR, unlocked: 0 } 458 | ] 459 | } 460 | ].forEach(async (grant) => { 461 | context(`grant: ${grant.tokens}, startOffset: ${grant.startOffset}, cliffOffset: ${grant.cliffOffset}, ` + 462 | `endOffset: ${grant.endOffset}`, async () => { 463 | 464 | let holder = accounts[1]; 465 | 466 | beforeEach(async () => { 467 | await trustee.grant(holder, grant.tokens, now + grant.startOffset, now + grant.cliffOffset, now + 468 | grant.endOffset, false); 469 | }); 470 | 471 | it('should unlock tokens according to the schedule', async () => { 472 | for (let res of grant.results) { 473 | console.log(`\texpecting ${res.unlocked} tokens unlocked and transferred after another ` + 474 | `${res.diff} seconds`); 475 | 476 | // Get previous state. 477 | let totalVesting = (await trustee.totalVesting()).toNumber(); 478 | let trusteeBalance = (await token.balanceOf(trustee.address)).toNumber(); 479 | let userBalance = (await token.balanceOf(holder)).toNumber(); 480 | let transferred = (await getGrant(holder)).transferred.toNumber(); 481 | 482 | // Jump forward in time by the requested diff. 483 | await time.increaseTime(res.diff); 484 | await trustee.unlockVestedTokens({from: holder}); 485 | 486 | // Verify new state. 487 | let totalVesting2 = (await trustee.totalVesting()).toNumber(); 488 | let trusteeBalance2 = (await token.balanceOf(trustee.address)).toNumber(); 489 | let userBalance2 = (await token.balanceOf(holder)).toNumber(); 490 | let transferred2 = (await getGrant(holder)).transferred.toNumber(); 491 | 492 | assertHelper.around(totalVesting2, totalVesting - res.unlocked, MAX_ERROR); 493 | assertHelper.around(trusteeBalance2, trusteeBalance - res.unlocked, MAX_ERROR); 494 | assertHelper.around(userBalance2, userBalance + res.unlocked, MAX_ERROR); 495 | assertHelper.around(transferred2, transferred + res.unlocked, MAX_ERROR); 496 | } 497 | }); 498 | }); 499 | }); 500 | 501 | it('should allow revoking multiple grants', async () => { 502 | let grants = [ 503 | {tokens: 1000, startOffset: 0, cliffOffset: MONTH, endOffset: YEAR, holder: accounts[1]}, 504 | {tokens: 1000, startOffset: 0, cliffOffset: MONTH, endOffset: YEAR, holder: accounts[2]}, 505 | {tokens: 1000000, startOffset: 0, cliffOffset: 0, endOffset: 4 * YEAR, holder: accounts[3]}, 506 | {tokens: 1245, startOffset: 0, cliffOffset: 0, endOffset: 1 * YEAR, holder: accounts[4]}, 507 | {tokens: 233223, startOffset: 0, cliffOffset: 2 * MONTH, endOffset: 2 * YEAR, holder: accounts[5]} 508 | ]; 509 | 510 | let granterBalance = (await token.balanceOf(granter)).toNumber(); 511 | let trusteeBalance = (await token.balanceOf(trustee.address)).toNumber(); 512 | assert.equal(granterBalance, 0); 513 | assert.equal(trusteeBalance, balance); 514 | 515 | let totalGranted = 0; 516 | 517 | for (let grant of grants) { 518 | await token.issue(trustee.address, grant.tokens); 519 | await trustee.grant(grant.holder, grant.tokens, now + grant.startOffset, now + grant.cliffOffset, now + 520 | grant.endOffset, true); 521 | 522 | totalGranted += grant.tokens; 523 | } 524 | 525 | let granterBalance2 = (await token.balanceOf(granter)).toNumber(); 526 | let trusteeBalance2 = (await token.balanceOf(trustee.address)).toNumber(); 527 | assert.equal(granterBalance2, 0); 528 | assert.equal(trusteeBalance2, trusteeBalance + totalGranted); 529 | 530 | for (let grant of grants) { 531 | await trustee.revoke(grant.holder); 532 | } 533 | 534 | let granterBalance3 = (await token.balanceOf(granter)).toNumber(); 535 | let trusteeBalance3 = (await token.balanceOf(trustee.address)).toNumber(); 536 | assert.equal(granterBalance3, totalGranted); 537 | assert.equal(trusteeBalance3, trusteeBalance2 - totalGranted); 538 | }); 539 | }); 540 | }); 541 | -------------------------------------------------------------------------------- /test/StoxSmartTokenSale.js: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import expectThrow from './helpers/expectThrow'; 3 | import time from './helpers/time'; 4 | 5 | const StoxSmartToken = artifacts.require('../contracts/StoxSmartToken.sol'); 6 | const StoxSmartTokenSaleMock = artifacts.require('./helpers/StoxSmartTokenSaleMock.sol'); 7 | const Trustee = artifacts.require('../contracts/Trustee.sol'); 8 | 9 | contract('StoxSmartTokenSale', (accounts) => { 10 | const MINUTE = 60; 11 | const HOUR = 60 * MINUTE; 12 | const DAY = 24 * HOUR; 13 | const YEAR = 365 * DAY; 14 | 15 | const ETH = Math.pow(10, 18); 16 | const STX = Math.pow(10, 18); 17 | const DEFAULT_GAS_PRICE = 100000000000; 18 | 19 | const ETH_CAP = 148000; 20 | const EXCHANGE_RATE = 200; // 200 STX for ETH 21 | 22 | const PARTNERS = [ 23 | {address: '0x9065260ef6830f6372F1Bde408DeC57Fe3150530', value: 14800000 * STX} 24 | ]; 25 | 26 | let VESTING_GRANTS = [ 27 | {grantee: '0xb54c6a870d4aD65e23d471Fb7941aD271D323f5E', percent: 25, vesting: 1 * YEAR}, 28 | {grantee: '0x4eB4Cd1D125d9d281709Ff38d65b99a6927b46c1', percent: 20, vesting: 2 * YEAR} 29 | ]; 30 | 31 | let STRATEGIC_PARTNERSHIP_GRANT = {address: '0xbC14105ccDdeAadB96Ba8dCE18b40C45b6bACf58', percent: 55}; 32 | 33 | // $30M worth of STX. 34 | const TOKEN_SALE_CAP = new BigNumber(ETH_CAP).mul(EXCHANGE_RATE).mul(STX); 35 | 36 | let setupTokenSale = async (token, sale) => { 37 | await token.transferOwnership(sale.address); 38 | await sale.acceptSmartTokenOwnership(); 39 | }; 40 | 41 | let now; 42 | 43 | let increaseTime = async (by) => { 44 | await time.increaseTime(by); 45 | now = web3.eth.getBlock(web3.eth.blockNumber).timestamp; 46 | }; 47 | 48 | let fundRecipient = accounts[8]; 49 | let token; 50 | 51 | beforeEach(async () => { 52 | now = web3.eth.getBlock(web3.eth.blockNumber).timestamp; 53 | 54 | token = await StoxSmartToken.new(); 55 | }); 56 | 57 | describe('construction', async () => { 58 | it('should be initialized with a valid token address', async () => { 59 | await expectThrow(StoxSmartTokenSaleMock.new(null, fundRecipient, now + 100)); 60 | }); 61 | 62 | it('should be initialized with a valid funding recipient address', async () => { 63 | await expectThrow(StoxSmartTokenSaleMock.new(token.address, null, now + 100)); 64 | }); 65 | 66 | it('should be initialized with a future starting time', async () => { 67 | await expectThrow(StoxSmartTokenSaleMock.new(token.address, fundRecipient, now - 1)); 68 | }); 69 | 70 | it('should be initialized as not finalized', async () => { 71 | let sale = await StoxSmartTokenSaleMock.new(token.address, fundRecipient, now + 100); 72 | assert.equal(await sale.isFinalized(), false); 73 | }); 74 | 75 | it('should be initialized as not distributed', async () => { 76 | let sale = await StoxSmartTokenSaleMock.new(token.address, fundRecipient, now + 100); 77 | assert.equal(await sale.isDistributed(), false); 78 | }); 79 | 80 | it('should be initialized without a trustee', async () => { 81 | let sale = await StoxSmartTokenSaleMock.new(token.address, fundRecipient, now + 100); 82 | await setupTokenSale(token, sale); 83 | 84 | assert.equal(await sale.trustee(), 0); 85 | }); 86 | 87 | it('should be ownable', async () => { 88 | let sale = await StoxSmartTokenSaleMock.new(token.address, fundRecipient, now + 100); 89 | await setupTokenSale(token, sale); 90 | 91 | assert.equal(await sale.owner(), accounts[0]); 92 | }); 93 | 94 | describe('token', async () => { 95 | let sale; 96 | 97 | beforeEach(async () => { 98 | sale = await StoxSmartTokenSaleMock.new(token.address, fundRecipient, now + 100); 99 | await setupTokenSale(token, sale); 100 | 101 | assert.equal(token.address, await sale.stox()); 102 | }); 103 | 104 | it('should own the token', async () => { 105 | assert.equal(await token.owner(), sale.address); 106 | }); 107 | 108 | it('should not be transferable', async () => { 109 | assert.equal(await token.transfersEnabled(), false); 110 | }); 111 | }); 112 | }); 113 | 114 | describe('distributePartnerTokens', async () => { 115 | let sale; 116 | 117 | beforeEach(async () => { 118 | sale = await StoxSmartTokenSaleMock.new(token.address, fundRecipient, now + 100); 119 | }); 120 | 121 | context('with set up token', async() => { 122 | beforeEach(async () => { 123 | await setupTokenSale(token, sale); 124 | }); 125 | 126 | it('should be only possible to call by the owner', async () => { 127 | let notOwner = accounts[8]; 128 | await expectThrow(sale.distributePartnerTokens({from: notOwner})); 129 | }); 130 | 131 | it('should be only possible to called once', async () => { 132 | await sale.distributePartnerTokens(); 133 | await expectThrow(sale.distributePartnerTokens()); 134 | }); 135 | 136 | it('should distribute STX to partners', async () => { 137 | await sale.distributePartnerTokens(); 138 | 139 | let totalPartnersSupply = new BigNumber(0); 140 | 141 | for (let partner of PARTNERS) { 142 | assert.equal((await token.balanceOf(partner.address)).toNumber(), partner.value); 143 | 144 | totalPartnersSupply = totalPartnersSupply.add(partner.value); 145 | } 146 | 147 | assert.equal((await token.totalSupply()).toNumber(), totalPartnersSupply.toNumber()); 148 | assert.equal((await sale.tokensSold()).toNumber(), totalPartnersSupply.toNumber()); 149 | }); 150 | }); 151 | 152 | context('without a set up token', async() => { 153 | it('should throw', async () => { 154 | await expectThrow(sale.distributePartnerTokens()); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('finalize', async () => { 160 | let sale; 161 | let start; 162 | let startFrom = 1000; 163 | let end; 164 | 165 | beforeEach(async () => { 166 | start = now + startFrom; 167 | end = start + 14 * DAY; 168 | sale = await StoxSmartTokenSaleMock.new(token.address, fundRecipient, start); 169 | await setupTokenSale(token, sale); 170 | await sale.distributePartnerTokens(); 171 | }); 172 | 173 | context('before the ending time', async() => { 174 | beforeEach(async () => { 175 | assert(now < end); 176 | }); 177 | 178 | it('should throw', async () => { 179 | await expectThrow(sale.finalize()); 180 | }); 181 | }); 182 | 183 | let testFinalization = async () => { 184 | it('should finalize the token sale', async () => { 185 | assert.equal(await sale.isFinalized(), false); 186 | 187 | await sale.finalize(); 188 | 189 | assert.equal(await sale.isFinalized(), true); 190 | }); 191 | 192 | it('should re-enable the token transfers', async () => { 193 | assert.equal(await token.transfersEnabled(), false); 194 | 195 | await sale.finalize(); 196 | 197 | assert.equal(await token.transfersEnabled(), true); 198 | }); 199 | 200 | it('should not allow to end a token sale when already ended', async () => { 201 | await sale.finalize(); 202 | 203 | await expectThrow(sale.finalize()); 204 | }); 205 | 206 | describe('vesting and grants', async () => { 207 | // We'd allow (up to) 100 seconds of time difference between the execution (i.e., mining) of the 208 | // contract. 209 | const MAX_TIME_ERROR = 100; 210 | 211 | let trustee; 212 | 213 | let getGrant = async (address) => { 214 | let grant = await trustee.grants(address); 215 | 216 | return { 217 | value: grant[0].toNumber(), 218 | start: grant[1].toNumber(), 219 | cliff: grant[2].toNumber(), 220 | end: grant[3].toNumber(), 221 | transferred: grant[4].toNumber(), 222 | revokable: grant[5] 223 | }; 224 | } 225 | 226 | beforeEach(async () => { 227 | let partnershipBalance = (await token.balanceOf(STRATEGIC_PARTNERSHIP_GRANT.address)).toNumber(); 228 | assert.equal(partnershipBalance, 0); 229 | 230 | await sale.finalize(); 231 | 232 | trustee = Trustee.at(await sale.trustee()); 233 | }); 234 | 235 | for (let grant of VESTING_GRANTS) { 236 | it(`should grant ${grant.grantee} ${grant.percent}% over ${grant.vesting}`, async () => { 237 | let tokenGrant = await getGrant(grant.grantee); 238 | 239 | let tokensSold = await sale.tokensSold(); 240 | let granted = tokensSold.mul(grant.percent).div(100).floor(); 241 | 242 | assert.equal(tokenGrant.value, granted.toNumber()); 243 | 244 | assert.equal(tokenGrant.cliff, tokenGrant.start); 245 | assert.equal(tokenGrant.end, tokenGrant.start + grant.vesting); 246 | assert.equal(tokenGrant.transferred, 0); 247 | assert.equal(tokenGrant.revokable, true); 248 | }); 249 | } 250 | 251 | it('should grant the trustee enough tokens to support the grants', async () => { 252 | let tokensSold = await sale.tokensSold(); 253 | let totalGranted = new BigNumber(0); 254 | 255 | for (let grant of VESTING_GRANTS) { 256 | let granted = tokensSold.mul(grant.percent).div(100).floor(); 257 | 258 | totalGranted = totalGranted.add(granted); 259 | } 260 | 261 | let partnerGrant = tokensSold.mul(STRATEGIC_PARTNERSHIP_GRANT.percent).div(100).floor(); 262 | 263 | assert.equal((await token.balanceOf(trustee.address)).toNumber(), totalGranted.toNumber()); 264 | assert.equal(totalGranted.toNumber(), tokensSold.minus(partnerGrant).toNumber()); 265 | }); 266 | 267 | it('should grant strategic partnership grant', async () => { 268 | let tokensSold = await sale.tokensSold(); 269 | 270 | let partnersActualGrant = tokensSold.mul(STRATEGIC_PARTNERSHIP_GRANT.percent).div(100).floor(). 271 | toNumber(); 272 | 273 | let partnershipBalance = (await token.balanceOf(STRATEGIC_PARTNERSHIP_GRANT.address)).toNumber(); 274 | assert.equal(partnershipBalance, partnersActualGrant); 275 | }); 276 | }); 277 | } 278 | 279 | context('after the ending time', async() => { 280 | beforeEach(async () => { 281 | await increaseTime(YEAR); 282 | }); 283 | 284 | context('sold all of the tokens', async() => { 285 | beforeEach(async () => { 286 | await sale.setTokensSold(TOKEN_SALE_CAP.toNumber()); 287 | }); 288 | 289 | testFinalization(); 290 | }); 291 | 292 | context('sold only half of the tokens', async() => { 293 | beforeEach(async () => { 294 | await sale.setTokensSold(TOKEN_SALE_CAP.div(2).toNumber()); 295 | }); 296 | 297 | testFinalization(); 298 | }); 299 | }); 300 | 301 | context('reached token cap', async () => { 302 | beforeEach(async () => { 303 | await sale.setTokensSold(TOKEN_SALE_CAP.toNumber()); 304 | }); 305 | 306 | testFinalization(); 307 | }); 308 | }); 309 | 310 | let verifyTransactions = async (sale, fundRecipient, method, transactions) => { 311 | let totalTokensSold = await sale.tokensSold(); 312 | 313 | let i = 0; 314 | for (let t of transactions) { 315 | let tokens = BigNumber.min(new BigNumber(t.value.toString()).mul(EXCHANGE_RATE), 316 | TOKEN_SALE_CAP.minus(totalTokensSold)); 317 | 318 | let contribution = tokens.div(EXCHANGE_RATE).floor(); 319 | 320 | console.log(`\t[${++i} / ${transactions.length}] expecting account ${t.from} to buy ` + 321 | `${tokens.toNumber() / STX} STX for ${t.value / ETH} ETH`); 322 | 323 | if (tokens == 0) { 324 | await expectThrow(method(sale, t.value, t.from)); 325 | 326 | continue; 327 | } 328 | 329 | let fundRecipientETHBalance = web3.eth.getBalance(fundRecipient); 330 | let participantETHBalance = web3.eth.getBalance(t.from); 331 | let participantSTXBalance = await token.balanceOf(t.from); 332 | 333 | let tokensSold = await sale.tokensSold(); 334 | assert.equal(totalTokensSold.toNumber(), tokensSold.toNumber()); 335 | 336 | // Perform the transaction. 337 | let transaction = await method(sale, t.value, t.from); 338 | let gasUsed = DEFAULT_GAS_PRICE * transaction.receipt.gasUsed; 339 | 340 | let fundRecipientETHBalance2 = web3.eth.getBalance(fundRecipient); 341 | let participantETHBalance2 = web3.eth.getBalance(t.from); 342 | let participantSTXBalance2 = await token.balanceOf(t.from); 343 | 344 | totalTokensSold = totalTokensSold.plus(tokens); 345 | 346 | let tokensSold2 = await sale.tokensSold(); 347 | assert.equal(tokensSold2.toNumber(), tokensSold.plus(tokens).toNumber()); 348 | 349 | assert.equal(fundRecipientETHBalance2.toNumber(), fundRecipientETHBalance.plus(contribution.toString()).toNumber()); 350 | assert.equal(participantETHBalance2.toNumber(), participantETHBalance.minus(contribution.toString()).minus(gasUsed).toNumber()); 351 | assert.equal(participantSTXBalance2.toNumber(), participantSTXBalance.plus(tokens).toNumber()); 352 | 353 | // If the all of the tokens are sold - finalize. 354 | if (totalTokensSold.equals(TOKEN_SALE_CAP)) { 355 | console.log('\tFinalizing sale...'); 356 | 357 | await sale.finalize(); 358 | } 359 | } 360 | }; 361 | 362 | let generateTokenTests = async (name, method) => { 363 | describe(name, async () => { 364 | let sale; 365 | let start; 366 | let startFrom = 1000; 367 | let value = 1000; 368 | 369 | beforeEach(async () => { 370 | start = now + startFrom; 371 | sale = await StoxSmartTokenSaleMock.new(token.address, fundRecipient, start); 372 | await setupTokenSale(token, sale); 373 | await sale.distributePartnerTokens(); 374 | }); 375 | 376 | context('after the ending time', async() => { 377 | beforeEach(async () => { 378 | await increaseTime(YEAR); 379 | }); 380 | 381 | it('should throw if called after the end fo the sale', async () => { 382 | await expectThrow(method(sale, value)); 383 | }); 384 | }); 385 | 386 | context('finalized', async () => { 387 | beforeEach(async () => { 388 | await sale.setFinalized(true); 389 | 390 | assert.equal(await sale.isFinalized(), true); 391 | }); 392 | 393 | it('should not allow to end a token sale when already ended', async () => { 394 | await expectThrow(method(sale, value)); 395 | }); 396 | }); 397 | 398 | context('reached token cap', async () => { 399 | beforeEach(async () => { 400 | await sale.setTokensSold(TOKEN_SALE_CAP.toNumber()); 401 | assert.equal((await sale.tokensSold()).toNumber(), TOKEN_SALE_CAP.toNumber()); 402 | }); 403 | 404 | it('should throw if reached token cap', async () => { 405 | await expectThrow(method(sale, value)); 406 | }); 407 | }); 408 | 409 | context('before the start of the sale', async() => { 410 | beforeEach(async () => { 411 | assert(now < start); 412 | }); 413 | 414 | it('should throw if called before the start fo the sale', async () => { 415 | await expectThrow(method(sale, value)); 416 | }); 417 | }); 418 | 419 | context('during the token sale', async () => { 420 | // Please note that we'd only have (end - start) blocks to run the tests below. 421 | beforeEach(async () => { 422 | await increaseTime(start - now + 1); 423 | }); 424 | 425 | it('should throw if called with 0 ETH', async () => { 426 | await expectThrow(method(sale, 0)); 427 | }); 428 | 429 | it('should throw if have not distributed tokens to pre-sale participants', async () => { 430 | await sale.setDistributed(false); 431 | await expectThrow(method(sale, 1000)); 432 | }); 433 | 434 | [ 435 | [ 436 | { from: accounts[1], value: ETH }, 437 | { from: accounts[1], value: ETH }, 438 | { from: accounts[1], value: ETH }, 439 | { from: accounts[2], value: 150 * ETH } 440 | ], 441 | [ 442 | { from: accounts[1], value: ETH }, 443 | { from: accounts[2], value: 0.9 * ETH }, 444 | { from: accounts[3], value: 200 * ETH }, 445 | { from: accounts[2], value: 50 * ETH }, 446 | { from: accounts[4], value: 0.001 * ETH }, 447 | { from: accounts[5], value: 12.25 * ETH }, 448 | { from: accounts[2], value: 0.11 * ETH }, 449 | { from: accounts[2], value: 15000 * ETH }, 450 | { from: accounts[1], value: 1.01 * ETH } 451 | ], 452 | [ 453 | { from: accounts[1], value: 5 * ETH }, 454 | { from: accounts[2], value: 300 * ETH }, 455 | { from: accounts[2], value: 300 * ETH }, 456 | { from: accounts[2], value: ETH }, 457 | { from: accounts[4], value: 1000 * ETH }, 458 | { from: accounts[5], value: 1.91 * ETH }, 459 | { from: accounts[2], value: 0.1 * ETH }, 460 | { from: accounts[2], value: 600 * ETH }, 461 | { from: accounts[1], value: 0.03 * ETH } 462 | ], 463 | [ 464 | { from: accounts[3], value: TOKEN_SALE_CAP / STX / EXCHANGE_RATE / 4 * ETH }, 465 | { from: accounts[3], value: TOKEN_SALE_CAP / STX / EXCHANGE_RATE / 4 * ETH }, 466 | { from: accounts[3], value: TOKEN_SALE_CAP / STX / EXCHANGE_RATE / 4 * ETH }, 467 | { from: accounts[3], value: TOKEN_SALE_CAP / STX / EXCHANGE_RATE / 4 * ETH } 468 | ], 469 | [ 470 | { from: accounts[3], value: (TOKEN_SALE_CAP / STX / EXCHANGE_RATE * ETH) + 300 * ETH } 471 | ], 472 | [ 473 | { from: accounts[3], value: 10000 * ETH }, 474 | { from: accounts[3], value: (TOKEN_SALE_CAP / STX / EXCHANGE_RATE * ETH) + 300 * ETH } 475 | ] 476 | ].forEach((transactions) => { 477 | context(`${JSON.stringify(transactions).slice(0, 200)}...`, async function() { 478 | // These are long tests, so we need to disable timeouts. 479 | this.timeout(0); 480 | 481 | it('should execute sale orders', async () => { 482 | await verifyTransactions(sale, fundRecipient, method, transactions); 483 | }); 484 | }); 485 | }); 486 | }); 487 | }); 488 | } 489 | 490 | // Generate tests which check the "create" method. 491 | generateTokenTests('using the create function', async (sale, value, from) => { 492 | let account = from || accounts[0]; 493 | return sale.create(account, {value: value, from: account}); 494 | }); 495 | 496 | // Generate tests which check the contract's fallback method. 497 | generateTokenTests('using fallback function', async (sale, value, from) => { 498 | if (from) { 499 | return sale.sendTransaction({value: value, from: from}); 500 | } 501 | 502 | return sale.send(value); 503 | }); 504 | 505 | describe('transfer ownership', async () => { 506 | let sale; 507 | let trustee; 508 | let start; 509 | let startFrom = 1000; 510 | 511 | beforeEach(async () => { 512 | start = now + startFrom; 513 | sale = await StoxSmartTokenSaleMock.new(token.address, fundRecipient, start); 514 | await setupTokenSale(token, sale); 515 | await sale.distributePartnerTokens(); 516 | }); 517 | 518 | let testTransferAndAcceptTokenOwnership = async () => { 519 | let owner = accounts[0]; 520 | let newOwner = accounts[1]; 521 | let notOwner = accounts[8]; 522 | 523 | describe('transferSmartTokenOwnership', async () => { 524 | it('should be only possible to call by the owner', async () => { 525 | await expectThrow(sale.transferSmartTokenOwnership(newOwner, {from: notOwner})); 526 | }); 527 | 528 | it('should transfer ownership', async () => { 529 | assert.equal(await token.owner(), sale.address); 530 | 531 | await sale.transferSmartTokenOwnership(newOwner, {from: owner}); 532 | assert.equal(await token.owner(), sale.address); 533 | 534 | await token.acceptOwnership({from: newOwner}); 535 | assert.equal(await token.owner(), newOwner); 536 | 537 | // Shouldn't be possible to called twice. 538 | await expectThrow(sale.transferSmartTokenOwnership(newOwner, {from: owner})); 539 | }); 540 | }); 541 | 542 | describe('acceptSmartTokenOwnership', async () => { 543 | it('should be only possible to call by the owner', async () => { 544 | await expectThrow(sale.acceptSmartTokenOwnership({from: notOwner})); 545 | }); 546 | 547 | it('should be able to claim ownership back', async () => { 548 | assert.equal(await token.owner(), sale.address); 549 | 550 | await sale.transferSmartTokenOwnership(newOwner, {from: owner}); 551 | await token.acceptOwnership({from: newOwner}); 552 | assert.equal(await token.owner(), newOwner); 553 | 554 | await token.transferOwnership(sale.address, {from: newOwner}); 555 | assert.equal(await token.owner(), newOwner); 556 | 557 | await sale.acceptSmartTokenOwnership({from: owner}); 558 | assert.equal(await token.owner(), sale.address); 559 | }); 560 | }); 561 | }; 562 | 563 | let testTransferAndAcceptTrusteeOwnership = async () => { 564 | let owner = accounts[0]; 565 | let newOwner = accounts[1]; 566 | let notOwner = accounts[8]; 567 | 568 | describe('transferTrusteeOwnership', async () => { 569 | it('should be only possible to call by the owner', async () => { 570 | await expectThrow(sale.transferTrusteeOwnership(newOwner, {from: notOwner})); 571 | }); 572 | 573 | it('should transfer ownership', async () => { 574 | assert.equal(await trustee.owner(), sale.address); 575 | 576 | await sale.transferTrusteeOwnership(newOwner, {from: owner}); 577 | assert.equal(await trustee.owner(), sale.address); 578 | 579 | await trustee.acceptOwnership({from: newOwner}); 580 | assert.equal(await trustee.owner(), newOwner); 581 | 582 | // Shouldn't be possible to called twice. 583 | await expectThrow(sale.transferTrusteeOwnership(newOwner, {from: owner})); 584 | }); 585 | }); 586 | 587 | describe('acceptTrusteeOwnership', async () => { 588 | it('should be only possible to call by the owner', async () => { 589 | await expectThrow(sale.acceptTrusteeOwnership({from: notOwner})); 590 | }); 591 | 592 | it('should be able to claim ownership back', async () => { 593 | assert.equal(await trustee.owner(), sale.address); 594 | 595 | await sale.transferTrusteeOwnership(newOwner, {from: owner}); 596 | await trustee.acceptOwnership({from: newOwner}); 597 | assert.equal(await trustee.owner(), newOwner); 598 | 599 | await trustee.transferOwnership(sale.address, {from: newOwner}); 600 | assert.equal(await trustee.owner(), newOwner); 601 | 602 | await sale.acceptTrusteeOwnership({from: owner}); 603 | assert.equal(await trustee.owner(), sale.address); 604 | }); 605 | }); 606 | 607 | }; 608 | 609 | context('during the sale', async () => { 610 | beforeEach(async () => { 611 | await increaseTime(start - now + 1); 612 | }); 613 | 614 | testTransferAndAcceptTokenOwnership(); 615 | }); 616 | 617 | context('after the sale', async () => { 618 | context('reached token cap', async() => { 619 | beforeEach(async () => { 620 | await sale.setTokensSold(TOKEN_SALE_CAP.toNumber()); 621 | await sale.finalize(); 622 | 623 | trustee = Trustee.at(await sale.trustee()); 624 | }); 625 | 626 | testTransferAndAcceptTokenOwnership(); 627 | testTransferAndAcceptTrusteeOwnership(); 628 | }); 629 | 630 | context('after the ending time', async() => { 631 | beforeEach(async () => { 632 | await increaseTime(YEAR); 633 | await sale.finalize(); 634 | 635 | trustee = Trustee.at(await sale.trustee()); 636 | }); 637 | 638 | testTransferAndAcceptTokenOwnership(); 639 | testTransferAndAcceptTrusteeOwnership(); 640 | }); 641 | }); 642 | }); 643 | }); 644 | -------------------------------------------------------------------------------- /test/MultiSigWallet.js: -------------------------------------------------------------------------------- 1 | import expectThrow from './helpers/expectThrow'; 2 | import coder from 'web3/packages/web3-eth-abi/src/index.js'; 3 | 4 | const StoxSmartToken = artifacts.require('../contracts/StoxSmartToken.sol'); 5 | const MultiSigWalletMock = artifacts.require('./heplers/MultiSigWalletMock.sol'); 6 | 7 | contract('MultiSigWallet', (accounts) => { 8 | const MAX_OWNER_COUNT = 50; 9 | const DEFAULT_GAS_PRICE = 100000000000; 10 | 11 | const ERC20_TRANSFER_ABI = { 12 | name: 'transfer', 13 | type: 'function', 14 | inputs: [{ 15 | type: 'address', 16 | name: 'to' 17 | }, 18 | { 19 | type: 'uint256', 20 | name: 'value' 21 | }] 22 | }; 23 | 24 | const MULTISIGWALLET_ABI = { 25 | addOwner: { 26 | name: 'addOwner', 27 | type: 'function', 28 | inputs: [{ 29 | type: 'address', 30 | name: 'owner' 31 | }] 32 | }, 33 | removeOwner: { 34 | name: 'removeOwner', 35 | type: 'function', 36 | inputs: [{ 37 | type: 'address', 38 | name: 'owner' 39 | }] 40 | }, 41 | replaceOwner: { 42 | name: 'replaceOwner', 43 | type: 'function', 44 | inputs: [{ 45 | type: 'address', 46 | name: 'owner' 47 | }, { 48 | type: 'address', 49 | name: 'newOwner' 50 | }] 51 | }, 52 | changeRequirement: { 53 | name: 'changeRequirement', 54 | type: 'function', 55 | inputs: [{ 56 | type: 'uint256', 57 | name: 'required' 58 | }] 59 | } 60 | }; 61 | 62 | describe('construction', async () => { 63 | context('error', async () => { 64 | it(`should throw if created with more than ${MAX_OWNER_COUNT} owners`, async () => { 65 | let owners = []; 66 | for (let i = 0; i < MAX_OWNER_COUNT + 1; ++i) { 67 | owners.push(i + 1); 68 | } 69 | 70 | await expectThrow(MultiSigWalletMock.new(owners, 2)); 71 | }); 72 | 73 | it('should throw if created without any owners', async () => { 74 | await expectThrow(MultiSigWalletMock.new([], 2)); 75 | }); 76 | 77 | it('should throw if created without any requirements', async () => { 78 | await expectThrow(MultiSigWalletMock.new([accounts[0], accounts[1]], 0)); 79 | }); 80 | 81 | it('should throw if created with a requirement larger than the number of owners', async () => { 82 | await expectThrow(MultiSigWalletMock.new([accounts[0], accounts[1], accounts[2]], 10)); 83 | }); 84 | 85 | it('should throw if created with duplicate owners', async () => { 86 | await expectThrow(MultiSigWalletMock.new([accounts[0], accounts[1], accounts[2], accounts[1]], 3)); 87 | }); 88 | }); 89 | 90 | context('success', async () => { 91 | let owners = [accounts[0], accounts[1], accounts[2]]; 92 | let requirement = 2; 93 | 94 | it('should be initialized with 0 balance', async () => { 95 | let wallet = await MultiSigWalletMock.new(owners, requirement); 96 | 97 | assert.equal(web3.eth.getBalance(wallet.address), 0); 98 | }); 99 | 100 | it('should initialize owners', async () => { 101 | let wallet = await MultiSigWalletMock.new(owners, requirement); 102 | 103 | assert.deepEqual(owners.sort(), (await wallet.getOwners()).sort()); 104 | }); 105 | 106 | it('should initialize owners\' mapping', async () => { 107 | let wallet = await MultiSigWalletMock.new(owners, requirement); 108 | 109 | for (let owner of owners) { 110 | assert.equal(await wallet.isOwner(owner), true); 111 | } 112 | 113 | assert.equal(await wallet.isOwner(accounts[9]), false); 114 | }); 115 | 116 | it('should initialize requirement', async () => { 117 | let wallet = await MultiSigWalletMock.new(owners, requirement); 118 | 119 | assert.equal(requirement, (await wallet.required()).toNumber()); 120 | }); 121 | 122 | it('should initialize with empty transaction count', async () => { 123 | let wallet = await MultiSigWalletMock.new(owners, requirement); 124 | 125 | assert.equal((await wallet.transactionCount()).toNumber(), 0); 126 | }); 127 | }); 128 | }); 129 | 130 | describe('fallback function', async () => { 131 | let owners = [accounts[0], accounts[1], accounts[2]]; 132 | let requirement = 2; 133 | let wallet; 134 | let sender = accounts[3]; 135 | 136 | beforeEach(async () => { 137 | wallet = await MultiSigWalletMock.new(owners, requirement); 138 | }); 139 | 140 | it('should receive ETH', async () => { 141 | let senderBalance = web3.eth.getBalance(sender); 142 | let walletBalance = web3.eth.getBalance(wallet.address); 143 | assert.equal(walletBalance.toNumber(), 0); 144 | 145 | let value = 10000; 146 | let transaction = await wallet.sendTransaction({from: sender, value: value}); 147 | let gasUsed = DEFAULT_GAS_PRICE * transaction.receipt.gasUsed; 148 | 149 | let senderBalance2 = web3.eth.getBalance(sender); 150 | assert.equal(senderBalance2.toNumber(), senderBalance.minus(value).minus(gasUsed).toNumber()); 151 | 152 | let walletBalance2 = web3.eth.getBalance(wallet.address); 153 | assert.equal(walletBalance2.toNumber(), walletBalance.plus(value).toNumber()); 154 | }); 155 | 156 | it('should receive STX', async () => { 157 | let token = await StoxSmartToken.new(); 158 | await token.disableTransfers(false); 159 | 160 | let value = 200; 161 | await token.issue(sender, value); 162 | 163 | let senderBalance = await token.balanceOf(sender); 164 | let walletBalance = await token.balanceOf(wallet.address); 165 | assert.equal(senderBalance.toNumber(), value); 166 | assert.equal(walletBalance.toNumber(), 0); 167 | 168 | await token.transfer(wallet.address, value, {from: sender}); 169 | 170 | let senderBalance2 = await token.balanceOf(sender); 171 | assert.equal(senderBalance2.toNumber(), senderBalance.minus(value).toNumber()); 172 | 173 | let walletBalance2 = await token.balanceOf(wallet.address); 174 | assert.equal(walletBalance2.toNumber(), walletBalance.plus(value).toNumber()); 175 | }); 176 | }); 177 | 178 | describe('transaction submission and confirmation', async () => { 179 | [ 180 | { owners: [accounts[1], accounts[2]], requirement: 1 }, 181 | { owners: [accounts[1], accounts[2]], requirement: 2 }, 182 | { owners: [accounts[1], accounts[2], accounts[3]], requirement: 2 }, 183 | { owners: [accounts[1], accounts[2], accounts[3]], requirement: 3 }, 184 | { owners: [accounts[1], accounts[2], accounts[3], accounts[4]], requirement: 1 }, 185 | { owners: [accounts[1], accounts[2], accounts[3], accounts[4]], requirement: 2 }, 186 | { owners: [accounts[1], accounts[2], accounts[3], accounts[4]], requirement: 3 }, 187 | { owners: [accounts[1], accounts[2], accounts[3], accounts[4]], requirement: 4 }, 188 | { owners: [accounts[1], accounts[2], accounts[3], accounts[4], accounts[5]], requirement: 3 } 189 | ].forEach((spec) => { 190 | context(`with ${spec.owners.length} owners and requirement of ${spec.requirement}`, async () => { 191 | let wallet; 192 | let token; 193 | let initETHBalance = 10000; 194 | let initSTXBalance = 12345678; 195 | let value = 234; 196 | let sender = spec.owners[0]; 197 | let notOwner = accounts[8]; 198 | let receiver = accounts[9]; 199 | 200 | beforeEach(async () => { 201 | wallet = await MultiSigWalletMock.new(spec.owners, spec.requirement); 202 | await wallet.sendTransaction({value: initETHBalance}); 203 | assert.equal(web3.eth.getBalance(wallet.address).toNumber(), initETHBalance); 204 | 205 | token = await StoxSmartToken.new(); 206 | await token.disableTransfers(false); 207 | 208 | await token.issue(wallet.address, initSTXBalance); 209 | assert.equal((await token.balanceOf(wallet.address)).toNumber(), initSTXBalance); 210 | }); 211 | 212 | describe('submitTransaction', async() => { 213 | it('should throw an error, if sent from not an owner', async () => { 214 | await expectThrow(wallet.submitTransaction(receiver, value, [], {from: notOwner})); 215 | }); 216 | 217 | it('should throw an error, if sent to a 0 address', async () => { 218 | await expectThrow(wallet.submitTransaction(null, value, [], {from: sender})); 219 | }); 220 | }); 221 | 222 | describe('confirmTransaction', async() => { 223 | it('should throw an error, if confirming the same transaction after submitting it', async () => { 224 | await wallet.submitTransaction(receiver, value, [], {from: sender}); 225 | 226 | let transactionId = await wallet.transactionId(); 227 | await expectThrow(wallet.confirmTransaction(transactionId, {from: sender})); 228 | }); 229 | 230 | if (spec.requirement > 1) { 231 | it('should throw an error, if sent from not an owner', async () => { 232 | await wallet.submitTransaction(receiver, value, [], {from: sender}); 233 | let transactionId = await wallet.transactionId(); 234 | 235 | await expectThrow(wallet.confirmTransaction(transactionId, {from: notOwner})); 236 | }); 237 | 238 | it('should throw an error, if confirming the same transaction twice', async () => { 239 | await wallet.submitTransaction(receiver, value, [], {from: sender}); 240 | let transactionId = await wallet.transactionId(); 241 | 242 | let confirmer = spec.owners[1]; 243 | await wallet.confirmTransaction(transactionId, {from: confirmer}); 244 | 245 | await expectThrow(wallet.confirmTransaction(transactionId, {from: confirmer})); 246 | }); 247 | } 248 | 249 | it('should throw an error, if confirming a non-existing transaction', async () => { 250 | await expectThrow(wallet.confirmTransaction(12345, {from: spec.owners[0]})); 251 | }); 252 | }); 253 | 254 | describe('revokeConfirmation', async () => { 255 | if (spec.requirement > 1) { 256 | it('should throw an error, if sent from not an owner', async () => { 257 | await wallet.submitTransaction(receiver, value, [], {from: sender}); 258 | let transactionId = await wallet.transactionId(); 259 | 260 | let confirmer = spec.owners[1]; 261 | await wallet.confirmTransaction(transactionId, {from: confirmer}); 262 | 263 | await expectThrow(wallet.revokeConfirmation(transactionId, {from: notOwner})); 264 | }); 265 | 266 | it('should throw an error, if asked to revoke a non-confirmed transaction', async () => { 267 | await wallet.submitTransaction(receiver, value, [], {from: sender}); 268 | let transactionId = await wallet.transactionId(); 269 | 270 | await expectThrow(wallet.revokeConfirmation(transactionId, {from: spec.owners[1]})); 271 | }); 272 | } 273 | 274 | if (spec.requirement > 2) { 275 | it('should revoke a confirmation', async () => { 276 | await wallet.submitTransaction(receiver, value, [], {from: sender}); 277 | let transactionId = await wallet.transactionId(); 278 | 279 | let confirmer = spec.owners[1]; 280 | await wallet.confirmTransaction(transactionId, {from: confirmer}); 281 | assert.equal(await wallet.getConfirmationCount(transactionId), 2); 282 | 283 | await wallet.revokeConfirmation(transactionId, {from: confirmer}); 284 | assert.equal(await wallet.getConfirmationCount(transactionId), 1); 285 | }); 286 | } 287 | 288 | it('should throw an error, if asked to revoke an executed transaction', async () => { 289 | await wallet.submitTransaction(receiver, value, [], {from: sender}); 290 | let transactionId = await wallet.transactionId(); 291 | 292 | let confirmations = 1; 293 | for (let i = 1; i < spec.owners.length && confirmations < spec.requirement; i++) { 294 | await wallet.confirmTransaction(transactionId, {from: spec.owners[i]}); 295 | confirmations++; 296 | } 297 | 298 | await expectThrow(wallet.revokeConfirmation(transactionId, {from: sender})); 299 | }); 300 | }); 301 | 302 | let getBalance = async (address, coin) => { 303 | switch (coin) { 304 | case 'ETH': 305 | return web3.eth.getBalance(address); 306 | 307 | case 'STX': 308 | return await token.balanceOf(address); 309 | 310 | default: 311 | throw new Error(`Invalid type: ${type}!`); 312 | } 313 | } 314 | 315 | let submitTransaction = async (receiver, value, from, coin) => { 316 | switch (coin) { 317 | case 'ETH': 318 | return await wallet.submitTransaction(receiver, value, [], {from: from}); 319 | 320 | case 'STX': 321 | let params = [receiver, value]; 322 | let encoded = coder.encodeFunctionCall(ERC20_TRANSFER_ABI, params); 323 | 324 | return await wallet.submitTransaction(token.address, 0, encoded, {from: from}); 325 | 326 | default: 327 | throw new Error(`Invalid type: ${type}!`); 328 | } 329 | } 330 | 331 | [ 332 | 'ETH', 333 | 'STX' 334 | ].forEach((coin) => { 335 | it(`should only send ${coin} when all confirmations were received`, async () => { 336 | let transaction = submitTransaction(receiver, value, spec.owners[0], coin); 337 | let transactionId = await wallet.transactionId(); 338 | 339 | let confirmations = 1; 340 | 341 | for (let i = 1; i < spec.owners.length; i++) { 342 | let confirmer = spec.owners[i]; 343 | 344 | let prevWalletBalanace = await getBalance(wallet.address, coin); 345 | let prevReceiverBalance = await getBalance(receiver, coin); 346 | 347 | // If this is not the final confirmation - don't expect any change. 348 | if (confirmations < spec.requirement) { 349 | assert.equal(await wallet.isConfirmed(transactionId), false); 350 | 351 | await wallet.confirmTransaction(transactionId, {from: confirmer}); 352 | confirmations++; 353 | assert.equal((await wallet.getConfirmationCount(transactionId)).toNumber(), 354 | confirmations); 355 | 356 | // Should throw an error if trying to confirm the same transaction twice. 357 | await expectThrow(wallet.confirmTransaction(transactionId, {from: confirmer})); 358 | 359 | let walletBalanace = await getBalance(wallet.address, coin); 360 | let receiverBalance = await getBalance(receiver, coin); 361 | 362 | if (confirmations == spec.requirement) { 363 | assert.equal(await wallet.isConfirmed(transactionId), true); 364 | 365 | assert.equal(walletBalanace.toNumber(), prevWalletBalanace.minus(value).toNumber()); 366 | assert.equal(receiverBalance.toNumber(), prevReceiverBalance.plus(value).toNumber()); 367 | } else { 368 | assert.equal(await wallet.isConfirmed(transactionId), false); 369 | 370 | assert.equal(walletBalanace.toNumber(), prevWalletBalanace.toNumber()); 371 | assert.equal(receiverBalance.toNumber(), prevReceiverBalance.toNumber()); 372 | } 373 | } else { 374 | assert.equal(await wallet.isConfirmed(transactionId), true); 375 | 376 | // Should throw an error if trying to confirm an already executed transaction. 377 | await expectThrow(wallet.confirmTransaction(transactionId, {from: confirmer})); 378 | 379 | let walletBalanace = await getBalance(wallet.address, coin); 380 | let receiverBalance = await getBalance(receiver, coin); 381 | 382 | assert.equal(walletBalanace.toNumber(), prevWalletBalanace.toNumber()); 383 | assert.equal(receiverBalance.toNumber(), prevReceiverBalance.toNumber()); 384 | } 385 | } 386 | }); 387 | }); 388 | }); 389 | }); 390 | }); 391 | 392 | describe('internal methods', async () => { 393 | let wallet; 394 | 395 | [ 396 | { owners: [accounts[1], accounts[2]], requirement: 1 }, 397 | { owners: [accounts[1], accounts[2]], requirement: 2 }, 398 | { owners: [accounts[1], accounts[2], accounts[3]], requirement: 2 }, 399 | { owners: [accounts[1], accounts[2], accounts[3]], requirement: 3 }, 400 | { owners: [accounts[1], accounts[2], accounts[3], accounts[4]], requirement: 1 }, 401 | { owners: [accounts[1], accounts[2], accounts[3], accounts[4]], requirement: 2 }, 402 | { owners: [accounts[1], accounts[2], accounts[3], accounts[4]], requirement: 3 }, 403 | { owners: [accounts[1], accounts[2], accounts[3], accounts[4]], requirement: 4 }, 404 | { owners: [accounts[1], accounts[2], accounts[3], accounts[4], accounts[5]], requirement: 3 } 405 | ].forEach((spec) => { 406 | context(`with ${spec.owners.length} owners and requirement of ${spec.requirement}`, async () => { 407 | let wallet; 408 | let notOwner = accounts[8]; 409 | let notOwner2 = accounts[9]; 410 | 411 | beforeEach(async () => { 412 | wallet = await MultiSigWalletMock.new(spec.owners, spec.requirement); 413 | }); 414 | 415 | describe('addOwner', async () => { 416 | let addOwner = async (owner, from) => { 417 | let params = [owner]; 418 | let encoded = coder.encodeFunctionCall(MULTISIGWALLET_ABI.addOwner, params); 419 | 420 | let transaction = await wallet.submitTransaction(wallet.address, 0, encoded, {from: from}); 421 | let transactionId = await wallet.transactionId(); 422 | 423 | let confirmations = 1; 424 | 425 | for (let i = 1; i < spec.owners.length; i++) { 426 | let confirmer = spec.owners[i]; 427 | 428 | // If this is not the final confirmation - confirm. 429 | if (confirmations < spec.requirement) { 430 | transaction = await wallet.confirmTransaction(transactionId, {from: confirmer}); 431 | confirmations++; 432 | } 433 | } 434 | 435 | for (let log of transaction.logs) { 436 | if (log.event === 'ExecutionFailure') { 437 | throw new Error('invalid opcode'); 438 | } 439 | } 440 | }; 441 | 442 | it('should throw an error, if called directly', async () => { 443 | await expectThrow(wallet.addOwner(notOwner, {from: spec.owners[0]})); 444 | }); 445 | 446 | it('should throw an error, if called by not an owner', async () => { 447 | await expectThrow(addOwner(notOwner2, notOwner)); 448 | }); 449 | 450 | it('should throw an error, if adding an empty owner', async () => { 451 | await expectThrow(addOwner('0000000000000000000000000000000000000000', spec.owners[0])); 452 | }); 453 | 454 | it('should throw an error, if adding an existing owner', async () => { 455 | await expectThrow(addOwner(spec.owners[1], spec.owners[0])); 456 | }); 457 | 458 | it('should add an owner', async () => { 459 | assert.equal(await wallet.isOwner(notOwner), false); 460 | 461 | await addOwner(notOwner, spec.owners[0]); 462 | 463 | assert.equal(await wallet.isOwner(notOwner), true); 464 | }); 465 | }); 466 | 467 | describe('removeOwner', async () => { 468 | let removeOwner = async (owner, from) => { 469 | let params = [owner]; 470 | let encoded = coder.encodeFunctionCall(MULTISIGWALLET_ABI.removeOwner, params); 471 | 472 | let transaction = await wallet.submitTransaction(wallet.address, 0, encoded, {from: from}); 473 | let transactionId = await wallet.transactionId(); 474 | 475 | let confirmations = 1; 476 | 477 | for (let i = 1; i < spec.owners.length; i++) { 478 | let confirmer = spec.owners[i]; 479 | 480 | // If this is not the final confirmation - confirm. 481 | if (confirmations < spec.requirement) { 482 | transaction = await wallet.confirmTransaction(transactionId, {from: confirmer}); 483 | confirmations++; 484 | } 485 | } 486 | 487 | for (let log of transaction.logs) { 488 | if (log.event === 'ExecutionFailure') { 489 | throw new Error('invalid opcode'); 490 | } 491 | } 492 | }; 493 | 494 | it('should throw an error, if called directly', async () => { 495 | await expectThrow(wallet.removeOwner(spec.owners[0], {from: spec.owners[0]})); 496 | }); 497 | 498 | it('should throw an error, if called by not an owner', async () => { 499 | await expectThrow(removeOwner(spec.owners[0], notOwner)); 500 | }); 501 | 502 | it('should throw an error, if removing a non-existing owner', async () => { 503 | await expectThrow(removeOwner(notOwner, spec.owners[0])); 504 | }); 505 | 506 | it('should remove an owner', async () => { 507 | let owner = spec.owners[1]; 508 | let requirement = (await wallet.required()).toNumber(); 509 | 510 | assert.equal(await wallet.isOwner(owner), true); 511 | 512 | await removeOwner(owner, spec.owners[0]); 513 | 514 | let newRequirement = (await wallet.required()).toNumber(); 515 | if (spec.requirement > spec.owners.length - 1) { 516 | assert.equal(newRequirement, requirement - 1); 517 | } else { 518 | assert.equal(newRequirement, requirement); 519 | } 520 | 521 | assert.equal(await wallet.isOwner(owner), false); 522 | }); 523 | }); 524 | 525 | describe('replaceOwner', async () => { 526 | let replaceOwner = async (owner, newOwner, from) => { 527 | let params = [owner, newOwner]; 528 | let encoded = coder.encodeFunctionCall(MULTISIGWALLET_ABI.replaceOwner, params); 529 | 530 | let transaction = await wallet.submitTransaction(wallet.address, 0, encoded, {from: from}); 531 | let transactionId = await wallet.transactionId(); 532 | 533 | let confirmations = 1; 534 | 535 | for (let i = 1; i < spec.owners.length; i++) { 536 | let confirmer = spec.owners[i]; 537 | 538 | // If this is not the final confirmation - confirm. 539 | if (confirmations < spec.requirement) { 540 | transaction = await wallet.confirmTransaction(transactionId, {from: confirmer}); 541 | confirmations++; 542 | } 543 | } 544 | 545 | for (let log of transaction.logs) { 546 | if (log.event === 'ExecutionFailure') { 547 | throw new Error('invalid opcode'); 548 | } 549 | } 550 | }; 551 | 552 | it('should throw an error, if called directly', async () => { 553 | await expectThrow(wallet.replaceOwner(spec.owners[0], spec.owners[1], {from: spec.owners[0]})); 554 | }); 555 | 556 | it('should throw an error, if called by not an owner', async () => { 557 | await expectThrow(replaceOwner(spec.owners[0], spec.owners[1], notOwner)); 558 | }); 559 | 560 | it('should throw an error, if replacing a non-existing owner', async () => { 561 | await expectThrow(replaceOwner(notOwner, spec.owners[1], spec.owners[0])); 562 | }); 563 | 564 | it('should replace an owner', async () => { 565 | let owner = spec.owners[1]; 566 | let requirement = (await wallet.required()).toNumber(); 567 | 568 | assert.equal(await wallet.isOwner(owner), true); 569 | assert.equal(await wallet.isOwner(notOwner), false); 570 | 571 | await replaceOwner(owner, notOwner, spec.owners[0]); 572 | 573 | assert.equal(await wallet.isOwner(owner), false); 574 | assert.equal(await wallet.isOwner(notOwner), true); 575 | }); 576 | }); 577 | 578 | describe('changeRequirement', async () => { 579 | let changeRequirement = async (requirement, from) => { 580 | let params = [requirement]; 581 | let encoded = coder.encodeFunctionCall(MULTISIGWALLET_ABI.changeRequirement, params); 582 | 583 | let transaction = await wallet.submitTransaction(wallet.address, 0, encoded, {from: from}); 584 | let transactionId = await wallet.transactionId(); 585 | 586 | let confirmations = 1; 587 | 588 | for (let i = 1; i < spec.owners.length; i++) { 589 | let confirmer = spec.owners[i]; 590 | 591 | // If this is not the final confirmation - confirm. 592 | if (confirmations < spec.requirement) { 593 | transaction = await wallet.confirmTransaction(transactionId, {from: confirmer}); 594 | confirmations++; 595 | } 596 | } 597 | 598 | for (let log of transaction.logs) { 599 | if (log.event === 'ExecutionFailure') { 600 | throw new Error('invalid opcode'); 601 | } 602 | } 603 | }; 604 | 605 | it('should throw an error, if called directly', async () => { 606 | let requirement = spec.requirement == 1 ? 2 : spec.requirement - 1; 607 | await expectThrow(wallet.changeRequirement(requirement, {from: spec.owners[0]})); 608 | }); 609 | 610 | it('should throw an error, if called by not an owner', async () => { 611 | let requirement = spec.requirement == 1 ? 2 : spec.requirement - 1; 612 | await expectThrow(changeRequirement(requirement, notOwner)); 613 | }); 614 | 615 | if (spec.requirement < spec.owners.length) { 616 | it('should increase requirement by 1', async () => { 617 | let requirement = (await wallet.required()).toNumber(); 618 | assert.equal(requirement, spec.requirement); 619 | 620 | await changeRequirement(spec.requirement + 1, spec.owners[0]); 621 | 622 | requirement = (await wallet.required()).toNumber(); 623 | assert.equal(requirement, spec.requirement + 1); 624 | }); 625 | } else { 626 | it('should decrease requirement by 1', async () => { 627 | let requirement = (await wallet.required()).toNumber(); 628 | assert.equal(requirement, spec.requirement); 629 | 630 | await changeRequirement(spec.requirement - 1, spec.owners[0]); 631 | 632 | requirement = (await wallet.required()).toNumber(); 633 | assert.equal(requirement, spec.requirement - 1); 634 | }); 635 | } 636 | }); 637 | }); 638 | }); 639 | }); 640 | }); 641 | --------------------------------------------------------------------------------