├── .eslintrc.json ├── .gitignore ├── conf ├── token.json ├── sale.json ├── timelocks.json └── preBuyers.json ├── migrations ├── 1_initial_migration.js └── 2_deploy_sale.js ├── ethpm.json ├── contracts ├── Migrations.sol ├── Disbursement.sol └── Sale.sol ├── package.json ├── truffle.js ├── test ├── pre_sale_period.js ├── instantiation.js ├── initial_issuance.js ├── sale_start.js ├── emergency_stop.js ├── sale_end.js ├── utils.js ├── post_sale_period.js ├── disbursers.js └── owner_only.js └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | secrets.json 3 | node_modules 4 | installed_contracts/ 5 | logs/ 6 | -------------------------------------------------------------------------------- /conf/token.json: -------------------------------------------------------------------------------- 1 | { 2 | "initialAmount": "1000000000000000000", 3 | "tokenName": "MyToken", 4 | "decimalUnits": "9", 5 | "tokenSymbol": "MYT" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | /* global artifacts */ 2 | 3 | const Migrations = artifacts.require('./Migrations.sol'); 4 | 5 | module.exports = (deployer) => { 6 | deployer.deploy(Migrations); 7 | }; 8 | -------------------------------------------------------------------------------- /conf/sale.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "0x627306090abab3a6e1400e9345bc60c78a8bef57", 3 | "wallet": "0x3Ebe70Ed8122cF0EE7c1Ae1dAc5A7362DC240Ec0", 4 | "price": "100", 5 | "startBlock": "500", 6 | "freezeBlock": "420" 7 | } 8 | 9 | -------------------------------------------------------------------------------- /ethpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "package_name": "Simple Token Sale", 3 | "version": "0.1.0", 4 | "description": "A fixed-price, finite-supply token sale", 5 | "authors": [ 6 | "Mike Goldin" 7 | ], 8 | "keywords": [ 9 | "tokens", 10 | "consensys" 11 | ], 12 | "dependencies": { 13 | "tokens": "1.0.0" 14 | }, 15 | "license": "Apache 2.0" 16 | } 17 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.4; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | modifier restricted() { 8 | if (msg.sender == owner) _; 9 | } 10 | 11 | function Migrations() public { 12 | owner = msg.sender; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tokenlaunch", 3 | "version": "0.0.1", 4 | "description": "Contracts for fixed-price, finite-supply token sales.", 5 | "scripts": { 6 | "install": "truffle install", 7 | "test": "eslint ./ && truffle test", 8 | "fix": "eslint --fix", 9 | "lint": "eslint", 10 | "compile": "truffle compile" 11 | }, 12 | "author": "Mike Goldin ", 13 | "license": "ISC", 14 | "dependencies": { 15 | "bn.js": "4.11.6", 16 | "ethjs-provider-http": "0.1.6", 17 | "ethjs-query": "0.2.4", 18 | "ethjs-rpc": "0.1.5", 19 | "truffle": "4.0.6", 20 | "truffle-hdwallet-provider": "0.0.3" 21 | }, 22 | "devDependencies": { 23 | "eslint": "4.11.0", 24 | "eslint-config-airbnb-base": "12.1.0", 25 | "eslint-plugin-import": "2.8.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('truffle-hdwallet-provider'); 2 | const fs = require('fs'); 3 | 4 | // first read in the secrets.json to get our mnemonic 5 | let secrets; 6 | let mnemonic; 7 | if (fs.existsSync('secrets.json')) { 8 | secrets = JSON.parse(fs.readFileSync('secrets.json', 'utf8')); 9 | ({ mnemonic } = secrets); 10 | } else { 11 | console.log('no secrets.json found. You can only deploy to the testrpc.'); 12 | mnemonic = ''; 13 | } 14 | 15 | module.exports = { 16 | networks: { 17 | kovan: { 18 | provider: new HDWalletProvider(mnemonic, 'https://kovan.infura.io'), 19 | network_id: '*', 20 | gas: 4500000, 21 | gasPrice: 25000000000, 22 | }, 23 | rinkeby: { 24 | provider: new HDWalletProvider(mnemonic, 'https://rinkeby.infura.io'), 25 | network_id: '*', 26 | gas: 4500000, 27 | gasPrice: 25000000000, 28 | }, 29 | mainnet: { 30 | provider: new HDWalletProvider(mnemonic, 'https://mainnet.infura.io'), 31 | network_id: 1, 32 | gas: 4500000, 33 | gasPrice: 4000000000, 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /test/pre_sale_period.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert contract */ 3 | 4 | const fs = require('fs'); 5 | const BN = require('bn.js'); 6 | const utils = require('./utils'); 7 | 8 | const saleConf = JSON.parse(fs.readFileSync('./conf/sale.json')); 9 | const tokenConf = JSON.parse(fs.readFileSync('./conf/token.json')); 10 | 11 | contract('Sale', (accounts) => { 12 | const [, james] = accounts; 13 | 14 | describe('Pre-sale period', () => { 15 | const earlyPurchaseError = ' was able to purchase tokens early'; 16 | 17 | before(() => { 18 | saleConf.price = new BN(saleConf.price, 10); 19 | saleConf.startBlock = new BN(saleConf.startBlock, 10); 20 | tokenConf.initialAmount = new BN(tokenConf.initialAmount, 10); 21 | }); 22 | 23 | it('should reject a purchase from James.', async () => { 24 | const startingBalance = await utils.getTokenBalanceOf(james); 25 | try { 26 | await utils.purchaseTokens(james, new BN('420', 10)); 27 | const errMsg = james + earlyPurchaseError; 28 | assert(false, errMsg); 29 | } catch (err) { 30 | const errMsg = err.toString(); 31 | assert(utils.isEVMRevert(err), errMsg); 32 | } 33 | const finalBalance = await utils.getTokenBalanceOf(james); 34 | const expected = startingBalance; 35 | const errMsg = james + earlyPurchaseError; 36 | assert.equal(finalBalance.toString(10), expected.toString(10), errMsg); 37 | }); 38 | }); 39 | }); 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Token Sale 2 | [ ![Codeship Status for skmgoldin/simple-token-launch](https://app.codeship.com/projects/5392ad30-6041-0135-6b30-4614bcb67ade/status?branch=master)](https://app.codeship.com/projects/239399) 3 | 4 | This codebase can be used to deploy fixed-price, finite-supply token sales. It uses json conf files to specify sale parameters, supports token distributions for pre-sale buyers, and the distribution of timelocked tokens for founders. It also includes a comprehensive test suite. 5 | 6 | # Initialize 7 | ``` 8 | npm install 9 | npm run compile 10 | ``` 11 | 12 | # The tests 13 | To run the tests, simply `npm run test`. 14 | 15 | The parameters tested are the same as those which will be deployed. This means the tests can take a very long time if your start block is up in the millions, and some tests will be skipped if signing keys cannot be unlocked for the required accounts, particularly the final test block for the founder timelocking mechanisms. The `owner` address in `sale.json` must be the first address generated by the mnemonic in `secrets.json`. 16 | 17 | # Composition of the repo 18 | The repo is composed as a Truffle project. The test suite can be found in `test/sale.json`. The sale contract is in `contracts/Sale.sol`. The deployment scripts are in the `migrations` folder. 19 | 20 | The Sale contract deploys the token contract, disburses funds to pre-sale purchasers and then deploys timelock contracts to store the founders tokens. `Disbursement.sol` and `Filter.sol` comprise the timelock contracts. Two `Disbursement.sol` contracts are deployed which unlock funds at a particular date. The `Filter.sol` contracts sit in front of them and allow particular addresses to withdraw particular amounts of funds. 21 | 22 | # Using the repo to deploy a real token 23 | Config files where the parameters of your own sale can be filled in are in the `conf` directory. 24 | 25 | Note that the `owner` in `sale.json` must be the first address generated by a mnemonic in a `secrets.json` at the project root. 26 | 27 | ``` 28 | { 29 | "mnemonic": "igor cannot dunk" 30 | } 31 | ``` 32 | 33 | Having done all that you can `truffle migrate --network mainnet`. Save the contents of your build and logs directories, you'll want to have all that data. 34 | 35 | -------------------------------------------------------------------------------- /test/instantiation.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global artifacts assert contract */ 3 | 4 | const fs = require('fs'); 5 | const BN = require('bn.js'); 6 | 7 | const Sale = artifacts.require('./Sale.sol'); 8 | 9 | const saleConf = JSON.parse(fs.readFileSync('./conf/sale.json')); 10 | const tokenConf = JSON.parse(fs.readFileSync('./conf/token.json')); 11 | 12 | contract('Sale', () => { 13 | describe('Instantiation', () => { 14 | const badInitialization = 'was not initialized properly'; 15 | 16 | before(() => { 17 | saleConf.price = new BN(saleConf.price, 10); 18 | saleConf.startBlock = new BN(saleConf.startBlock, 10); 19 | tokenConf.initialAmount = new BN(tokenConf.initialAmount, 10); 20 | }); 21 | 22 | it(`should instantiate with the price set to ${saleConf.price} Wei.`, async () => { 23 | const sale = await Sale.deployed(); 24 | const price = await sale.price.call(); 25 | const expected = saleConf.price; 26 | const errMsg = `The price ${badInitialization}`; 27 | assert.strictEqual(price.toString(10), expected.toString(10), errMsg); 28 | }); 29 | 30 | it(`should instantiate with the owner set to ${saleConf.owner}.`, async () => { 31 | const sale = await Sale.deployed(); 32 | const actualOwner = await sale.owner.call(); 33 | const expected = saleConf.owner.toLowerCase(); 34 | const errMsg = `The owner ${badInitialization}`; 35 | assert.strictEqual(actualOwner.valueOf(), expected, errMsg); 36 | }); 37 | 38 | it(`should instantiate with the wallet set to ${saleConf.wallet}.`, async () => { 39 | const sale = await Sale.deployed(); 40 | const wallet = await sale.wallet.call(); 41 | const expected = saleConf.wallet; 42 | const errMsg = `The wallet ${badInitialization}`; 43 | assert.strictEqual(wallet.valueOf(), expected.toLowerCase(), errMsg); 44 | }); 45 | 46 | it(`should instantiate with the startBlock set to ${saleConf.startBlock}.`, async () => { 47 | const sale = await Sale.deployed(); 48 | const startBlock = await sale.startBlock.call(); 49 | const expected = saleConf.startBlock; 50 | const errMsg = `The start block ${badInitialization}`; 51 | assert.strictEqual(startBlock.toString(10), expected.toString(10), errMsg); 52 | }); 53 | }); 54 | }); 55 | 56 | -------------------------------------------------------------------------------- /conf/timelocks.json: -------------------------------------------------------------------------------- 1 | { 2 | "0": { 3 | "tranches": { 4 | "0": { 5 | "date": "21994675200", 6 | "period": "1", 7 | "amount": "110000000000000000" 8 | } 9 | }, 10 | "address": "0x0d1d4e623d10f9fba5db95830f7d3839406c6af2" 11 | }, 12 | "1": { 13 | "tranches": { 14 | "0": { 15 | "date": "21994675200", 16 | "period": "1", 17 | "amount": "45000000000000000" 18 | } 19 | }, 20 | "address": "0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e" 21 | }, 22 | "2": { 23 | "tranches": { 24 | "0": { 25 | "date": "21994675200", 26 | "period": "1", 27 | "amount": "27000000000000000" 28 | } 29 | }, 30 | "address": "0x2191ef87e392377ec08e7c08eb105ef5448eced5" 31 | }, 32 | "3": { 33 | "tranches": { 34 | "0": { 35 | "date": "21994675200", 36 | "period": "1", 37 | "amount": "18000000000000000" 38 | } 39 | }, 40 | "address": "0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5" 41 | }, 42 | "4": { 43 | "tranches": { 44 | "0": { 45 | "date": "21994675200", 46 | "period": "1", 47 | "amount": "56500000000000000" 48 | } 49 | }, 50 | "address": "0x6330a553fc93768f612722bb8c2ec78ac90b3bbc" 51 | }, 52 | "5": { 53 | "tranches": { 54 | "0": { 55 | "date": "21994675200", 56 | "period": "1", 57 | "amount": "66000000000000000" 58 | } 59 | }, 60 | "address": "0x5aeda56215b167893e80b4fe645ba6d5bab767de" 61 | }, 62 | "6": { 63 | "tranches": { 64 | "0": { 65 | "date": "21994675200", 66 | "period": "1", 67 | "amount": "12500000000000000" 68 | } 69 | }, 70 | "address": "0xE44c4cf797505AF1527B11e4F4c6f95531b4Be24" 71 | }, 72 | "7": { 73 | "tranches": { 74 | "0": { 75 | "date": "21994675200", 76 | "period": "1", 77 | "amount": "55000000000000000" 78 | } 79 | }, 80 | "address": "0x69e1CB5cFcA8A311586e3406ed0301C06fb839a2" 81 | }, 82 | "8": { 83 | "tranches": { 84 | "0": { 85 | "date": "19994675200", 86 | "period": "1", 87 | "amount": "6000000000000000" 88 | }, 89 | "1": { 90 | "date": "21994675200", 91 | "period": "100000", 92 | "amount": "4000000000000000" 93 | } 94 | }, 95 | "address": "0xF014343BDFFbED8660A9d8721deC985126f189F3" 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /test/initial_issuance.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global artifacts assert contract */ 3 | 4 | const fs = require('fs'); 5 | const BN = require('bn.js'); 6 | const utils = require('./utils'); 7 | 8 | const Sale = artifacts.require('./Sale.sol'); 9 | 10 | const preBuyersConf = JSON.parse(fs.readFileSync('./conf/preBuyers.json')); 11 | const saleConf = JSON.parse(fs.readFileSync('./conf/sale.json')); 12 | const tokenConf = JSON.parse(fs.readFileSync('./conf/token.json')); 13 | 14 | let tokensForSale; 15 | 16 | contract('Sale', () => { 17 | describe('Initial token issuance', () => { 18 | const wrongTokenBalance = 'has an incorrect token balance.'; 19 | 20 | before(() => { 21 | const tokensPreAllocated = utils.totalPreSoldTokens().add(utils.totalTimelockedTokens()); 22 | saleConf.price = new BN(saleConf.price, 10); 23 | saleConf.startBlock = new BN(saleConf.startBlock, 10); 24 | tokenConf.initialAmount = new BN(tokenConf.initialAmount, 10); 25 | tokensForSale = tokenConf.initialAmount.sub(tokensPreAllocated); 26 | }); 27 | 28 | it('should instantiate preBuyers with the proper number of tokens', () => 29 | Promise.all(Object.keys(preBuyersConf).map(async (curr) => { 30 | const tokenBalance = 31 | await utils.getTokenBalanceOf(preBuyersConf[curr].address); 32 | const expected = preBuyersConf[curr].amount; 33 | const errMsg = `A pre-buyer ${wrongTokenBalance}`; 34 | assert.strictEqual(tokenBalance.toString(10), expected.toString(10), errMsg); 35 | }))); 36 | 37 | it('should instantiate disburser contracts with the proper number of tokens', async () => 38 | Promise.all(utils.getTimelockedBeneficiaries().map(async (beneficiary) => { 39 | const beneficiaryTranches = utils.getTranchesForBeneficiary(beneficiary.address); 40 | return Promise.all(Object.keys(beneficiaryTranches).map(async (tranchIndex) => { 41 | const tranch = beneficiary.tranches[tranchIndex]; 42 | const disburser = 43 | utils.getDisburserByBeneficiaryAndTranch(beneficiary.address, tranch); 44 | const tokenBalance = await utils.getTokenBalanceOf(disburser.address); 45 | const expected = tranch.amount; 46 | const errMsg = `A disburser contract ${wrongTokenBalance}`; 47 | assert.strictEqual(tokenBalance.toString(10), expected.toString(10), errMsg); 48 | })); 49 | }))); 50 | 51 | it('should instantiate the public sale with the total supply of tokens ' + 52 | 'minus the sum of tokens pre-sold.', async () => { 53 | const tokenBalance = await utils.getTokenBalanceOf(Sale.address); 54 | const expected = tokensForSale.toString(10); 55 | const errMsg = `The sale contract ${wrongTokenBalance}`; 56 | assert.strictEqual(tokenBalance.toString(10), expected.toString(10), errMsg); 57 | }); 58 | }); 59 | }); 60 | 61 | -------------------------------------------------------------------------------- /test/sale_start.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global artifacts assert contract */ 3 | 4 | const fs = require('fs'); 5 | const BN = require('bn.js'); 6 | const utils = require('./utils'); 7 | 8 | const Sale = artifacts.require('./Sale.sol'); 9 | 10 | const saleConf = JSON.parse(fs.readFileSync('./conf/sale.json')); 11 | const tokenConf = JSON.parse(fs.readFileSync('./conf/token.json')); 12 | 13 | contract('Sale', (accounts) => { 14 | const [owner, james, miguel, edwhale] = accounts; 15 | 16 | describe('Sale start', () => { 17 | const balanceError = 'A balance was not utils.as expected following a purchase'; 18 | 19 | before(async () => { 20 | saleConf.price = new BN(saleConf.price, 10); 21 | saleConf.startBlock = new BN(saleConf.startBlock, 10); 22 | tokenConf.initialAmount = new BN(tokenConf.initialAmount, 10); 23 | 24 | await utils.forceMine(saleConf.startBlock); 25 | }); 26 | 27 | it('should not allow the owner to change the price', async () => { 28 | const sale = await Sale.deployed(); 29 | try { 30 | await utils.as(owner, sale.changePrice, saleConf.price + 1); 31 | } catch (err) { 32 | const errMsg = err.toString(); 33 | assert(utils.isEVMRevert(err), errMsg); 34 | } 35 | const price = await sale.price.call(); 36 | const expected = saleConf.price; 37 | const errMsg = 'The owner was able to change the price after the freeze block'; 38 | assert.strictEqual(price.toString(10), expected.toString(10), errMsg); 39 | }); 40 | 41 | it('should transfer 1 token to James.', async () => { 42 | const startingBalance = await utils.getTokenBalanceOf(james); 43 | const purchaseAmount = new BN('1', 10); 44 | await utils.purchaseTokens(james, purchaseAmount); 45 | const finalBalance = await utils.getTokenBalanceOf(james); 46 | const expected = startingBalance.add(purchaseAmount); 47 | const errMsg = balanceError; 48 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 49 | }); 50 | 51 | it('should transfer 10 tokens to Miguel.', async () => { 52 | const startingBalance = await utils.getTokenBalanceOf(miguel); 53 | const purchaseAmount = new BN('10', 10); 54 | await utils.purchaseTokens(miguel, purchaseAmount); 55 | const finalBalance = await utils.getTokenBalanceOf(miguel); 56 | const expected = startingBalance.add(purchaseAmount); 57 | const errMsg = balanceError; 58 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 59 | }); 60 | 61 | it('should transfer 100 tokens to Edwhale.', async () => { 62 | const startingBalance = await utils.getTokenBalanceOf(edwhale); 63 | const purchaseAmount = new BN('100', 10); 64 | await utils.purchaseTokens(edwhale, purchaseAmount); 65 | const finalBalance = await utils.getTokenBalanceOf(edwhale); 66 | const expected = startingBalance.add(purchaseAmount); 67 | const errMsg = balanceError; 68 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 69 | }); 70 | }); 71 | }); 72 | 73 | -------------------------------------------------------------------------------- /contracts/Disbursement.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | import "tokens/eip20/EIP20.sol"; 3 | 4 | /// @title Disbursement contract - allows to distribute tokens over time 5 | /// @author Stefan George - 6 | contract Disbursement { 7 | 8 | /* 9 | * Storage 10 | */ 11 | address public owner; 12 | address public receiver; 13 | uint public disbursementPeriod; 14 | uint public startDate; 15 | uint public withdrawnTokens; 16 | EIP20 public token; 17 | 18 | /* 19 | * Modifiers 20 | */ 21 | modifier isOwner() { 22 | if (msg.sender != owner) 23 | // Only owner is allowed to proceed 24 | revert(); 25 | _; 26 | } 27 | 28 | modifier isReceiver() { 29 | if (msg.sender != receiver) 30 | // Only receiver is allowed to proceed 31 | revert(); 32 | _; 33 | } 34 | 35 | modifier isSetUp() { 36 | if (address(token) == 0) 37 | // Contract is not set up 38 | revert(); 39 | _; 40 | } 41 | 42 | /* 43 | * Public functions 44 | */ 45 | /// @dev Constructor function sets contract owner 46 | /// @param _receiver Receiver of vested tokens 47 | /// @param _disbursementPeriod Vesting period in seconds 48 | /// @param _startDate Start date of disbursement period (cliff) 49 | function Disbursement(address _receiver, uint _disbursementPeriod, uint _startDate) 50 | public 51 | { 52 | if (_receiver == 0 || _disbursementPeriod == 0) 53 | // Arguments are null 54 | revert(); 55 | owner = msg.sender; 56 | receiver = _receiver; 57 | disbursementPeriod = _disbursementPeriod; 58 | startDate = _startDate; 59 | if (startDate == 0) 60 | startDate = now; 61 | } 62 | 63 | /// @dev Setup function sets external contracts' addresses 64 | /// @param _token Token address 65 | function setup(EIP20 _token) 66 | public 67 | isOwner 68 | { 69 | if (address(token) != 0 || address(_token) == 0) 70 | // Setup was executed already or address is null 71 | revert(); 72 | token = _token; 73 | } 74 | 75 | /// @dev Transfers tokens to a given address 76 | /// @param _to Address of token receiver 77 | /// @param _value Number of tokens to transfer 78 | function withdraw(address _to, uint256 _value) 79 | public 80 | isReceiver 81 | isSetUp 82 | { 83 | uint maxTokens = calcMaxWithdraw(); 84 | if (_value > maxTokens) 85 | revert(); 86 | withdrawnTokens += _value; 87 | token.transfer(_to, _value); 88 | } 89 | 90 | /// @dev Calculates the maximum amount of vested tokens 91 | /// @return Number of vested tokens to withdraw 92 | function calcMaxWithdraw() 93 | public 94 | constant 95 | returns (uint) 96 | { 97 | uint maxTokens = (token.balanceOf(this) + withdrawnTokens) * (now - startDate) / disbursementPeriod; 98 | if (withdrawnTokens >= maxTokens || startDate > now) 99 | return 0; 100 | return maxTokens - withdrawnTokens; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/emergency_stop.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global artifacts assert contract */ 3 | 4 | const fs = require('fs'); 5 | const BN = require('bn.js'); 6 | const utils = require('./utils'); 7 | 8 | const Sale = artifacts.require('./Sale.sol'); 9 | 10 | const saleConf = JSON.parse(fs.readFileSync('./conf/sale.json')); 11 | const tokenConf = JSON.parse(fs.readFileSync('./conf/token.json')); 12 | 13 | contract('Sale', (accounts) => { 14 | const [owner, james, miguel, edwhale] = accounts; 15 | 16 | describe('Emergency stop', () => { 17 | const purchaseInStopError = ' was able to purchase during the emergency stop'; 18 | const balanceError = 'A balance was not utils.as expected following a purchase'; 19 | 20 | before(async () => { 21 | saleConf.price = new BN(saleConf.price, 10); 22 | saleConf.startBlock = new BN(saleConf.startBlock, 10); 23 | tokenConf.initialAmount = new BN(tokenConf.initialAmount, 10); 24 | 25 | await utils.forceMine(saleConf.startBlock); 26 | const sale = await Sale.deployed(); 27 | await utils.as(owner, sale.emergencyToggle); 28 | }); 29 | 30 | it('should not transfer 1 token to James.', async () => { 31 | const startingBalance = await utils.getTokenBalanceOf(james); 32 | const purchaseAmount = new BN('1', 10); 33 | try { 34 | await utils.purchaseTokens(james, purchaseAmount); 35 | const errMsg = james + purchaseInStopError; 36 | assert(false, errMsg); 37 | } catch (err) { 38 | const errMsg = err.toString(); 39 | assert(utils.isEVMRevert(err), errMsg); 40 | } 41 | const finalBalance = await utils.getTokenBalanceOf(james); 42 | const expected = startingBalance; 43 | const errMsg = balanceError; 44 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 45 | }); 46 | 47 | it('should not transfer 10 tokens to Miguel.', async () => { 48 | const startingBalance = await utils.getTokenBalanceOf(miguel); 49 | const purchaseAmount = new BN('10', 10); 50 | try { 51 | await utils.purchaseTokens(miguel, purchaseAmount); 52 | const errMsg = miguel + purchaseInStopError; 53 | assert(false, errMsg); 54 | } catch (err) { 55 | const errMsg = err.toString(); 56 | assert(utils.isEVMRevert(err), errMsg); 57 | } 58 | const finalBalance = await utils.getTokenBalanceOf(miguel); 59 | const expected = startingBalance; 60 | const errMsg = balanceError; 61 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 62 | }); 63 | 64 | it('should not transfer 100 tokens to Edwhale.', async () => { 65 | const startingBalance = await utils.getTokenBalanceOf(edwhale); 66 | const purchaseAmount = new BN('100', 10); 67 | try { 68 | await utils.purchaseTokens(edwhale, purchaseAmount); 69 | const errMsg = edwhale + purchaseInStopError; 70 | assert(false, errMsg); 71 | } catch (err) { 72 | const errMsg = err.toString(); 73 | assert(utils.isEVMRevert(err), errMsg); 74 | } 75 | const finalBalance = await utils.getTokenBalanceOf(edwhale); 76 | const expected = startingBalance; 77 | const errMsg = balanceError; 78 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 79 | }); 80 | }); 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /test/sale_end.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global artifacts assert contract */ 3 | 4 | const fs = require('fs'); 5 | const BN = require('bn.js'); 6 | const HttpProvider = require('ethjs-provider-http'); 7 | const EthQuery = require('ethjs-query'); 8 | const utils = require('./utils'); 9 | 10 | const Sale = artifacts.require('./Sale.sol'); 11 | 12 | const ethQuery = new EthQuery(new HttpProvider('http://localhost:7545')); 13 | 14 | const saleConf = JSON.parse(fs.readFileSync('./conf/sale.json')); 15 | const tokenConf = JSON.parse(fs.readFileSync('./conf/token.json')); 16 | 17 | contract('Sale', (accounts) => { 18 | const [, , , edwhale] = accounts; 19 | 20 | describe('Sale end', () => { 21 | const balanceError = 'A balance was not utils.as expected following a purchase'; 22 | 23 | before(async () => { 24 | saleConf.price = new BN(saleConf.price, 10); 25 | saleConf.startBlock = new BN(saleConf.startBlock, 10); 26 | tokenConf.initialAmount = new BN(tokenConf.initialAmount, 10); 27 | 28 | await utils.forceMine(saleConf.startBlock); 29 | }); 30 | 31 | it('should reject a transfer of tokens to Edwhale greater than the sum ' + 32 | 'of tokens available for purchase.', async () => { 33 | const startingBalance = await utils.getTokenBalanceOf(edwhale); 34 | const saleBalance = await utils.getTokenBalanceOf(Sale.address); 35 | const tooMuch = saleBalance.add(new BN('1', 10)); 36 | try { 37 | await utils.purchaseTokens(edwhale, tooMuch); 38 | const errMsg = `${edwhale} was able to purchase more tokens than should ` + 39 | 'be available'; 40 | assert(false, errMsg); 41 | } catch (err) { 42 | const errMsg = err.toString(); 43 | assert(utils.isEVMRevert(err), errMsg); 44 | } 45 | const finalBalance = await utils.getTokenBalanceOf(edwhale); 46 | const expected = startingBalance; 47 | const errMsg = balanceError; 48 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 49 | }); 50 | 51 | it('should return excess Wei to Edwhale', async () => { 52 | const startingBalance = await ethQuery.getBalance(edwhale); 53 | const gasPrice = await ethQuery.gasPrice(); 54 | const sale = await Sale.deployed(); 55 | const excessEther = saleConf.price.div(new BN('2', 10)); 56 | const receipt = 57 | await sale.purchaseTokens({ 58 | value: saleConf.price.add(excessEther), 59 | from: edwhale, 60 | gasPrice, 61 | }); 62 | const gasUsed = new BN(receipt.receipt.gasUsed, 10); 63 | const expectedEthDebit = gasPrice.mul(gasUsed).add(saleConf.price); 64 | const finalBalance = await ethQuery.getBalance(edwhale); 65 | const expected = startingBalance.sub(expectedEthDebit); 66 | const errMsg = 'Edwhale\'s ether balance is not utils.as expected following ' + 67 | 'a purchase transaction'; 68 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 69 | }); 70 | 71 | it('should transfer all the remaining tokens to Edwhale.', async () => { 72 | const startingBalance = await utils.getTokenBalanceOf(edwhale); 73 | const saleBalance = await utils.getTokenBalanceOf(Sale.address); 74 | await utils.purchaseTokens(edwhale, saleBalance); 75 | const finalBalance = await utils.getTokenBalanceOf(edwhale); 76 | const expected = startingBalance.add(saleBalance); 77 | const errMsg = balanceError; 78 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 79 | }); 80 | }); 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /conf/preBuyers.json: -------------------------------------------------------------------------------- 1 | { 2 | "0": { 3 | "amount": "2999999000000000", 4 | "address": "0x260E5500fbEeb26d3632Cca07CA819CaA1E29B7A" 5 | }, 6 | "1": { 7 | "amount": "33958334000000000", 8 | "address": "0x70EEe2D2f75ED29a62866b80b57C8f2990e1ca36" 9 | }, 10 | "2": { 11 | "amount": "26458334000000000", 12 | "address": "0x002611951BA386Db26261142b8F685a26Ec91205" 13 | }, 14 | "3": { 15 | "amount": "7500000000000000", 16 | "address": "0x9cc5B4DD8D672DA071EBb69A789956b54b820525" 17 | }, 18 | "4": { 19 | "amount": "12500000000000000", 20 | "address": "0x508ad1f1511b9Bd4CeB89c12514241e3546FacD2" 21 | }, 22 | "5": { 23 | "amount": "416667000000000", 24 | "address": "0x13525cE40B1a16A4350A21Bae04920A3079412Bb" 25 | }, 26 | "6": { 27 | "amount": "416667000000000", 28 | "address": "0xedB0DFf82f71647A2757baCe9D76969ac4963781" 29 | }, 30 | "7": { 31 | "amount": "1666667000000000", 32 | "address": "0xf3374c5D7394DB9f84421fB4C7dD3f7A48928B80" 33 | }, 34 | "8": { 35 | "amount": "833333000000000", 36 | "address": "0xBCeF1ec3c78eeBcf8Bf269118aF0AD4cB22C43c4" 37 | }, 38 | "9": { 39 | "amount": "1250000000000000", 40 | "address": "0x92Af7D428fa7044c38F0Ac5C7a4258D3395988Ea" 41 | }, 42 | "10": { 43 | "amount": "833333000000000", 44 | "address": "0x898c85ebd232dDb5e27628a7069Ab7dcB8a548A4" 45 | }, 46 | "11": { 47 | "amount": "208333000000000", 48 | "address": "0x6eE5408aE077D90a616EB617467939680e1e84ef" 49 | }, 50 | "12": { 51 | "amount": "416667000000000", 52 | "address": "0xFb79bEe9184b3427AC60F984Fff8FF7bB5fe9771" 53 | }, 54 | "13": { 55 | "amount": "416667000000000", 56 | "address": "0x408EF32D69dc5E71152f776e94059b4d87BdF4cf" 57 | }, 58 | "14": { 59 | "amount": "416667000000000", 60 | "address": "0x74f06A98A33F390b46C521747CCd34D328ccd2f2" 61 | }, 62 | "15": { 63 | "amount": "208333000000000", 64 | "address": "0x69c63cbf504cd8841decc8875d4018df136cd344" 65 | }, 66 | "16": { 67 | "amount": "903614000000000", 68 | "address": "0x00aF7A08F0a421F2BE1BD659116F03D6c0473Ab7" 69 | }, 70 | "17": { 71 | "amount": "50000000000000", 72 | "address": "0x9463f75B8af2eb6DE64aec52F8dfBc7c4Ad20469" 73 | }, 74 | "18": { 75 | "amount": "50000000000000", 76 | "address": "0x5fa079673749104641B0e59388a87c84c2843Fe6" 77 | }, 78 | "19": { 79 | "amount": "50000000000000", 80 | "address": "0x5bF50c00da77b1f3864Cae3C927d029750c040a8" 81 | }, 82 | "20": { 83 | "amount": "25000000000000", 84 | "address": "0x0047C20d2eF4998dC2be6bC28967374C7c6a6B5d" 85 | }, 86 | "21": { 87 | "amount": "50000000000000", 88 | "address": "0x06905127EcB3f59c46a468489e5b262d7AfCc2e8" 89 | }, 90 | "22": { 91 | "amount": "25000000000000", 92 | "address": "0xbC411247092da7133eD5A4bFEa4cc71e54Fe16E0" 93 | }, 94 | "23": { 95 | "amount": "25000000000000", 96 | "address": "0xDdBd16316e2D9328199579EfBDfE9f84aa3Cd678" 97 | }, 98 | "24": { 99 | "amount": "10000000000000", 100 | "address": "0xB14e4b24B46CD8cEb73bC6A9607004D69B6F564d" 101 | }, 102 | "25": { 103 | "amount": "1000000000000000", 104 | "address": "0xE9Aa55013556590186AcDFab4F82919F81C24559" 105 | }, 106 | "26": { 107 | "amount": "250000000000000", 108 | "address": "0xf23DadE2097018306A4b91bA82b6017012477E1d" 109 | }, 110 | "27": { 111 | "amount": "250000000000000", 112 | "address": "0xb827433D881Ee05b025bCE6b5144171B76eDd4fd" 113 | }, 114 | "28": { 115 | "amount": "5000000000000", 116 | "address": "0x2207f2a9d82130c1fb2f818c3d851bf5ea9255c9" 117 | }, 118 | "29": { 119 | "amount": "6806385000000000", 120 | "address": "0xe35546E159973e3dC637f3fF9438c60D92fb0E07" 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /migrations/2_deploy_sale.js: -------------------------------------------------------------------------------- 1 | /* global artifacts */ 2 | 3 | const Sale = artifacts.require('./Sale.sol'); 4 | const fs = require('fs'); 5 | 6 | const distributePreBuyersTokens = async function distributePreBuyersTokens(addresses, tokens) { 7 | const BATCHSIZE = 30; 8 | if (addresses.length !== tokens.length) { 9 | throw new Error('The number of pre-buyers and pre-buyer token allocations do not match'); 10 | } 11 | 12 | const addressesChunk = addresses.slice(0, BATCHSIZE); 13 | const tokensChunk = tokens.slice(0, BATCHSIZE); 14 | const sale = await Sale.deployed(); 15 | await sale.distributePreBuyersRewards(addressesChunk, tokensChunk); 16 | console.log(`Distributed tokens to a batch of ${addressesChunk.length} pre-buyers`); 17 | 18 | if (addresses.length <= BATCHSIZE) { 19 | return addressesChunk; 20 | } 21 | 22 | return addressesChunk.concat(await distributePreBuyersTokens( 23 | addresses.slice(BATCHSIZE), 24 | tokens.slice(BATCHSIZE), 25 | )); 26 | }; 27 | 28 | const distributeTimelockedTokens = async function distributeTimeLockedTokens( 29 | addresses, tokens, 30 | timelocks, periods, logs, 31 | ) { 32 | const BATCHSIZE = 4; 33 | if (addresses.length !== tokens.length) { // expand 34 | throw new Error('The number of pre-buyers and pre-buyer token allocations do not match'); 35 | } 36 | 37 | const addressesChunk = addresses.slice(0, BATCHSIZE); 38 | const tokensChunk = tokens.slice(0, BATCHSIZE); 39 | const timelocksChunk = timelocks.slice(0, BATCHSIZE); 40 | const periodsChunk = periods.slice(0, BATCHSIZE); 41 | 42 | const sale = await Sale.deployed(); 43 | const receipt = await sale.distributeTimelockedTokens( 44 | addressesChunk, tokensChunk, 45 | timelocksChunk, periodsChunk, 46 | ); 47 | console.log(`Distributed a batch of ${addressesChunk.length} timelocked token chunks`); 48 | 49 | if (addresses.length <= BATCHSIZE) { 50 | return logs.concat(receipt.logs); 51 | } 52 | 53 | return distributeTimeLockedTokens( 54 | addresses.slice(BATCHSIZE), 55 | tokens.slice(BATCHSIZE), 56 | timelocks.slice(BATCHSIZE), 57 | periods.slice(BATCHSIZE), 58 | logs.concat(receipt.logs), 59 | ); 60 | }; 61 | 62 | const flattenTimeLockData = function flattenTimeLockData(timeLockData) { 63 | const flattenedTimeLockData = { 64 | beneficiaries: [], 65 | allocations: [], 66 | disbursementDates: [], 67 | disbursementPeriods: [], 68 | }; 69 | 70 | Object.keys(timeLockData).map((beneficiaryIndex) => { 71 | const beneficiary = timeLockData[beneficiaryIndex]; 72 | Object.keys(beneficiary.tranches).map((tranchIndex) => { 73 | const tranch = beneficiary.tranches[tranchIndex]; 74 | flattenedTimeLockData.beneficiaries.push(beneficiary.address); 75 | flattenedTimeLockData.allocations.push(tranch.amount); 76 | flattenedTimeLockData.disbursementDates.push(tranch.date); 77 | flattenedTimeLockData.disbursementPeriods.push(tranch.period); 78 | return tranch; 79 | }); 80 | return beneficiary; 81 | }); 82 | 83 | return flattenedTimeLockData; 84 | }; 85 | 86 | module.exports = (deployer) => { 87 | const saleConf = JSON.parse(fs.readFileSync('./conf/sale.json')); 88 | const tokenConf = JSON.parse(fs.readFileSync('./conf/token.json')); 89 | const preBuyersConf = JSON.parse(fs.readFileSync('./conf/preBuyers.json')); 90 | const timelocksConf = JSON.parse(fs.readFileSync('./conf/timelocks.json')); 91 | 92 | const preBuyers = Object.keys(preBuyersConf).map(preBuyer => preBuyersConf[preBuyer].address); 93 | const preBuyersTokens = Object.keys(preBuyersConf).map(preBuyer => 94 | preBuyersConf[preBuyer].amount); 95 | 96 | const timeLockData = flattenTimeLockData(timelocksConf); 97 | 98 | return deployer.deploy( 99 | Sale, 100 | saleConf.owner, 101 | saleConf.wallet, 102 | tokenConf.initialAmount, 103 | tokenConf.tokenName, 104 | tokenConf.decimalUnits, 105 | tokenConf.tokenSymbol, 106 | saleConf.price, 107 | saleConf.startBlock, 108 | saleConf.freezeBlock, 109 | preBuyers.length, 110 | timeLockData.beneficiaries.length, 111 | ) 112 | .then(() => distributePreBuyersTokens(preBuyers, preBuyersTokens)) 113 | .then(() => distributeTimelockedTokens( 114 | timeLockData.beneficiaries, 115 | timeLockData.allocations, 116 | timeLockData.disbursementDates, 117 | timeLockData.disbursementPeriods, 118 | [], 119 | )) 120 | .then((logs) => { 121 | if (!fs.existsSync('logs')) { 122 | fs.mkdirSync('logs'); 123 | } 124 | fs.writeFileSync('logs/logs.json', JSON.stringify(logs, null, 2)); 125 | }); 126 | }; 127 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | /* global artifacts */ 2 | 3 | const BN = require('bn.js'); 4 | const fs = require('fs'); 5 | const HttpProvider = require('ethjs-provider-http'); 6 | const EthRPC = require('ethjs-rpc'); 7 | const EthQuery = require('ethjs-query'); 8 | 9 | const Sale = artifacts.require('./Sale.sol'); 10 | const EIP20 = artifacts.require('tokens/eip20/EIP20.sol'); 11 | const Disbursement = artifacts.require('./Disbursement.sol'); 12 | 13 | const preBuyersConf = JSON.parse(fs.readFileSync('./conf/preBuyers.json')); 14 | const timelocksConf = JSON.parse(fs.readFileSync('./conf/timelocks.json')); 15 | const saleConf = JSON.parse(fs.readFileSync('./conf/sale.json')); 16 | const logs = JSON.parse(fs.readFileSync('./logs/logs.json')); 17 | 18 | const ethRPC = new EthRPC(new HttpProvider('http://localhost:7545')); 19 | const ethQuery = new EthQuery(new HttpProvider('http://localhost:7545')); 20 | 21 | const utils = { 22 | 23 | purchaseTokens: async (actor, amount) => { 24 | if (!BN.isBN(amount)) { throw new Error('Supplied amount is not a BN.'); } 25 | const sale = await Sale.deployed(); 26 | await sale.purchaseTokens({ from: actor, value: amount.mul(new BN(saleConf.price, 10)) }); 27 | }, 28 | 29 | getTokenBalanceOf: async (actor) => { 30 | const sale = await Sale.deployed(); 31 | const tokenAddr = await sale.token.call(); 32 | const token = EIP20.at(tokenAddr); 33 | const balance = await token.balanceOf.call(actor); 34 | return new BN(balance.toString(10), 10); 35 | }, 36 | 37 | totalPreSoldTokens: () => { 38 | const preSoldTokens = Object.keys(preBuyersConf).map(curr => 39 | new BN(preBuyersConf[curr].amount, 10)); 40 | return preSoldTokens.reduce((sum, value) => sum.add(new BN(value, 10)), new BN(0, 10)); 41 | }, 42 | 43 | 44 | getTranchesForBeneficiary: (addr) => { 45 | const beneficiary = timelocksConf[ 46 | Object.keys(timelocksConf).find((beneficiaryIndex) => { 47 | const thisBeneficiary = timelocksConf[beneficiaryIndex]; 48 | return thisBeneficiary.address === addr; 49 | }) 50 | ]; 51 | 52 | return beneficiary.tranches; 53 | }, 54 | 55 | getDisburserByBeneficiaryAndTranch: (beneficiary, tranch) => { 56 | const logForTranch = logs.find(log => 57 | log.args.beneficiary === beneficiary.toLowerCase() && 58 | log.args.amount === tranch.amount); 59 | 60 | if (logForTranch === undefined) { throw new Error(`Missing disburser for ${beneficiary}`); } 61 | 62 | return Disbursement.at(logForTranch.args.disburser); 63 | }, 64 | 65 | getDisbursersForBeneficiary: (beneficiary) => { 66 | const tranches = Object.keys(utils.getTranchesForBeneficiary(beneficiary)).map(tranchIndex => 67 | utils.getTranchesForBeneficiary(beneficiary)[tranchIndex]); 68 | return tranches.map(tranch => 69 | utils.getDisburserByBeneficiaryAndTranch(beneficiary, tranch)); 70 | }, 71 | 72 | getTimelockedBeneficiaries: () => 73 | Object.keys(timelocksConf).map(beneficiaryIndex => timelocksConf[beneficiaryIndex]), 74 | 75 | totalTimelockedTokens: () => { 76 | function getDisburserTokenBalances() { 77 | let disburserTokenBalances = []; 78 | 79 | utils.getTimelockedBeneficiaries().forEach((beneficiary) => { 80 | const tranches = utils.getTranchesForBeneficiary(beneficiary.address); 81 | disburserTokenBalances = 82 | disburserTokenBalances.concat(Object.keys(tranches).map((tranchIndex) => { 83 | const tranch = tranches[tranchIndex]; 84 | return tranch.amount; 85 | })); 86 | }); 87 | 88 | return disburserTokenBalances; 89 | } 90 | 91 | const timelockedTokens = getDisburserTokenBalances(); 92 | 93 | return timelockedTokens.reduce((sum, value) => sum.add(new BN(value, 10)), new BN(0, 10)); 94 | }, 95 | 96 | isSignerAccessFailure: (err) => { 97 | const signerAccessFailure = 'could not unlock signer account'; 98 | return err.toString().includes(signerAccessFailure); 99 | }, 100 | 101 | isEVMRevert: err => err.toString().includes('revert'), 102 | 103 | forceMine: blockToMine => 104 | new Promise(async (resolve, reject) => { 105 | if (!BN.isBN(blockToMine)) { 106 | reject(new Error('Supplied block number must be a BN.')); 107 | } 108 | const blockNumber = await ethQuery.blockNumber(); 109 | if (blockNumber.lt(blockToMine)) { 110 | ethRPC.sendAsync({ method: 'evm_mine' }, (err) => { 111 | if (err !== undefined && err !== null) { reject(err); } 112 | resolve(utils.forceMine(blockToMine)); 113 | }); 114 | } else { 115 | resolve(); 116 | } 117 | }), 118 | 119 | as: (actor, fn, ...args) => { 120 | function detectSendObject(potentialSendObj) { 121 | function hasOwnProperty(obj, prop) { 122 | const proto = obj.constructor.prototype; 123 | return (prop in obj) && 124 | (!(prop in proto) || proto[prop] !== obj[prop]); 125 | } 126 | if (typeof potentialSendObj !== 'object') { return undefined; } 127 | if ( 128 | hasOwnProperty(potentialSendObj, 'from') || 129 | hasOwnProperty(potentialSendObj, 'to') || 130 | hasOwnProperty(potentialSendObj, 'gas') || 131 | hasOwnProperty(potentialSendObj, 'gasPrice') || 132 | hasOwnProperty(potentialSendObj, 'value') 133 | ) { 134 | throw new Error('It is unsafe to use "as" with custom send objects'); 135 | } 136 | return undefined; 137 | } 138 | detectSendObject(args[args.length - 1]); 139 | const sendObject = { from: actor }; 140 | return fn(...args, sendObject); 141 | }, 142 | }; 143 | 144 | module.exports = utils; 145 | 146 | -------------------------------------------------------------------------------- /test/post_sale_period.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global artifacts assert contract */ 3 | 4 | const fs = require('fs'); 5 | const BN = require('bn.js'); 6 | const HttpProvider = require('ethjs-provider-http'); 7 | const EthQuery = require('ethjs-query'); 8 | const utils = require('./utils'); 9 | 10 | const Sale = artifacts.require('./Sale.sol'); 11 | const EIP20 = artifacts.require('tokens/eip20/EIP20.sol'); 12 | 13 | const ethQuery = new EthQuery(new HttpProvider('http://localhost:7545')); 14 | 15 | const saleConf = JSON.parse(fs.readFileSync('./conf/sale.json')); 16 | const tokenConf = JSON.parse(fs.readFileSync('./conf/token.json')); 17 | 18 | let tokensForSale; 19 | 20 | contract('Sale', (accounts) => { 21 | const [, james, miguel, edwhale] = accounts; 22 | 23 | describe('Post-sale period', () => { 24 | const balanceError = 'A balance was not utils.as expected following a purchase'; 25 | const sellOutError = ' was able to purchase when the sale was sold out'; 26 | 27 | before(async () => { 28 | const tokensPreAllocated = utils.totalPreSoldTokens().add(utils.totalTimelockedTokens()); 29 | saleConf.price = new BN(saleConf.price, 10); 30 | saleConf.startBlock = new BN(saleConf.startBlock, 10); 31 | tokenConf.initialAmount = new BN(tokenConf.initialAmount, 10); 32 | tokensForSale = tokenConf.initialAmount.sub(tokensPreAllocated); 33 | 34 | await utils.forceMine(saleConf.startBlock); 35 | 36 | const saleBalance = await utils.getTokenBalanceOf(Sale.address); 37 | await utils.purchaseTokens(edwhale, saleBalance); 38 | }); 39 | 40 | it('should not transfer 1 token to James.', async () => { 41 | const startingBalance = await utils.getTokenBalanceOf(james); 42 | const purchaseAmount = new BN('1', 10); 43 | try { 44 | await utils.purchaseTokens(james, purchaseAmount); 45 | const errMsg = james + sellOutError; 46 | assert(false, errMsg); 47 | } catch (err) { 48 | const errMsg = err.toString(); 49 | assert(utils.isEVMRevert(err), errMsg); 50 | } 51 | const finalBalance = await utils.getTokenBalanceOf(james); 52 | const expected = startingBalance; 53 | const errMsg = balanceError; 54 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 55 | }); 56 | 57 | it('should not transfer 10 tokens to Miguel.', async () => { 58 | const startingBalance = await utils.getTokenBalanceOf(miguel); 59 | const purchaseAmount = new BN('10', 10); 60 | try { 61 | await utils.purchaseTokens(miguel, purchaseAmount); 62 | const errMsg = miguel + sellOutError; 63 | assert(false, errMsg); 64 | } catch (err) { 65 | const errMsg = err.toString(); 66 | assert(utils.isEVMRevert(err), errMsg); 67 | } 68 | const finalBalance = await utils.getTokenBalanceOf(miguel); 69 | const expected = startingBalance; 70 | const errMsg = balanceError; 71 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 72 | }); 73 | 74 | it('should not transfer 100 tokens to Edwhale.', async () => { 75 | const startingBalance = await utils.getTokenBalanceOf(edwhale); 76 | const purchaseAmount = new BN('100', 10); 77 | try { 78 | await utils.purchaseTokens(edwhale, purchaseAmount); 79 | const errMsg = edwhale + sellOutError; 80 | assert(false, errMsg); 81 | } catch (err) { 82 | const errMsg = err.toString(); 83 | assert(utils.isEVMRevert(err), errMsg); 84 | } 85 | const finalBalance = await utils.getTokenBalanceOf(edwhale); 86 | const expected = startingBalance; 87 | const errMsg = balanceError; 88 | assert.strictEqual(finalBalance.toString(10), expected.toString(10), errMsg); 89 | }); 90 | 91 | it('should report the proper sum of Wei in the wallet.', async () => { 92 | const balance = await ethQuery.getBalance(saleConf.wallet); 93 | const expected = tokensForSale.mul(saleConf.price); 94 | const errMsg = 'The amount of Ether in the wallet is not what it should be at sale end'; 95 | assert.strictEqual(balance.toString(10), expected.toString(10), errMsg); 96 | }); 97 | 98 | it('should report a zero balance for the sale contract.', async () => { 99 | const balance = await utils.getTokenBalanceOf(Sale.address); 100 | const expected = new BN('0', 10); 101 | const errMsg = 'The sale contract still has tokens in it when it should be sold out'; 102 | assert.strictEqual(balance.toString(10), expected.toString(10), errMsg); 103 | }); 104 | 105 | it('should allow Edwhale to transfer 10 tokens to James.', async () => { 106 | const transferAmount = new BN('10', 10); 107 | const edwhaleStartingBalance = await utils.getTokenBalanceOf(edwhale); 108 | const jamesStartingBalance = await utils.getTokenBalanceOf(james); 109 | const sale = await Sale.deployed(); 110 | const tokenAddr = await sale.token.call(); 111 | const token = EIP20.at(tokenAddr); 112 | await utils.as(edwhale, token.transfer, james, transferAmount.toString(10)); 113 | const edwhaleFinalBalance = await utils.getTokenBalanceOf(edwhale); 114 | const edwhaleExpected = edwhaleStartingBalance.sub(transferAmount); 115 | const errMsg = balanceError; 116 | assert.strictEqual(edwhaleFinalBalance.toString(10), edwhaleExpected.toString(10), errMsg); 117 | const jamesFinalBalance = await utils.getTokenBalanceOf(james); 118 | const jamesExpected = jamesStartingBalance.add(transferAmount); 119 | assert.strictEqual(jamesFinalBalance.toString(10), jamesExpected.toString(10), errMsg); 120 | }); 121 | }); 122 | }); 123 | 124 | -------------------------------------------------------------------------------- /test/disbursers.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global assert contract */ 3 | 4 | const fs = require('fs'); 5 | const BN = require('bn.js'); 6 | const HttpProvider = require('ethjs-provider-http'); 7 | const EthRPC = require('ethjs-rpc'); 8 | const utils = require('./utils'); 9 | 10 | const ethRPC = new EthRPC(new HttpProvider('http://localhost:7545')); 11 | 12 | const saleConf = JSON.parse(fs.readFileSync('./conf/sale.json')); 13 | const tokenConf = JSON.parse(fs.readFileSync('./conf/token.json')); 14 | 15 | contract('Sale', () => { 16 | describe('Filters and disbursers', () => { 17 | before(() => { 18 | saleConf.price = new BN(saleConf.price, 10); 19 | saleConf.startBlock = new BN(saleConf.startBlock, 10); 20 | tokenConf.initialAmount = new BN(tokenConf.initialAmount, 10); 21 | }); 22 | 23 | function signerAccessFailureFor(address) { 24 | return `WARNING: could not unlock account ${address}.\n` + 25 | 'This is probably because this beneficiary\'s private key is not generated \n' + 26 | 'by the same mnemonic utils.as your owner privKey. This is probably fine, but \n' + 27 | 'it means we can\'t run this test.'; 28 | } 29 | 30 | it('Should not allow beneficiarys to withdraw tokens before the vesting date', async () => 31 | Promise.all(utils.getTimelockedBeneficiaries().map(async (beneficiary) => { 32 | const disbursers = utils.getDisbursersForBeneficiary(beneficiary.address); 33 | return Promise.all(disbursers.map(async (disburser) => { 34 | try { 35 | const maxWithdraw = 36 | await utils.as(beneficiary.address, disburser.calcMaxWithdraw.call); 37 | const expected = '0'; 38 | assert.strictEqual( 39 | maxWithdraw.toString(10), expected, 40 | `Expected maxWithdraw to be zero for ${beneficiary.address}`, 41 | ); 42 | await utils.as( 43 | beneficiary.address, disburser.withdraw, beneficiary.address, 44 | maxWithdraw + 1, 45 | ); 46 | assert( 47 | false, 48 | `${beneficiary.address} was able to withdraw timelocked tokens early`, 49 | ); 50 | } catch (err) { 51 | if (utils.isSignerAccessFailure(err)) { 52 | console.log(signerAccessFailureFor(beneficiary.address)); 53 | } else { 54 | assert(utils.isEVMRevert(err), err.toString()); 55 | } 56 | } 57 | })); 58 | }))); 59 | 60 | it('Should allow beneficiarys to withdraw from their disbursers after they vest', async () => { 61 | function getEVMSnapshot() { 62 | return new Promise((resolve, reject) => { 63 | ethRPC.sendAsync({ 64 | method: 'evm_snapshot', 65 | }, async (snapshotErr, snapshotID) => { 66 | if (snapshotErr) { reject(snapshotErr); } 67 | resolve(snapshotID); 68 | }); 69 | }); 70 | } 71 | 72 | function makeEVMRevert(_snapshot) { 73 | return new Promise((resolve, reject) => { 74 | ethRPC.sendAsync({ 75 | method: 'evm_revert', 76 | params: [_snapshot], 77 | }, async (revertErr) => { 78 | if (revertErr) { reject(revertErr); } 79 | resolve(); 80 | }); 81 | }); 82 | } 83 | 84 | function makeEVMIncreaseTime(seconds) { 85 | return new Promise((resolve, reject) => { 86 | ethRPC.sendAsync({ 87 | method: 'evm_increaseTime', 88 | params: [seconds], 89 | }, async (increaseTimeErr) => { 90 | if (increaseTimeErr) { reject(increaseTimeErr); } 91 | resolve(); 92 | }); 93 | }); 94 | } 95 | 96 | let snapshot = await getEVMSnapshot(); 97 | 98 | async function tranchWithdraw(tranches, beneficiary) { 99 | const tranch = tranches[0]; 100 | 101 | await makeEVMRevert(snapshot); 102 | snapshot = await getEVMSnapshot(); 103 | await makeEVMIncreaseTime(Number.parseInt(tranch.date, 10)); 104 | 105 | const beneficiaryStartingBalance = await utils.getTokenBalanceOf(beneficiary.address); 106 | const disburser = 107 | utils.getDisburserByBeneficiaryAndTranch(beneficiary.address, tranch); 108 | 109 | try { 110 | await utils.as( 111 | beneficiary.address, disburser.withdraw, 112 | beneficiary.address, new BN(tranch.amount, 10).toString(10), 113 | ); 114 | const beneficiaryBalance = await utils.getTokenBalanceOf(beneficiary.address); 115 | const expected = beneficiaryStartingBalance.add(new BN(tranch.amount, 10)); 116 | const errMsg = 'Beneficiary has an unaccountable balance'; 117 | assert.strictEqual(beneficiaryBalance.toString(10), expected.toString(10), errMsg); 118 | } catch (err) { 119 | if (utils.isSignerAccessFailure(err)) { 120 | console.log(signerAccessFailureFor(beneficiary.address)); 121 | } else { 122 | throw err; 123 | } 124 | } 125 | 126 | if (tranches.length === 1) { return undefined; } 127 | return tranchWithdraw(tranches.slice(1), beneficiary); 128 | } 129 | 130 | async function beneficiaryWithdraw(beneficiaries) { 131 | const beneficiary = beneficiaries[0]; 132 | const tranches = Object.keys(utils.getTranchesForBeneficiary(beneficiary.address)) 133 | .map(tranchIndex => utils.getTranchesForBeneficiary(beneficiary.address)[tranchIndex]); 134 | await tranchWithdraw(tranches, beneficiary); 135 | if (beneficiaries.length === 1) { return undefined; } 136 | return beneficiaryWithdraw(beneficiaries.slice(1)); 137 | } 138 | 139 | await beneficiaryWithdraw(utils.getTimelockedBeneficiaries()); 140 | }); 141 | }); 142 | }); 143 | 144 | -------------------------------------------------------------------------------- /test/owner_only.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* global artifacts assert contract */ 3 | 4 | const fs = require('fs'); 5 | const BN = require('bn.js'); 6 | const utils = require('./utils'); 7 | 8 | const Sale = artifacts.require('./Sale.sol'); 9 | 10 | const saleConf = JSON.parse(fs.readFileSync('./conf/sale.json')); 11 | const tokenConf = JSON.parse(fs.readFileSync('./conf/token.json')); 12 | 13 | contract('Sale', (accounts) => { 14 | const [owner, james, miguel] = accounts; 15 | 16 | describe('Owner-only functions', () => { 17 | const nonOwnerAccessError = 'A non-owner was able to'; 18 | const ownerAccessError = 'An owner was unable able to'; 19 | 20 | before(() => { 21 | saleConf.price = new BN(saleConf.price, 10); 22 | saleConf.startBlock = new BN(saleConf.startBlock, 10); 23 | tokenConf.initialAmount = new BN(tokenConf.initialAmount, 10); 24 | }); 25 | 26 | it('should not allow a non-owner to change the price.', async () => { 27 | const sale = await Sale.deployed(); 28 | try { 29 | await utils.as(james, sale.changePrice, saleConf.price + 1); 30 | } catch (err) { 31 | const errMsg = err.toString(); 32 | assert(utils.isEVMRevert(err), errMsg); 33 | } 34 | const price = await sale.price.call(); 35 | const expected = saleConf.price; 36 | const errMsg = `${nonOwnerAccessError} change the price`; 37 | assert.strictEqual(price.toString(10), expected.toString(10), errMsg); 38 | }); 39 | 40 | it('should not allow a non-owner to change the startBlock.', async () => { 41 | const sale = await Sale.deployed(); 42 | try { 43 | await utils.as(james, sale.changeStartBlock, saleConf.startBlock + 1); 44 | } catch (err) { 45 | const errMsg = err.toString(); 46 | assert(utils.isEVMRevert(err), errMsg); 47 | } 48 | const startBlock = await sale.startBlock.call(); 49 | const expected = saleConf.startBlock; 50 | const errMsg = `${nonOwnerAccessError} change the start block`; 51 | assert.strictEqual(startBlock.toString(10), expected.toString(10), errMsg); 52 | }); 53 | 54 | it('should not allow a non-owner to change the owner', async () => { 55 | const sale = await Sale.deployed(); 56 | try { 57 | await utils.as(james, sale.changeOwner, james); 58 | } catch (err) { 59 | const errMsg = err.toString(); 60 | assert(utils.isEVMRevert(err), errMsg); 61 | } 62 | const actualOwner = await sale.owner.call(); 63 | const expected = saleConf.owner.toLowerCase(); 64 | const errMsg = `${nonOwnerAccessError} change the owner`; 65 | assert.strictEqual(actualOwner.toString(), expected.toString(), errMsg); 66 | }); 67 | 68 | it('should not allow a non-owner to change the wallet', async () => { 69 | const sale = await Sale.deployed(); 70 | try { 71 | await utils.as(james, sale.changeWallet, james); 72 | } catch (err) { 73 | const errMsg = err.toString(); 74 | assert(utils.isEVMRevert(err), errMsg); 75 | } 76 | const wallet = await sale.wallet.call(); 77 | const expected = saleConf.wallet; 78 | const errMsg = `${nonOwnerAccessError} change the wallet`; 79 | assert.strictEqual(wallet.toString(), expected.toLowerCase(), errMsg); 80 | }); 81 | 82 | it('should not allow a non-owner to activate the emergencyToggle', async () => { 83 | const sale = await Sale.deployed(); 84 | try { 85 | await utils.as(james, sale.emergencyToggle); 86 | } catch (err) { 87 | const errMsg = err.toString(); 88 | assert(utils.isEVMRevert(err), errMsg); 89 | } 90 | const emergencyFlag = await sale.emergencyFlag.call(); 91 | const expected = false; 92 | const errMsg = `${nonOwnerAccessError} change the emergencyToggle`; 93 | assert.strictEqual(emergencyFlag, expected, errMsg); 94 | }); 95 | 96 | it('should change the owner to miguel.', async () => { 97 | const sale = await Sale.deployed(); 98 | await utils.as(saleConf.owner, sale.changeOwner, miguel); 99 | const actualOwner = await sale.owner.call(); 100 | const expected = miguel; 101 | const errMsg = `${ownerAccessError} change the owner`; 102 | assert.strictEqual(actualOwner, expected, errMsg); 103 | await utils.as(miguel, sale.changeOwner, saleConf.owner); 104 | }); 105 | 106 | it('should change the price to 2666.', async () => { 107 | const sale = await Sale.deployed(); 108 | await utils.as(owner, sale.changePrice, 2666); 109 | const price = await sale.price.call(); 110 | const expected = 2666; 111 | const errMsg = `${ownerAccessError} change the price`; 112 | assert.strictEqual(price.toString(10), expected.toString(10), errMsg); 113 | await utils.as(owner, sale.changePrice, saleConf.price.toString(10)); 114 | }); 115 | 116 | it('should change the startBlock to 2666.', async () => { 117 | const sale = await Sale.deployed(); 118 | await utils.as(owner, sale.changeStartBlock, 2666); 119 | const price = await sale.startBlock.call(); 120 | const expected = 2666; 121 | const errMsg = `${ownerAccessError} change the start block`; 122 | assert.strictEqual(price.toString(10), expected.toString(10), errMsg); 123 | await utils.as(owner, sale.changeStartBlock, saleConf.startBlock.toString(10)); 124 | }); 125 | 126 | it('should change the wallet address', async () => { 127 | const newWallet = '0x0000000000000000000000000000000000000001'; 128 | const sale = await Sale.deployed(); 129 | await utils.as(owner, sale.changeWallet, newWallet); 130 | const wallet = await sale.wallet.call(); 131 | const expected = newWallet; 132 | const errMsg = `${ownerAccessError} change the wallet address`; 133 | assert.strictEqual(wallet, expected, errMsg); 134 | await utils.as(owner, sale.changeWallet, saleConf.wallet); 135 | }); 136 | 137 | it('should activate the emergencyFlag.', async () => { 138 | const sale = await Sale.deployed(); 139 | await utils.as(owner, sale.emergencyToggle); 140 | const emergencyFlag = await sale.emergencyFlag.call(); 141 | const expected = true; 142 | const errMsg = `${ownerAccessError} set the emergency toggle`; 143 | assert.strictEqual(emergencyFlag.valueOf(), expected, errMsg); 144 | await utils.as(owner, sale.emergencyToggle); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /contracts/Sale.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.11; 2 | import "tokens/eip20/EIP20.sol"; 3 | import "./Disbursement.sol"; 4 | 5 | contract Sale { 6 | 7 | /* 8 | * Events 9 | */ 10 | 11 | event PurchasedTokens(address indexed purchaser, uint amount); 12 | event TransferredPreBuyersReward(address indexed preBuyer, uint amount); 13 | event TransferredTimelockedTokens(address beneficiary, address disburser, uint amount); 14 | 15 | /* 16 | * Storage 17 | */ 18 | 19 | address public owner; 20 | address public wallet; 21 | EIP20 public token; 22 | uint public price; 23 | uint public startBlock; 24 | uint public freezeBlock; 25 | 26 | uint public totalPreBuyers; 27 | uint public preBuyersDispensedTo = 0; 28 | uint public totalTimelockedBeneficiaries; 29 | uint public timeLockedBeneficiariesDisbursedTo = 0; 30 | 31 | bool public emergencyFlag = false; 32 | bool public preSaleTokensDisbursed = false; 33 | bool public timelockedTokensDisbursed = false; 34 | 35 | /* 36 | * Modifiers 37 | */ 38 | 39 | modifier saleStarted { 40 | require(block.number >= startBlock); 41 | _; 42 | } 43 | 44 | modifier onlyOwner { 45 | require(msg.sender == owner); 46 | _; 47 | } 48 | 49 | modifier notFrozen { 50 | require(block.number < freezeBlock); 51 | _; 52 | } 53 | 54 | modifier setupComplete { 55 | require(preSaleTokensDisbursed && timelockedTokensDisbursed); 56 | _; 57 | } 58 | 59 | modifier notInEmergency { 60 | require(emergencyFlag == false); 61 | _; 62 | } 63 | 64 | /* 65 | * Public functions 66 | */ 67 | 68 | /// @dev Sale(): constructor for Sale contract 69 | /// @param _owner the address which owns the sale, can access owner-only functions 70 | /// @param _wallet the sale's beneficiary address 71 | /// @param _tokenSupply the total number of tokens to mint 72 | /// @param _tokenName the token's human-readable name 73 | /// @param _tokenDecimals the number of display decimals in token balances 74 | /// @param _tokenSymbol the token's human-readable asset symbol 75 | /// @param _price price of the token in Wei 76 | /// @param _startBlock the block at which this contract will begin selling its token balance 77 | function Sale( 78 | address _owner, 79 | address _wallet, 80 | uint256 _tokenSupply, 81 | string _tokenName, 82 | uint8 _tokenDecimals, 83 | string _tokenSymbol, 84 | uint _price, 85 | uint _startBlock, 86 | uint _freezeBlock, 87 | uint _totalPreBuyers, 88 | uint _totalTimelockedBeneficiaries 89 | ) 90 | public 91 | { 92 | owner = _owner; 93 | wallet = _wallet; 94 | token = new EIP20(_tokenSupply, _tokenName, _tokenDecimals, _tokenSymbol); 95 | price = _price; 96 | startBlock = _startBlock; 97 | freezeBlock = _freezeBlock; 98 | totalPreBuyers = _totalPreBuyers; 99 | totalTimelockedBeneficiaries = _totalTimelockedBeneficiaries; 100 | 101 | token.transfer(this, token.totalSupply()); 102 | assert(token.balanceOf(this) == token.totalSupply()); 103 | assert(token.balanceOf(this) == _tokenSupply); 104 | } 105 | 106 | /// @dev distributePreBuyersRewards(): private utility function called by constructor 107 | /// @param _preBuyers an array of addresses to which awards will be distributed 108 | /// @param _preBuyersTokens an array of integers specifying preBuyers rewards 109 | function distributePreBuyersRewards( 110 | address[] _preBuyers, 111 | uint[] _preBuyersTokens 112 | ) 113 | public 114 | onlyOwner 115 | { 116 | assert(!preSaleTokensDisbursed); 117 | 118 | for(uint i = 0; i < _preBuyers.length; i++) { 119 | token.transfer(_preBuyers[i], _preBuyersTokens[i]); 120 | preBuyersDispensedTo += 1; 121 | TransferredPreBuyersReward(_preBuyers[i], _preBuyersTokens[i]); 122 | } 123 | 124 | if(preBuyersDispensedTo == totalPreBuyers) { 125 | preSaleTokensDisbursed = true; 126 | } 127 | } 128 | 129 | /// @dev distributeTimelockedTokens(): private utility function called by constructor 130 | /// @param _beneficiaries an array of addresses specifying disbursement beneficiaries 131 | /// @param _beneficiariesTokens an array of integers specifying disbursement amounts 132 | /// @param _timelocks an array of UNIX timestamps specifying vesting dates 133 | /// @param _periods an array of durations in seconds specifying vesting periods 134 | function distributeTimelockedTokens( 135 | address[] _beneficiaries, 136 | uint[] _beneficiariesTokens, 137 | uint[] _timelocks, 138 | uint[] _periods 139 | ) 140 | public 141 | onlyOwner 142 | { 143 | assert(preSaleTokensDisbursed); 144 | assert(!timelockedTokensDisbursed); 145 | 146 | for(uint i = 0; i < _beneficiaries.length; i++) { 147 | address beneficiary = _beneficiaries[i]; 148 | uint beneficiaryTokens = _beneficiariesTokens[i]; 149 | 150 | Disbursement disbursement = new Disbursement( 151 | beneficiary, 152 | _periods[i], 153 | _timelocks[i] 154 | ); 155 | 156 | disbursement.setup(token); 157 | token.transfer(disbursement, beneficiaryTokens); 158 | timeLockedBeneficiariesDisbursedTo += 1; 159 | 160 | TransferredTimelockedTokens(beneficiary, disbursement, beneficiaryTokens); 161 | } 162 | 163 | if(timeLockedBeneficiariesDisbursedTo == totalTimelockedBeneficiaries) { 164 | timelockedTokensDisbursed = true; 165 | } 166 | } 167 | 168 | /// @dev purchaseToken(): function that exchanges ETH for tokens (main sale function) 169 | /// @notice You're about to purchase the equivalent of `msg.value` Wei in tokens 170 | function purchaseTokens() 171 | saleStarted 172 | setupComplete 173 | notInEmergency 174 | payable 175 | public 176 | { 177 | /* Calculate whether any of the msg.value needs to be returned to 178 | the sender. The purchaseAmount is the actual number of tokens which 179 | will be purchased. */ 180 | uint purchaseAmount = msg.value / price; 181 | uint excessAmount = msg.value % price; 182 | 183 | // Cannot purchase more tokens than this contract has available to sell 184 | require(purchaseAmount <= token.balanceOf(this)); 185 | 186 | // Return any excess msg.value 187 | if (excessAmount > 0) { 188 | msg.sender.transfer(excessAmount); 189 | } 190 | 191 | // Forward received ether minus any excessAmount to the wallet 192 | wallet.transfer(this.balance); 193 | 194 | // Transfer the sum of tokens tokenPurchase to the msg.sender 195 | token.transfer(msg.sender, purchaseAmount); 196 | 197 | PurchasedTokens(msg.sender, purchaseAmount); 198 | } 199 | 200 | /* 201 | * Owner-only functions 202 | */ 203 | 204 | function changeOwner(address _newOwner) 205 | onlyOwner 206 | public 207 | { 208 | require(_newOwner != 0); 209 | owner = _newOwner; 210 | } 211 | 212 | function changePrice(uint _newPrice) 213 | onlyOwner 214 | notFrozen 215 | public 216 | { 217 | require(_newPrice != 0); 218 | price = _newPrice; 219 | } 220 | 221 | function changeWallet(address _wallet) 222 | onlyOwner 223 | notFrozen 224 | public 225 | { 226 | require(_wallet != 0); 227 | wallet = _wallet; 228 | } 229 | 230 | function changeStartBlock(uint _newBlock) 231 | onlyOwner 232 | notFrozen 233 | public 234 | { 235 | require(_newBlock != 0); 236 | 237 | freezeBlock = _newBlock - (startBlock - freezeBlock); 238 | startBlock = _newBlock; 239 | } 240 | 241 | function emergencyToggle() 242 | onlyOwner 243 | public 244 | { 245 | emergencyFlag = !emergencyFlag; 246 | } 247 | 248 | } 249 | --------------------------------------------------------------------------------