Repository: decentraland/mana Branch: master Commit: d92bba7f43d8 Files: 32 Total size: 36.6 KB Directory structure: gitextract_8qja0b87/ ├── .babelrc ├── .circleci/ │ └── config.yml ├── .editorconfig ├── .eslintrc ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── contracts/ │ ├── BurnableToken.sol │ ├── ContinuousSale.sol │ ├── MANAContinuousSale.sol │ ├── MANACrowdsale.sol │ ├── MANAToken.sol │ ├── Migrations.sol │ └── WhitelistedCrowdsale.sol ├── flow-typed/ │ └── defs.js ├── migrations/ │ ├── 1_initial_migration.js │ └── 2_deploy_contracts.js ├── package.json ├── scripts/ │ └── test.sh ├── test/ │ ├── BurnableToken.js │ ├── MANAContinuousSale.js │ ├── MANACrowdsale.js │ ├── MANAToken.js │ ├── WhitelistedCrowdsale.js │ ├── helpers/ │ │ ├── BurnableTokenMock.sol │ │ └── WhitelistedCrowdsaleImpl.sol │ └── utils.js └── truffle.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["flow", "env"] } ================================================ FILE: .circleci/config.yml ================================================ version: 2 jobs: build: docker: # specify the version you desire here - image: circleci/node:10.2-stretch working_directory: ~/repo steps: - checkout # Download and cache dependencies - restore_cache: keys: - v1-dependencies-{{ checksum "package-lock.json" }} # fallback to using the latest cache if no exact match is found - v1-dependencies- - run: name: Install packages command: npm install - save_cache: paths: - node_modules key: v1-dependencies-{{ checksum "package-lock.json" }} # run tests! - run: name: Run tests command: npm run test release: docker: # specify the version you desire here - image: circleci/node:10.2-stretch working_directory: ~/repo steps: - checkout - restore_cache: keys: - v1-dependencies-{{ checksum "package-lock.json" }} # fallback to using the latest cache if no exact match is found - v1-dependencies- - run: name: Install packages command: npm install # Compile contracts! - run: name: Compile contracts command: npm run compile - run: name: Semantic release command: npm run semantic-release workflows: version: 2 test-build-release: jobs: - build - release: requires: - build filters: branches: only: - master ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 [*.sol] indent_style = space indent_size = 4 ================================================ FILE: .eslintrc ================================================ { "extends": ["standard"], "env": { "node": true, "jasmine": true }, "globals": { "web3": false, "artifacts": false, "contract": false } } ================================================ FILE: .flowconfig ================================================ [ignore] [include] [libs] [options] [lints] ================================================ FILE: .gitattributes ================================================ *.sol linguist-language=Solidity *.js linguist-vendored ================================================ FILE: .gitignore ================================================ node_modules/ build/ ================================================ FILE: .npmignore ================================================ migrations test zos.* scripts flow-typed ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "7" cache: directories: - "node_modules" ================================================ FILE: LICENSE ================================================ Copyright 2017 The Decentraland Team Licensed under the Apache License, Version 2.0 (the "License"); you may not use files included in this repository except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ ![](https://decentraland.org/favicon.ico) Decentraland MANA Token ======================= [![Build Status](https://travis-ci.org/decentraland/mana.svg?branch=master)](https://travis-ci.org/decentraland/mana) Solidity smart contracts for the Decentraland Crowdsale and MANA Token ================================================ FILE: contracts/BurnableToken.sol ================================================ pragma solidity ^0.4.11; import 'zeppelin-solidity/contracts/token/StandardToken.sol'; /** * @title Burnable Token * @dev A token that can be irreversibly burned. */ contract BurnableToken is StandardToken { event Burn(address indexed burner, uint256 value); /** * @dev Burns a specified amount of tokens. * @param _value The amount of tokens to burn. */ function burn(uint256 _value) public { require(_value > 0); address burner = msg.sender; balances[burner] = balances[burner].sub(_value); totalSupply = totalSupply.sub(_value); Burn(msg.sender, _value); } } ================================================ FILE: contracts/ContinuousSale.sol ================================================ pragma solidity ^0.4.11; import "zeppelin-solidity/contracts/token/MintableToken.sol"; /** * @title ContinuousSale * @dev ContinuousSale implements a contract for managing a continuous token sale */ contract ContinuousSale { using SafeMath for uint256; // time bucket size uint256 public constant BUCKET_SIZE = 12 hours; // the token being sold MintableToken public token; // address where funds are collected address public wallet; // amount of tokens emitted per wei uint256 public rate; // amount of raised money in wei uint256 public weiRaised; // max amount of tokens to mint per time bucket uint256 public issuance; // last time bucket from which tokens have been purchased uint256 public lastBucket = 0; // amount issued in the last bucket uint256 public bucketAmount = 0; event TokenPurchase(address indexed investor, address indexed beneficiary, uint256 weiAmount, uint256 tokens); function ContinuousSale( uint256 _rate, address _wallet, MintableToken _token ) { require(_rate != 0); require(_wallet != 0); // require(address(token) != 0x0); rate = _rate; wallet = _wallet; token = _token; } function() payable { buyTokens(msg.sender); } function buyTokens(address beneficiary) public payable { require(beneficiary != 0x0); require(msg.value != 0); prepareContinuousPurchase(); uint256 tokens = processPurchase(beneficiary); checkContinuousPurchase(tokens); } function prepareContinuousPurchase() internal { uint256 timestamp = block.timestamp; uint256 bucket = timestamp - (timestamp % BUCKET_SIZE); if (bucket > lastBucket) { lastBucket = bucket; bucketAmount = 0; } } function checkContinuousPurchase(uint256 tokens) internal { uint256 updatedBucketAmount = bucketAmount.add(tokens); require(updatedBucketAmount <= issuance); bucketAmount = updatedBucketAmount; } function processPurchase(address beneficiary) internal returns(uint256) { uint256 weiAmount = msg.value; // calculate token amount to be created uint256 tokens = weiAmount.mul(rate); // update state weiRaised = weiRaised.add(weiAmount); token.mint(beneficiary, tokens); TokenPurchase(msg.sender, beneficiary, weiAmount, tokens); forwardFunds(); return tokens; } function forwardFunds() internal { wallet.transfer(msg.value); } } ================================================ FILE: contracts/MANAContinuousSale.sol ================================================ pragma solidity ^0.4.11; import "zeppelin-solidity/contracts/ownership/Ownable.sol"; import "./ContinuousSale.sol"; import "./MANAToken.sol"; contract MANAContinuousSale is ContinuousSale, Ownable { uint256 public constant INFLATION = 8; bool public started = false; event RateChange(uint256 amount); event WalletChange(address wallet); function MANAContinuousSale( uint256 _rate, address _wallet, MintableToken _token ) ContinuousSale(_rate, _wallet, _token) { } modifier whenStarted() { require(started); _; } function start() onlyOwner { require(!started); // initialize issuance uint256 finalSupply = token.totalSupply(); uint256 annualIssuance = finalSupply.mul(INFLATION).div(100); issuance = annualIssuance.mul(BUCKET_SIZE).div(1 years); started = true; } function buyTokens(address beneficiary) whenStarted public payable { super.buyTokens(beneficiary); } function setWallet(address _wallet) onlyOwner { require(_wallet != 0x0); wallet = _wallet; WalletChange(_wallet); } function setRate(uint256 _rate) onlyOwner { rate = _rate; RateChange(_rate); } function unpauseToken() onlyOwner { MANAToken(token).unpause(); } function pauseToken() onlyOwner { MANAToken(token).pause(); } } ================================================ FILE: contracts/MANACrowdsale.sol ================================================ pragma solidity ^0.4.11; import "zeppelin-solidity/contracts/crowdsale/CappedCrowdsale.sol"; import "zeppelin-solidity/contracts/crowdsale/Crowdsale.sol"; import "zeppelin-solidity/contracts/crowdsale/FinalizableCrowdsale.sol"; import "./WhitelistedCrowdsale.sol"; import "./MANAContinuousSale.sol"; import "./MANAToken.sol"; contract MANACrowdsale is WhitelistedCrowdsale, CappedCrowdsale, FinalizableCrowdsale { uint256 public constant TOTAL_SHARE = 100; uint256 public constant CROWDSALE_SHARE = 40; uint256 public constant FOUNDATION_SHARE = 60; // price at which whitelisted buyers will be able to buy tokens uint256 public preferentialRate; // customize the rate for each whitelisted buyer mapping (address => uint256) public buyerRate; // initial rate at which tokens are offered uint256 public initialRate; // end rate at which tokens are offered uint256 public endRate; // continuous crowdsale contract MANAContinuousSale public continuousSale; event WalletChange(address wallet); event PreferentialRateChange(address indexed buyer, uint256 rate); event InitialRateChange(uint256 rate); event EndRateChange(uint256 rate); function MANACrowdsale( uint256 _startBlock, uint256 _endBlock, uint256 _initialRate, uint256 _endRate, uint256 _preferentialRate, address _wallet ) CappedCrowdsale(86206 ether) WhitelistedCrowdsale() FinalizableCrowdsale() Crowdsale(_startBlock, _endBlock, _initialRate, _wallet) { require(_initialRate > 0); require(_endRate > 0); require(_preferentialRate > 0); initialRate = _initialRate; endRate = _endRate; preferentialRate = _preferentialRate; continuousSale = createContinuousSaleContract(); MANAToken(token).pause(); } function createTokenContract() internal returns(MintableToken) { return new MANAToken(); } function createContinuousSaleContract() internal returns(MANAContinuousSale) { return new MANAContinuousSale(rate, wallet, token); } function setBuyerRate(address buyer, uint256 rate) onlyOwner public { require(rate != 0); require(isWhitelisted(buyer)); require(block.number < startBlock); buyerRate[buyer] = rate; PreferentialRateChange(buyer, rate); } function setInitialRate(uint256 rate) onlyOwner public { require(rate != 0); require(block.number < startBlock); initialRate = rate; InitialRateChange(rate); } function setEndRate(uint256 rate) onlyOwner public { require(rate != 0); require(block.number < startBlock); endRate = rate; EndRateChange(rate); } function getRate() internal returns(uint256) { // some early buyers are offered a discount on the crowdsale price if (buyerRate[msg.sender] != 0) { return buyerRate[msg.sender]; } // whitelisted buyers can purchase at preferential price before crowdsale ends if (isWhitelisted(msg.sender)) { return preferentialRate; } // otherwise compute the price for the auction uint256 elapsed = block.number - startBlock; uint256 rateRange = initialRate - endRate; uint256 blockRange = endBlock - startBlock; return initialRate.sub(rateRange.mul(elapsed).div(blockRange)); } // low level token purchase function function buyTokens(address beneficiary) payable { require(beneficiary != 0x0); require(validPurchase()); uint256 weiAmount = msg.value; uint256 updatedWeiRaised = weiRaised.add(weiAmount); uint256 rate = getRate(); // calculate token amount to be created uint256 tokens = weiAmount.mul(rate); // update state weiRaised = updatedWeiRaised; token.mint(beneficiary, tokens); TokenPurchase(msg.sender, beneficiary, weiAmount, tokens); forwardFunds(); } function setWallet(address _wallet) onlyOwner public { require(_wallet != 0x0); wallet = _wallet; continuousSale.setWallet(_wallet); WalletChange(_wallet); } function unpauseToken() onlyOwner { require(isFinalized); MANAToken(token).unpause(); } function pauseToken() onlyOwner { require(isFinalized); MANAToken(token).pause(); } function beginContinuousSale() onlyOwner public { require(isFinalized); token.transferOwnership(continuousSale); continuousSale.start(); continuousSale.transferOwnership(owner); } function finalization() internal { uint256 totalSupply = token.totalSupply(); uint256 finalSupply = TOTAL_SHARE.mul(totalSupply).div(CROWDSALE_SHARE); // emit tokens for the foundation token.mint(wallet, FOUNDATION_SHARE.mul(finalSupply).div(TOTAL_SHARE)); // NOTE: cannot call super here because it would finish minting and // the continuous sale would not be able to proceed } } ================================================ FILE: contracts/MANAToken.sol ================================================ pragma solidity ^0.4.11; import "zeppelin-solidity/contracts/token/PausableToken.sol"; import "zeppelin-solidity/contracts/token/MintableToken.sol"; import "./BurnableToken.sol"; contract MANAToken is BurnableToken, PausableToken, MintableToken { string public constant symbol = "MANA"; string public constant name = "Decentraland MANA"; uint8 public constant decimals = 18; function burn(uint256 _value) whenNotPaused public { super.burn(_value); } } ================================================ FILE: contracts/Migrations.sol ================================================ pragma solidity ^0.4.4; contract Migrations { address public owner; uint public last_completed_migration; modifier restricted() { if (msg.sender == owner) _; } function Migrations() { owner = msg.sender; } function setCompleted(uint completed) restricted { last_completed_migration = completed; } function upgrade(address new_address) restricted { Migrations upgraded = Migrations(new_address); upgraded.setCompleted(last_completed_migration); } } ================================================ FILE: contracts/WhitelistedCrowdsale.sol ================================================ pragma solidity ^0.4.11; import 'zeppelin-solidity/contracts/math/SafeMath.sol'; import 'zeppelin-solidity/contracts/crowdsale/Crowdsale.sol'; /** * @title WhitelistedCrowdsale * @dev Extension of Crowsdale where an owner can whitelist addresses * which can buy in crowdsale before it opens to the public */ contract WhitelistedCrowdsale is Crowdsale, Ownable { using SafeMath for uint256; // list of addresses that can purchase before crowdsale opens mapping (address => bool) public whitelist; function addToWhitelist(address buyer) public onlyOwner { require(buyer != 0x0); whitelist[buyer] = true; } // @return true if buyer is whitelisted function isWhitelisted(address buyer) public constant returns (bool) { return whitelist[buyer]; } // overriding Crowdsale#validPurchase to add whitelist logic // @return true if buyers can buy at the moment function validPurchase() internal constant returns (bool) { // [TODO] issue with overriding and associativity of logical operators return super.validPurchase() || (!hasEnded() && isWhitelisted(msg.sender)); } } ================================================ FILE: flow-typed/defs.js ================================================ declare var contract: (string, Function) => void declare var it: (string, Function) => void declare var describe: (string, Function) => void declare var beforeEach: Function => void declare var artifacts: { require: string => any } declare var web3: any ================================================ FILE: migrations/1_initial_migration.js ================================================ var Migrations = artifacts.require("./Migrations.sol"); module.exports = function(deployer) { deployer.deploy(Migrations); }; ================================================ FILE: migrations/2_deploy_contracts.js ================================================ const MANACrowdsale = artifacts.require("./MANACrowdsale.sol"); module.exports = function(deployer) { // deployer.deploy(MANACrowdsale); }; ================================================ FILE: package.json ================================================ { "name": "decentraland-mana", "version": "0.0.0-semantic-release", "description": "Solidity Contracts for the Decentraland MANA Token", "main": "y", "directories": { "test": "test" }, "dependencies": { "sha3": "^1.2.1", "truffle-hdwallet-provider": "0.0.3", "zeppelin-solidity": "1.2.0" }, "devDependencies": { "babel-polyfill": "^6.26.0", "babel-preset-env": "^1.6.1", "babel-preset-flow": "^6.23.0", "babel-register": "^6.26.0", "chai": "^4.1.2", "chai-as-promised": "^7.1.1", "chai-bignumber": "^2.0.2", "eslint": "^4.19.1", "eslint-config-standard": "^10.2.1", "eslint-plugin-import": "^2.11.0", "eslint-plugin-node": "^5.2.1", "eslint-plugin-promise": "^3.7.0", "eslint-plugin-standard": "^3.1.0", "ganache-cli": "^6.1.0", "husky": "^1.3.1", "semantic-release": "^15.13.3", "truffle": "^3.4.11", "validate-commit-msg": "^2.14.0" }, "scripts": { "test": "./scripts/test.sh", "eslint": "eslint test/", "semantic-release": "semantic-release", "commit-msg": "validate-commit-msg", "compile": "truffle compile" }, "repository": { "type": "git", "url": "git+https://github.com/decentraland/mana.git" }, "keywords": [ "decentraland", "mana", "solidity", "truffle", "crowdsale" ], "author": "Federico Bond", "license": "MIT", "bugs": { "url": "https://github.com/decentraland/mana/issues" }, "homepage": "https://github.com/decentraland/mana#readme" } ================================================ FILE: scripts/test.sh ================================================ #!/usr/bin/env bash # Executes cleanup function at script exit. trap cleanup EXIT cleanup() { # Kill the testrpc instance that we started (if we started one and if it's still running). if [ -n "$testrpc_pid" ] && ps -p $testrpc_pid > /dev/null; then kill -9 $testrpc_pid fi } testrpc_running() { nc -z localhost 8545 } if testrpc_running; then echo "Using existing testrpc instance" else echo "Starting our own testrpc instance" # We define 10 accounts with balance 1M ether, needed for high-value tests. ganache-cli -l 10000000 \ --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200,1000000000000000000000000" \ --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201,1000000000000000000000000" \ --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202,1000000000000000000000000" \ --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203,1000000000000000000000000" \ --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204,1000000000000000000000000" \ --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205,1000000000000000000000000" \ --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206,1000000000000000000000000" \ --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207,1000000000000000000000000" \ --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208,1000000000000000000000000" \ --account="0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209,1000000000000000000000000" \ > /dev/null & testrpc_pid=$! fi node_modules/.bin/truffle test "$@" ================================================ FILE: test/BurnableToken.js ================================================ // @flow 'use strict' const { should, EVMThrow } = require('./utils.js') const BurnableTokenMock = artifacts.require('./helpers/BurnableTokenMock.sol') const BigNumber = web3.BigNumber contract('BurnableToken', function (accounts) { let token let expectedTokenSupply = new BigNumber(900) beforeEach(async function () { token = await BurnableTokenMock.new(accounts[1], 1000) }) it('owner should be able to burn tokens', async function () { const { logs } = await token.burn(100, { from: accounts[1] }) const balance = await token.balanceOf(accounts[1]) balance.should.be.bignumber.equal(expectedTokenSupply) const totalSupply = await token.totalSupply() totalSupply.should.be.bignumber.equal(expectedTokenSupply) const event = logs.find(e => e.event === 'Burn') should.exist(event) }) it('cannot burn more tokens that you have', async function () { await token.burn(2000, { from: accounts[1] }) .should.be.rejectedWith(EVMThrow) }) }) ================================================ FILE: test/MANAContinuousSale.js ================================================ // @flow 'use strict' const expect = require('chai').expect const { advanceTime, EVMRevert } = require('./utils') const MANAContinuousSale = artifacts.require('./MANAContinuousSale.sol') const MANAToken = artifacts.require('./MANAToken.sol') const BigNumber = web3.BigNumber contract('MANAContinuousSale', function([owner, wallet, buyer, wallet2]) { const rate = new BigNumber(1) const newRate = new BigNumber(2) const value = new BigNumber(100) let token, sale beforeEach(async function() { token = await MANAToken.new() await token.pause() await token.mint(owner, new BigNumber(1000000)) sale = await MANAContinuousSale.new(rate, wallet, token.address) await token.transferOwnership(sale.address) }) it('should start with continuous sale disabled', async function() { let started = await sale.started() started.should.equal(false) await sale.start() started = await sale.started() started.should.equal(true) }) it('owner should be able to pause/unpause token', async function() { await sale.unpauseToken().should.be.fulfilled let paused = await token.paused() paused.should.equal(false) await sale.pauseToken().should.be.fulfilled paused = await token.paused() paused.should.equal(true) }) it('non-owners should not be able to pause/unpause token', async function() { await sale.unpauseToken({ from: buyer }).should.be.rejectedWith(EVMRevert) await sale.unpauseToken({ from: owner }) await sale.pauseToken({ from: buyer }).should.be.rejectedWith(EVMRevert) }) it('should accept payments only if sale has started', async function() { await sale.send(value, { from: buyer }).should.be.rejectedWith(EVMRevert) await sale.start().should.be.fulfilled await sale.buyTokens(buyer, { value: new BigNumber(100), from: buyer }).should.be.fulfilled }) it('only owner can change rate', async function() { await sale .setRate(newRate, { from: buyer }) .should.be.rejectedWith(EVMRevert) const { logs } = await sale.setRate(newRate) const event = logs.find(e => e.event === 'RateChange') expect(event).to.exist const updatedRate = await sale.rate() updatedRate.should.be.bignumber.equal(newRate) }) it('only owner can change wallet', async function() { await sale .setRate(newRate, { from: buyer }) .should.be.rejectedWith(EVMRevert) const { logs } = await sale.setRate(newRate) const event = logs.find(e => e.event === 'RateChange') expect(event).to.exist const updatedRate = await sale.rate() updatedRate.should.be.bignumber.equal(newRate) }) it('should reject payments if amount of tokens is bigger than block bucket', async function() { await sale.start() await sale .send(new BigNumber(1000), { from: buyer }) .should.be.rejectedWith(EVMRevert) }) it('should start with a fixed issuance rate', async function() { // total tokens emitted are 1000000 so issuance must be: // // seconds in 12 hours: 43200 // seconds in a year: 31536000 // 1000000 * 0.08 * 43200 / 31536000 = 109 let issuance = await sale.issuance() issuance.should.be.bignumber.equal(new BigNumber(0)) await sale.start() issuance = await sale.issuance() issuance.should.be.bignumber.equal(new BigNumber(109)) }) it('should handle time buckets for token issuance', async function() { await sale.start() await sale.buyTokens(buyer, { value, from: buyer }).should.be.fulfilled await sale .buyTokens(buyer, { value, from: buyer }) .should.be.rejectedWith(EVMRevert) const bucketSize = await sale.BUCKET_SIZE() await advanceTime(bucketSize) await sale.buyTokens(buyer, { value, from: buyer }).should.be.fulfilled }) }) ================================================ FILE: test/MANACrowdsale.js ================================================ // @flow 'use strict' const expect = require('chai').expect const { advanceToBlock, ether, should, EVMRevert } = require('./utils') const MANACrowdsale = artifacts.require('./MANACrowdsale.sol') const MANAContinuousSale = artifacts.require('./MANAContinuousSale.sol') const MANAToken = artifacts.require('./MANAToken.sol') const BigNumber = web3.BigNumber contract('MANACrowdsale', function([ _, wallet, wallet2, buyer, purchaser, buyer2, purchaser2 ]) { const initialRate = new BigNumber(1000) const endRate = new BigNumber(900) const newRate = new BigNumber(500) const preferentialRate = new BigNumber(2000) const value = ether(1) const expectedFoundationTokens = new BigNumber(6000) const expectedTokenSupply = new BigNumber(10000) let startBlock, endBlock let crowdsale, token beforeEach(async function() { startBlock = web3.eth.blockNumber + 10 endBlock = web3.eth.blockNumber + 20 crowdsale = await MANACrowdsale.new( startBlock, endBlock, initialRate, endRate, preferentialRate, wallet ) token = MANAToken.at(await crowdsale.token()) }) it('starts with token paused', async function() { const paused = await token.paused() paused.should.equal(true) }) it('owner should be able to change wallet', async function() { await crowdsale.setWallet(wallet2) let wallet = await crowdsale.wallet() wallet.should.equal(wallet2) const continuousSale = MANAContinuousSale.at( await crowdsale.continuousSale() ) wallet = await continuousSale.wallet() wallet.should.equal(wallet2) }) it('non-owner should not be able to change wallet', async function() { await crowdsale .setWallet(wallet2, { from: purchaser }) .should.be.rejectedWith(EVMRevert) }) it('owner should be able to start continuous sale', async function() { await crowdsale.beginContinuousSale().should.be.rejectedWith(EVMRevert) await advanceToBlock(endBlock) await crowdsale.finalize() const sale = MANAContinuousSale.at(await crowdsale.continuousSale()) let started = await sale.started() started.should.equal(false) await crowdsale.beginContinuousSale().should.be.fulfilled started = await sale.started() started.should.equal(true) }) it('owner should be able to unpause token after crowdsale ends', async function() { await advanceToBlock(endBlock) await crowdsale.unpauseToken().should.be.rejectedWith(EVMRevert) await crowdsale.finalize() let paused = await token.paused() paused.should.equal(true) await crowdsale.unpauseToken() paused = await token.paused() paused.should.equal(false) }) it('non-owners should not be able to start continuous sale', async function() { await crowdsale .beginContinuousSale({ from: purchaser }) .should.be.rejectedWith(EVMRevert) }) describe('rate during auction should decrease at a fixed step every block', async function() { let balance, startBlock, endBlock let initialRate = 9166 let endRate = 5500 let preferentialRate = initialRate const rateAtBlock10 = new BigNumber(9165) const rateAtBlock20 = new BigNumber(9164) const rateAtBlock100 = new BigNumber(9155) const rateAtBlock2 = new BigNumber(9166) const rateAtBlock10000 = new BigNumber(7973) const rateAtBlock30000 = new BigNumber(5586) beforeEach(async function() { startBlock = web3.eth.blockNumber + 10 endBlock = web3.eth.blockNumber + 10 + 30720 crowdsale = await MANACrowdsale.new( startBlock, endBlock, initialRate, endRate, preferentialRate, wallet ) token = MANAToken.at(await crowdsale.token()) }) it('at start', async function() { await advanceToBlock(startBlock - 1) await crowdsale.buyTokens(buyer, { value, from: purchaser }) balance = await token.balanceOf(buyer) balance.should.be.bignumber.equal(value.mul(initialRate)) }) it('at block 10', async function() { await advanceToBlock(startBlock + 9) await crowdsale.buyTokens(buyer2, { value, from: purchaser2 }) balance = await token.balanceOf(buyer2) balance.should.be.bignumber.equal(value.mul(rateAtBlock10)) }) it('at block 20', async function() { await advanceToBlock(startBlock + 19) await crowdsale.buyTokens(buyer2, { value, from: purchaser2 }) balance = await token.balanceOf(buyer2) balance.should.be.bignumber.equal(value.mul(rateAtBlock20)) }) it('at block 100', async function() { await advanceToBlock(startBlock + 99) await crowdsale.buyTokens(buyer2, { value, from: purchaser2 }) balance = await token.balanceOf(buyer2) balance.should.be.bignumber.equal(value.mul(rateAtBlock100)) }) it('at block 2', async function() { await advanceToBlock(startBlock + 1) await crowdsale.buyTokens(buyer2, { value, from: purchaser2 }) balance = await token.balanceOf(buyer2) balance.should.be.bignumber.equal(value.mul(rateAtBlock2)) }) it.skip('at block 10000', async function() { await advanceToBlock(startBlock + 9999) await crowdsale.buyTokens(buyer2, { value, from: purchaser2 }) balance = await token.balanceOf(buyer2) balance.should.be.bignumber.equal(value.mul(rateAtBlock10000)) }) it.skip('at block 30000', async function() { await advanceToBlock(startBlock + 29999) await crowdsale.buyTokens(buyer2, { value, from: purchaser2 }) balance = await token.balanceOf(buyer2) balance.should.be.bignumber.equal(value.mul(rateAtBlock30000)) }) }) it('whitelisted buyers should access tokens at reduced price until end of auction', async function() { await crowdsale.addToWhitelist(buyer) await crowdsale.buyTokens(buyer, { value, from: buyer }) const balance = await token.balanceOf(buyer) balance.should.be.bignumber.equal(value.mul(preferentialRate)) }) it('whitelisted big whale investor should not exceed the cap', async function() { const cap = await crowdsale.cap() const overCap = cap.mul(2) await crowdsale.addToWhitelist(buyer) await crowdsale .buyTokens(buyer, { value: overCap, from: buyer }) .should.be.rejectedWith(EVMRevert) const balance = await token.balanceOf(buyer) const raised = await crowdsale.weiRaised() balance.should.be.bignumber.equal(0) raised.should.be.bignumber.most(cap) }) it('owner can set the price for a particular buyer', async function() { await crowdsale.addToWhitelist(buyer) const preferentialRateForBuyer = new BigNumber(200) const { logs } = await crowdsale.setBuyerRate( buyer, preferentialRateForBuyer ) const event = logs.find(e => e.event === 'PreferentialRateChange') expect(event).to.exist await crowdsale.buyTokens(buyer, { value, from: buyer }) const balance = await token.balanceOf(buyer) balance.should.be.bignumber.equal(value.mul(preferentialRateForBuyer)) balance.should.not.be.bignumber.equal(value.mul(preferentialRate)) // cannot change rate after crowdsale starts await advanceToBlock(startBlock - 1) await crowdsale .setBuyerRate(buyer, preferentialRateForBuyer) .should.be.rejectedWith(EVMRevert) }) it('owner cannot set a custom rate before whitelisting a buyer', async function() { await crowdsale .setBuyerRate(buyer, new BigNumber(200)) .should.be.rejectedWith(EVMRevert) }) it('beneficiary is not the same as buyer', async function() { const beneficiary = buyer2 await crowdsale.addToWhitelist(buyer) await crowdsale.addToWhitelist(beneficiary) const preferentialRateForBuyer = new BigNumber(200) const invalidRate = new BigNumber(100) await crowdsale.setBuyerRate(buyer, preferentialRateForBuyer) await crowdsale.setBuyerRate(beneficiary, invalidRate) await crowdsale.buyTokens(beneficiary, { value, from: buyer }) const balance = await token.balanceOf(beneficiary) balance.should.be.bignumber.equal(value.mul(preferentialRateForBuyer)) }) it('tokens should be assigned correctly to foundation when finalized', async function() { await advanceToBlock(startBlock - 1) // since price at first block is 1000, total tokens emitted will be 4000 await crowdsale.buyTokens(buyer, { value: 4, from: purchaser }) await advanceToBlock(endBlock) await crowdsale.finalize() const balance = await token.balanceOf(wallet) balance.should.be.bignumber.equal(expectedFoundationTokens) const totalSupply = await token.totalSupply() totalSupply.should.be.bignumber.equal(expectedTokenSupply) }) }) ================================================ FILE: test/MANAToken.js ================================================ // @flow 'use strict' const { EVMRevert } = require('./utils') const MANAToken = artifacts.require('./MANAToken.sol') const BigNumber = web3.BigNumber contract('MANACrowdsale', function([owner, holder]) { let token beforeEach(async function() { token = await MANAToken.new() }) it('cannot burn tokens while paused', async function() { await token.mint(holder, 1000) await token.pause() await token.burn(500, { from: holder }).should.be.rejectedWith(EVMRevert) await token.unpause() await token.burn(500, { from: holder }).should.be.fulfilled }) }) ================================================ FILE: test/WhitelistedCrowdsale.js ================================================ // @flow import { ether, EVMRevert } from './utils' const BigNumber = web3.BigNumber const WhitelistedCrowdsale = artifacts.require( './helpers/WhitelistedCrowdsaleImpl.sol' ) const MintableToken = artifacts.require( 'zeppelin-solidity/contracts/tokens/MintableToken' ) contract('WhitelistCrowdsale', function([ _, owner, wallet, beneficiary, sender ]) { const rate = new BigNumber(1000) beforeEach(async function() { this.startBlock = web3.eth.blockNumber + 10 this.endBlock = this.startBlock + 10 this.crowdsale = await WhitelistedCrowdsale.new( this.startBlock, this.endBlock, rate, wallet, { from: owner } ) this.token = MintableToken.at(await this.crowdsale.token()) }) describe('whitelisting', function() { const amount = ether(1) it('should add address to whitelist', async function() { let whitelisted = await this.crowdsale.isWhitelisted(sender) whitelisted.should.equal(false) await this.crowdsale.addToWhitelist(sender, { from: owner }) whitelisted = await this.crowdsale.isWhitelisted(sender) whitelisted.should.equal(true) }) it('should reject non-whitelisted sender', async function() { await this.crowdsale .buyTokens(beneficiary, { value: amount, from: sender }) .should.be.rejectedWith(EVMRevert) }) it('should sell to whitelisted address', async function() { await this.crowdsale.addToWhitelist(sender, { from: owner }) await this.crowdsale.buyTokens(beneficiary, { value: amount, from: sender }).should.be.fulfilled }) }) }) ================================================ FILE: test/helpers/BurnableTokenMock.sol ================================================ pragma solidity ^0.4.11; import '../../contracts/BurnableToken.sol'; contract BurnableTokenMock is BurnableToken { function BurnableTokenMock(address initialAccount, uint initialBalance) { balances[initialAccount] = initialBalance; totalSupply = initialBalance; } } ================================================ FILE: test/helpers/WhitelistedCrowdsaleImpl.sol ================================================ pragma solidity ^0.4.11; import '../../contracts/WhitelistedCrowdsale.sol'; contract WhitelistedCrowdsaleImpl is WhitelistedCrowdsale { function WhitelistedCrowdsaleImpl ( uint256 _startBlock, uint256 _endBlock, uint256 _rate, address _wallet ) Crowdsale(_startBlock, _endBlock, _rate, _wallet) WhitelistedCrowdsale() { } } ================================================ FILE: test/utils.js ================================================ // @flow const BigNumber = web3.BigNumber export const should = require('chai') .use(require('chai-as-promised')) .use(require('chai-bignumber')(BigNumber)) .should() type RPCParams = { method: string, params?: Array } function rpcCall(call: RPCParams) { const payload: any = Object.assign( {}, { jsonrpc: '2.0', id: Date.now() }, call ) return new Promise((resolve, reject) => { web3.currentProvider.sendAsync(payload, (err, res) => { return err ? reject(err) : resolve(res) }) }) } export function advanceBlock() { return rpcCall({ method: 'evm_mine' }) } export function increaseTime(seconds: number) { return rpcCall({ method: 'evm_increaseTime', params: [+seconds] }) } // Advances the block number so that the last mined block is `number`. export async function advanceToBlock(number: number) { if (web3.eth.blockNumber > number) { throw Error( `block number ${number} is in the past (current is ${ web3.eth.blockNumber })` ) } while (web3.eth.blockNumber < number) { await advanceBlock() } } export async function advanceToTime(timestamp: number) { const block = await web3.eth.getBlock('latest') if (block.timestamp > timestamp) { throw Error( `time ${timestamp} is in the past (current is ${block.timestamp})` ) } await advanceTime(timestamp - block.timestamp) } export async function advanceTime(time: number) { await increaseTime(time) await advanceBlock() } export function ether(n: number) { return new web3.BigNumber(web3.toWei(n, 'ether')) } export const EVMThrow = 'invalid opcode' export const EVMRevert = 'revert' ================================================ FILE: truffle.js ================================================ require('babel-register'); require('babel-polyfill'); module.exports = { networks: { livenet: { host: "localhost", port: 8546, network_id: "1" // Match any network id }, development: { host: "localhost", port: 8545, gas: 10000000, network_id: "*" // Match any network id } } };