diff --git a/.gitignore b/.gitignore index 1370634..1cf8ad7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ npm-debug.log .node-xmlhttprequest-sync-[0-9]* coverage coverage.json -.private.js +private.js diff --git a/.solcover.js b/.solcover.js index 4afe2a7..59d2ab5 100644 --- a/.solcover.js +++ b/.solcover.js @@ -1,13 +1,11 @@ module.exports = { port: 8555, norpc: true, - // testCommand: 'node ../node_modules/.bin/truffle test --network coverage', + testCommand: 'GEN_TESTS_TIMEOUT=400 GEN_TESTS_QTY=40 truffle test --network coverage test/QiibeeToken.js test/QiibeeCrowdsale.js test/QiibeePresale.js test/WhitelistedCrowdsale.js test/QiibeePresaleGenTest.js test/QiibeeCrowdsaleGenTest.js', copyNodeModules: true, skipFiles: [ 'test-helpers/Message.sol', - 'VestedToken.sol', - 'CrowdsaleImpl.sol', - 'QiibeePresaleImpl.sol', - 'WhitelistedCrowdsaleImpl.sol' + 'Crowdsale.sol', + 'QiibeeMigrationToken.sol', ] } diff --git a/README.md b/README.md index d2ebb9b..febbd9d 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ npm install * To run all tests: `npm test` -* To run a specific test: `npm test -- test/Crowdsale.js` +* To run a specific test: `npm test -- test/QiibeeCrowdsale.js` There are also two environment variables (`GEN_TESTS_QTY` and `GEN_TESTS_TIMEOUT`) that regulate the duration/depth of the property-based tests, so for example: diff --git a/contracts/Crowdsale.sol b/contracts/Crowdsale.sol index 71300ea..7cc0710 100644 --- a/contracts/Crowdsale.sol +++ b/contracts/Crowdsale.sol @@ -2,7 +2,7 @@ pragma solidity ^0.4.11; import "zeppelin-solidity/contracts/lifecycle/Pausable.sol"; import "zeppelin-solidity/contracts/crowdsale/RefundVault.sol"; -import "./QiibeeToken.sol"; +// import "./QiibeeToken.sol"; /** @title Crowdsale for the QBX Token Generation Event @@ -20,6 +20,23 @@ import "./QiibeeToken.sol"; The function buyTokens() does not mint tokens. This function should be overriden to add that logic. */ +contract QiibeeToken { + function mintVestedTokens(address _to, + uint256 _value, + uint64 _start, + uint64 _cliff, + uint64 _vesting, + bool _revokable, + bool _burnsOnRevoke, + address _wallet + ) returns (bool); + function mint(address _to, uint256 _amount) returns (bool); + function transferOwnership(address _wallet); + function pause(); + function unpause(); + function finishMinting() returns (bool); +} + contract Crowdsale is Pausable { using SafeMath for uint256; @@ -31,6 +48,8 @@ contract Crowdsale is Pausable { uint256 public goal; // min amount of funds to be raised in wei RefundVault public vault; // refund vault used to hold funds while crowdsale is running + uint256 public rate; // how many token units a buyer gets per wei + QiibeeToken public token; // token being sold uint256 public tokensSold; // qbx minted (and sold) uint256 public weiRaised; // raised money in wei @@ -66,6 +85,7 @@ contract Crowdsale is Pausable { * @dev Constructor. Creates the token in a paused state * @param _startTime see `startTimestamp` * @param _endTime see `endTimestamp` + * @param _rate see `see rate` * @param _goal see `see goal` * @param _cap see `see cap` * @param _maxGasPrice see `see maxGasPrice` @@ -75,6 +95,7 @@ contract Crowdsale is Pausable { function Crowdsale ( uint256 _startTime, uint256 _endTime, + uint256 _rate, uint256 _goal, uint256 _cap, uint256 _maxGasPrice, @@ -84,6 +105,7 @@ contract Crowdsale is Pausable { { require(_startTime >= now); require(_endTime >= _startTime); + require(_rate > 0); require(_cap > 0); require(_goal > 0); require(_goal <= _cap); @@ -94,16 +116,17 @@ contract Crowdsale is Pausable { startTime = _startTime; endTime = _endTime; + rate = _rate; cap = _cap; goal = _goal; maxGasPrice = _maxGasPrice; minBuyingRequestInterval = _minBuyingRequestInterval; wallet = _wallet; - token = new QiibeeToken(); + // token = new QiibeeToken(); vault = new RefundVault(wallet); - token.pause(); + // token.pause(); } @@ -114,17 +137,26 @@ contract Crowdsale is Pausable { buyTokens(msg.sender); } - /** - * @dev Must be overridden to add token minting logic. The overriding function - * should call super.finalization() to ensure the chain of buy tokens is - * executed entirely. + /* + * @dev Low level token purchase function. + * @param beneficiary address where tokens are sent to */ function buyTokens(address beneficiary) public payable whenNotPaused { require(beneficiary != address(0)); require(validPurchase()); uint256 weiAmount = msg.value; + + // calculate token amount to be created + uint256 tokens = weiAmount.mul(rate); + + // update state weiRaised = weiRaised.add(weiAmount); + tokensSold = tokensSold.add(tokens); + lastCallTime[msg.sender] = now; + + token.mint(beneficiary, tokens); + TokenPurchase(msg.sender, beneficiary, weiAmount, tokens); forwardFunds(); } @@ -177,7 +209,7 @@ contract Crowdsale is Pausable { * @dev Must be called after crowdsale ends, to do some extra finalization * work. Calls the contract's finalization function. */ - function finalize() public { + function finalize() public onlyOwner { require(!isFinalized); require(hasEnded()); @@ -210,4 +242,14 @@ contract Crowdsale is Pausable { WalletChange(_wallet); } + /** + @dev changes the token owner + */ + //TODO: EXECUTE BEFORE START CROWDSALE + function setToken(address tokenAddress) onlyOwner { + require(now < startTime); + token = QiibeeToken(tokenAddress); + } + + } diff --git a/contracts/MigrationAgent.sol b/contracts/MigrationAgent.sol new file mode 100644 index 0000000..4124557 --- /dev/null +++ b/contracts/MigrationAgent.sol @@ -0,0 +1,53 @@ +pragma solidity ^0.4.11; + +import "zeppelin-solidity/contracts/ownership/Ownable.sol"; +import "./QiibeeToken.sol"; + +// interface +contract QiibeeMigrationTokenInterface { + function createToken(address _target, uint256 _amount); + function finalizeMigration(); + function totalSupply() returns (uint256); +} + +contract MigrationAgent is Ownable { + + address public qbxSourceToken; + address public qbxTargetToken; + uint256 public tokenSupply; + + function MigrationAgent(address _qbxSourceToken) { + require(QiibeeToken(_qbxSourceToken).migrationAgent() == address(0)); + tokenSupply = QiibeeToken(_qbxSourceToken).totalSupply(); + qbxSourceToken = _qbxSourceToken; + } + + function safetyInvariantCheck(uint256 _value) internal { + require(QiibeeToken(qbxSourceToken).totalSupply() + QiibeeMigrationTokenInterface(qbxTargetToken).totalSupply() == tokenSupply - _value); + } + + function setTargetToken(address _qbxTargetToken) public onlyOwner { + require(qbxTargetToken == address(0)); //Allow this change once only + qbxTargetToken = _qbxTargetToken; + } + + function migrateFrom(address _from, uint256 _value) public { + require(msg.sender == qbxSourceToken); + require(qbxTargetToken != address(0)); + + safetyInvariantCheck(_value); // qbxSourceToken has already been updated, but corresponding QBX have not been created in the qbxTargetToken contract yet + QiibeeMigrationTokenInterface(qbxTargetToken).createToken(_from, _value); + safetyInvariantCheck(0); // totalSupply invariant must hold + } + + function finalizeMigration() public onlyOwner { + require(qbxTargetToken != address(0)); + require(QiibeeToken(qbxSourceToken).totalSupply() == 0); //only finlize if all tokens have been migrated + safetyInvariantCheck(0); + QiibeeMigrationTokenInterface(qbxTargetToken).finalizeMigration(); + + qbxSourceToken = address(0); + qbxTargetToken = address(0); + tokenSupply = 0; + } +} diff --git a/contracts/QiibeeCrowdsale.sol b/contracts/QiibeeCrowdsale.sol index 10446d0..e0ea56f 100644 --- a/contracts/QiibeeCrowdsale.sol +++ b/contracts/QiibeeCrowdsale.sol @@ -1,6 +1,27 @@ pragma solidity ^0.4.11; -import "./Crowdsale.sol"; +import "zeppelin-solidity/contracts/crowdsale/FinalizableCrowdsale.sol"; +import "zeppelin-solidity/contracts/crowdsale/RefundableCrowdsale.sol"; +import "zeppelin-solidity/contracts/crowdsale/CappedCrowdsale.sol"; +import "zeppelin-solidity/contracts/crowdsale/Crowdsale.sol"; +import "zeppelin-solidity/contracts/lifecycle/Pausable.sol"; + +contract QiibeeToken { + function mintVestedTokens(address _to, + uint256 _value, + uint64 _start, + uint64 _cliff, + uint64 _vesting, + bool _revokable, + bool _burnsOnRevoke, + address _wallet + ) returns (bool); + function mint(address _to, uint256 _amount) returns (bool); + function transferOwnership(address _wallet); + function pause(); + function unpause(); + function finishMinting() returns (bool); +} /** @title Crowdsale for the QBX Token Generation Event @@ -19,17 +40,33 @@ import "./Crowdsale.sol"; foundation wallet. Token is unpaused and minting is disabled. */ -contract QiibeeCrowdsale is Crowdsale { +contract QiibeeCrowdsale is CappedCrowdsale, FinalizableCrowdsale, RefundableCrowdsale, Pausable { using SafeMath for uint256; + QiibeeToken public token; // token being sold + uint256 public constant FOUNDATION_SUPPLY = 10e27; // total amount of tokens in atto for the pools - uint256 public rate; // how many token units a buyer gets per wei + uint256 public tokensSold; // qbx minted (and sold) + + mapping (address => uint256) public balances; // balance of wei invested per investor + // spam prevention + mapping (address => uint256) public lastCallTime; // last call times by address + uint256 public maxGasPrice; // max gas price per transaction + uint256 public minBuyingRequestInterval; // min request interval for purchases from a single source (in seconds) + + // limits uint256 public minInvest; // minimum invest in wei an address can do uint256 public maxCumulativeInvest; // maximum cumulative invest an address can do + /* + * @dev event for change wallet logging + * @param wallet new wallet address + */ + event WalletChange(address wallet); + /* * @dev Constructor. Creates the token in a paused state * @param _startTime see `startTimestamp` @@ -55,28 +92,20 @@ contract QiibeeCrowdsale is Crowdsale { uint256 _minBuyingRequestInterval, address _wallet ) - Crowdsale(_startTime, _endTime, _goal, _cap, _maxGasPrice, _minBuyingRequestInterval, _wallet) + Crowdsale(_startTime, _endTime, _rate, _wallet) + CappedCrowdsale(_cap) + RefundableCrowdsale(_goal) { - require(_rate > 0); require(_minInvest > 0); require(_maxCumulativeInvest > 0); require(_minInvest <= _maxCumulativeInvest); + require(_maxGasPrice > 0); + require(_minBuyingRequestInterval > 0); - rate = _rate; minInvest = _minInvest; maxCumulativeInvest = _maxCumulativeInvest; - } - - /* - * @dev Returns the rate accordingly: before goal is reached, there is a fixed rate given by - * `rate`. After that, the formula applies. - * @return rate accordingly - */ - function getRate() public constant returns(uint256) { - if (goalReached()) { - return rate.mul(1000).div(tokensSold.mul(1000).div(goal)); - } - return rate; + maxGasPrice = _maxGasPrice; + minBuyingRequestInterval = _minBuyingRequestInterval; } /* @@ -85,31 +114,34 @@ contract QiibeeCrowdsale is Crowdsale { */ function buyTokens(address beneficiary) public payable whenNotPaused{ require(beneficiary != address(0)); - require(validPurchase(beneficiary)); + require(validPurchase(beneficiary, msg.value)); - uint256 rate = getRate(); - uint256 tokens = msg.value.mul(rate); + uint256 weiAmount = msg.value; + + uint256 tokens = weiAmount.mul(rate); // update state - assert(token.mint(beneficiary, tokens)); + assert(QiibeeToken(token).mint(beneficiary, tokens)); - weiRaised = weiRaised.add(msg.value); + weiRaised = weiRaised.add(weiAmount); tokensSold = tokensSold.add(tokens); lastCallTime[msg.sender] = now; - TokenPurchase(msg.sender, beneficiary, msg.value, tokens); + TokenPurchase(msg.sender, beneficiary, weiAmount, tokens); forwardFunds(); } + /* * Checks if the investment made is within the allowed limits */ - function validPurchase(address beneficiary) internal constant returns (bool) { - // check limits - uint256 newBalance = balances[beneficiary].add(msg.value); - bool withinLimits = newBalance <= maxCumulativeInvest && msg.value >= minInvest; - return withinLimits && super.validPurchase(); + function validPurchase(address beneficiary, uint256 weiAmount) internal constant returns (bool) { + uint256 newBalance = balances[beneficiary].add(weiAmount); + bool withinLimits = newBalance <= maxCumulativeInvest && weiAmount >= minInvest; + bool withinFrequency = now.sub(lastCallTime[msg.sender]) >= minBuyingRequestInterval; + bool withinGasPrice = tx.gasprice <= maxGasPrice; + return super.validPurchase() && withinLimits && withinFrequency && withinGasPrice; } /* @@ -117,7 +149,7 @@ contract QiibeeCrowdsale is Crowdsale { * and send them to the foundation wallet. */ function finalization() internal { - token.mint(wallet, FOUNDATION_SUPPLY); + QiibeeToken(token).mint(wallet, FOUNDATION_SUPPLY); super.finalization(); } @@ -126,7 +158,7 @@ contract QiibeeCrowdsale is Crowdsale { unpauses the token and transfers the token ownership to the foundation. This function can only be called when the crowdsale has ended. */ - function finalize() public { + function finalize() onlyOwner public { require(!isFinalized); require(hasEnded()); @@ -135,9 +167,9 @@ contract QiibeeCrowdsale is Crowdsale { isFinalized = true; - token.finishMinting(); - token.unpause(); - token.transferOwnership(wallet); + QiibeeToken(token).finishMinting(); + QiibeeToken(token).unpause(); + QiibeeToken(token).transferOwnership(wallet); } /** @@ -147,4 +179,14 @@ contract QiibeeCrowdsale is Crowdsale { token = QiibeeToken(tokenAddress); } + /* + * @dev Changes the current wallet for a new one. Only the owner can call this function. + * @param _wallet new wallet + */ + function setWallet(address _wallet) onlyOwner public { + require(_wallet != address(0)); + wallet = _wallet; + WalletChange(_wallet); + } + } diff --git a/contracts/QiibeeMigrationToken.sol b/contracts/QiibeeMigrationToken.sol new file mode 100644 index 0000000..081b27f --- /dev/null +++ b/contracts/QiibeeMigrationToken.sol @@ -0,0 +1,38 @@ +pragma solidity ^0.4.11; + +import "zeppelin-solidity/contracts/token/PausableToken.sol"; + +/** + @title Example of a new token + */ +contract QiibeeMigrationToken is PausableToken { + + string public constant SYMBOL = "QBX"; + + string public constant NAME = "qiibeeCoin"; + + uint8 public constant DECIMALS = 18; + + // migration vars + address public migrationAgent; + + function QiibeeMigrationToken(address _migrationAgent) { + require(_migrationAgent != address(0)); + migrationAgent = _migrationAgent; + } + + // Migration related methods + function createToken(address _target, uint256 _amount) { + require(msg.sender == migrationAgent); + + balances[_target] += _amount; + totalSupply += _amount; + + Transfer(migrationAgent, _target, _amount); + } + + function finalizeMigration() { + require(msg.sender == migrationAgent); + migrationAgent = 0; + } +} diff --git a/contracts/QiibeePresale.sol b/contracts/QiibeePresale.sol index d9f8348..e40a24b 100644 --- a/contracts/QiibeePresale.sol +++ b/contracts/QiibeePresale.sol @@ -1,34 +1,51 @@ pragma solidity ^0.4.11; -import "./Crowdsale.sol"; +import "zeppelin-solidity/contracts/crowdsale/FinalizableCrowdsale.sol"; +import "zeppelin-solidity/contracts/crowdsale/CappedCrowdsale.sol"; +import "zeppelin-solidity/contracts/lifecycle/Pausable.sol"; + +contract QiibeeTokenInterface { + function mintVestedTokens(address _to, + uint256 _value, + uint64 _start, + uint64 _cliff, + uint64 _vesting, + bool _revokable, + bool _burnsOnRevoke, + address _wallet + ) returns (bool); + function mint(address _to, uint256 _amount) returns (bool); + function transferOwnership(address _wallet); + function pause(); + function unpause(); + function finishMinting() returns (bool); +} /** @title Presale event - Implementation of the QBX Presale Token Generation Event (PTGE): A X-week capped presale with a - soft cap (goal) and a hard cap, both of them expressed in wei. + Implementation of the QBX Presale Token Generation Event (PTGE): A X-week presale with a hard cap + expressed in wei. This presale is only for accredited investors, who will have to be whitelisted by the owner - using the addAccreditedInvestor() function. Each accredited investor has its own token rate, a - minimum amount of wei for each one of his transactions, a maximum cumulative investment and - vesting settings (cliff and vesting period). + using the `addAccreditedInvestor()` function. Each accredited investor has a minimum amount of wei + for each one of his transactions, a maximum cumulative investment and vesting settings (cliff + and vesting period). - On each purchase, the corresponding amount of tokens (given by the investor’s rate) will be - minted and vested (if the investor has vesting settings). + On each purchase, the corresponding amount of tokens will be minted and vested (if the investor + has vesting settings). - In case of the goal not being reached by purchases made during the event, all funds sent during - this period will be made available to be claimed by the originating addresses. + After fundraising is done (now > endTime or weiRaised >= cap) and before the presale is finished, + the owner of the contract will distribute the tokens for the different pools by calling the + `distributeTokens()` function. - The token begins paused and remains like that at the end of the presale. The unpause of the token - will be done at the end of the qiibee crowdsale. */ -contract QiibeePresale is Crowdsale { +contract QiibeePresale is CappedCrowdsale, FinalizableCrowdsale, Pausable { using SafeMath for uint256; struct AccreditedInvestor { - uint256 rate; uint64 cliff; uint64 vesting; bool revokable; @@ -37,18 +54,39 @@ contract QiibeePresale is Crowdsale { uint256 maxCumulativeInvest; // maximum cumulative invest in wei for a given investor } + QiibeeTokenInterface public token; // token being sold + + uint256 public distributionCap; // cap in tokens that can be distributed to the pools + uint256 public tokensDistributed; // tokens distributed to pools + uint256 public tokensSold; // qbx minted (and sold) + + uint64 public vestFromTime = 1530316800; // start time for vested tokens (equiv. to 30/06/2018) + + mapping (address => uint256) public balances; // balance of wei invested per investor mapping (address => AccreditedInvestor) public accredited; // whitelist of investors + // spam prevention + mapping (address => uint256) public lastCallTime; // last call times by address + uint256 public maxGasPrice; // max gas price per transaction + uint256 public minBuyingRequestInterval; // min request interval for purchases from a single source (in seconds) + bool public isFinalized = false; event NewAccreditedInvestor(address indexed from, address indexed buyer); + event TokenDistributed(address indexed beneficiary, uint256 tokens); + + modifier afterFundraising { + require(now > endTime || weiRaised >= cap); + _; + } /* * @dev Constructor. * @param _startTime see `startTimestamp` * @param _endTime see `endTimestamp` - * @param _goal see `see goal` + * @param _rate see `see rate` * @param _cap see `see cap` + * @param _distributionCap see `see distributionCap` * @param _maxGasPrice see `see maxGasPrice` * @param _minBuyingRequestInterval see `see minBuyingRequestInterval` * @param _wallet see `wallet` @@ -56,68 +94,108 @@ contract QiibeePresale is Crowdsale { function QiibeePresale( uint256 _startTime, uint256 _endTime, - uint256 _goal, + uint256 _rate, uint256 _cap, + uint256 _distributionCap, uint256 _maxGasPrice, uint256 _minBuyingRequestInterval, address _wallet ) - Crowdsale(_startTime, _endTime, _goal, _cap, _maxGasPrice, _minBuyingRequestInterval, _wallet) + Crowdsale(_startTime, _endTime, _rate, _wallet) + CappedCrowdsale(_cap) { + require(_distributionCap > 0); + require(_maxGasPrice > 0); + require(_minBuyingRequestInterval > 0); + + distributionCap = _distributionCap; + maxGasPrice = _maxGasPrice; + minBuyingRequestInterval = _minBuyingRequestInterval; + } + + /* + * @param beneficiary address where tokens are sent to + */ + function buyTokens(address beneficiary) public payable whenNotPaused { + require(beneficiary != address(0)); + require(validPurchase()); + + AccreditedInvestor storage data = accredited[msg.sender]; + + // investor's data + uint256 minInvest = data.minInvest; + uint256 maxCumulativeInvest = data.maxCumulativeInvest; + uint64 from = vestFromTime; + uint64 cliff = from + data.cliff; + uint64 vesting = cliff + data.vesting; + bool revokable = data.revokable; + bool burnsOnRevoke = data.burnsOnRevoke; + + uint256 tokens = msg.value.mul(rate); + + // check investor's limits + uint256 newBalance = balances[beneficiary].add(msg.value); + require(newBalance <= maxCumulativeInvest && msg.value >= minInvest); + + if (data.cliff > 0 && data.vesting > 0) { + require(QiibeeTokenInterface(token).mintVestedTokens(beneficiary, tokens, from, cliff, vesting, revokable, burnsOnRevoke, wallet)); + } else { + require(QiibeeTokenInterface(token).mint(beneficiary, tokens)); + } + + // update state + balances[beneficiary] = newBalance; + weiRaised = weiRaised.add(msg.value); + tokensSold = tokensSold.add(tokens); + + TokenPurchase(msg.sender, beneficiary, msg.value, tokens); } /* - * @param beneficiary beneficiary address where tokens are sent to + * @dev This functions is used to manually distribute tokens. It works after the fundraising, can + * only be called by the owner and when the presale is not paused. It has a cap on the amount + * of tokens that can be manually distributed. + * + * @param _beneficiary address where tokens are sent to + * @param _tokens amount of tokens (in atto) to distribute + * @param _cliff duration in seconds of the cliff in which tokens will begin to vest. + * @param _vesting duration in seconds of the vesting in which tokens will vest. */ - function buyTokens(address beneficiary) public payable whenNotPaused{ - require(beneficiary != address(0)); - require(validPurchase()); - - AccreditedInvestor storage data = accredited[msg.sender]; - - // investor's data - uint256 rate = data.rate; - uint256 minInvest = data.minInvest; - uint256 maxCumulativeInvest = data.maxCumulativeInvest; - uint64 from = uint64(endTime); - uint64 cliff = from + data.cliff; - uint64 vesting = cliff + data.vesting; - bool revokable = data.revokable; - bool burnsOnRevoke = data.burnsOnRevoke; - - uint256 tokens = msg.value.mul(rate); - - // check investor's limits - uint256 newBalance = balances[beneficiary].add(msg.value); - require(newBalance <= maxCumulativeInvest && msg.value >= minInvest); - - if (data.cliff > 0 && data.vesting > 0) { - require(token.mintVestedTokens(beneficiary, tokens, from, cliff, vesting, revokable, burnsOnRevoke, wallet)); - } else { - require(token.mint(beneficiary, tokens)); - } - - // update state - balances[beneficiary] = newBalance; - weiRaised = weiRaised.add(msg.value); - tokensSold = tokensSold.add(tokens); - - TokenPurchase(msg.sender, beneficiary, msg.value, tokens); - - forwardFunds(); + function distributeTokens(address _beneficiary, uint256 _tokens, uint64 _cliff, uint64 _vesting, bool _revokable, bool _burnsOnRevoke) public onlyOwner whenNotPaused afterFundraising { + require(_beneficiary != address(0)); + require(_tokens > 0); + require(_vesting >= _cliff); + require(!isFinalized); + + // check distribution cap limit + uint256 totalDistributed = tokensDistributed.add(_tokens); + assert(totalDistributed <= distributionCap); + + if (_cliff > 0 && _vesting > 0) { + uint64 from = vestFromTime; + uint64 cliff = from + _cliff; + uint64 vesting = cliff + _vesting; + assert(QiibeeTokenInterface(token).mintVestedTokens(_beneficiary, _tokens, from, cliff, vesting, _revokable, _burnsOnRevoke, wallet)); + } else { + assert(QiibeeTokenInterface(token).mint(_beneficiary, _tokens)); + } + + // update state + tokensDistributed = tokensDistributed.add(_tokens); + + TokenDistributed(_beneficiary, _tokens); } /* * @dev Add an address to the accredited list. */ - function addAccreditedInvestor(address investor, uint256 rate, uint64 cliff, uint64 vesting, bool revokable, bool burnsOnRevoke, uint256 minInvest, uint256 maxCumulativeInvest) public onlyOwner { + function addAccreditedInvestor(address investor, uint64 cliff, uint64 vesting, bool revokable, bool burnsOnRevoke, uint256 minInvest, uint256 maxCumulativeInvest) public onlyOwner { require(investor != address(0)); - require(rate > 0); require(vesting >= cliff); require(minInvest > 0); require(maxCumulativeInvest > 0); - accredited[investor] = AccreditedInvestor(rate, cliff, vesting, revokable, burnsOnRevoke, minInvest, maxCumulativeInvest); + accredited[investor] = AccreditedInvestor(cliff, vesting, revokable, burnsOnRevoke, minInvest, maxCumulativeInvest); NewAccreditedInvestor(msg.sender, investor); } @@ -128,7 +206,7 @@ contract QiibeePresale is Crowdsale { */ function isAccredited(address investor) public constant returns (bool) { AccreditedInvestor storage data = accredited[investor]; - return data.rate > 0; //TODO: is there any way to properly check this? + return data.minInvest > 0; //TODO: is there any way to properly check this? } /* @@ -140,17 +218,21 @@ contract QiibeePresale is Crowdsale { } - // @return true if investors can buy at the moment + /* + * @return true if investors can buy at the moment + */ function validPurchase() internal constant returns (bool) { require(isAccredited(msg.sender)); - return super.validPurchase(); + bool withinFrequency = now.sub(lastCallTime[msg.sender]) >= minBuyingRequestInterval; + bool withinGasPrice = tx.gasprice <= maxGasPrice; + return super.validPurchase() && withinFrequency && withinGasPrice; } - /** + /* * @dev Must be called after crowdsale ends, to do some extra finalization - * work. Calls the contract's finalization function. + * work. Calls the contract's finalization function. Only owner can call it. */ - function finalize() public { + function finalize() public onlyOwner { require(!isFinalized); require(hasEnded()); @@ -160,7 +242,16 @@ contract QiibeePresale is Crowdsale { isFinalized = true; // transfer the ownership of the token to the foundation - token.transferOwnership(wallet); + QiibeeTokenInterface(token).transferOwnership(wallet); + } + + /* + * @dev sets the token that the presale will use. Call only be called by the owner and + * before the presale starts. + */ + function setToken(address tokenAddress) onlyOwner { + require(now < startTime); + token = QiibeeTokenInterface(tokenAddress); } } diff --git a/contracts/QiibeeToken.sol b/contracts/QiibeeToken.sol index 3c5381c..c7404a7 100644 --- a/contracts/QiibeeToken.sol +++ b/contracts/QiibeeToken.sol @@ -5,14 +5,17 @@ import "zeppelin-solidity/contracts/token/MintableToken.sol"; import "zeppelin-solidity/contracts/token/BurnableToken.sol"; import "zeppelin-solidity/contracts/token/VestedToken.sol"; +// @dev Migration Agent interface +contract MigrationAgentInterface { + function migrateFrom(address _from, uint256 _value); + function setSourceToken(address _qbxSourceToken); +} + /** @title QBX, the qiibee token - Implementation of QBX, an ERC20 token for the qiibee ecosystem. - It uses OpenZeppelin MintableToken and PausableToken. In addition, - it has a BurnableToken responsible for burning tokens. - - The smallest unit of a qbx is the atto. + Implementation of QBX, an ERC20 token for the qiibee ecosystem. The smallest unit of a qbx is + the atto. The token call be migrated to a new token by calling the `migrate()` function. */ contract QiibeeToken is BurnableToken, PausableToken, VestedToken, MintableToken { @@ -22,12 +25,24 @@ contract QiibeeToken is BurnableToken, PausableToken, VestedToken, MintableToken uint8 public constant DECIMALS = 18; - /** - @dev Burns a specific amount of tokens. - @param _value The amount of tokens to be burnt. - */ - function burn(uint256 _value) whenNotPaused public { - super.burn(_value); + // migration vars + uint256 public totalMigrated; + address public migrationAgent; + address public migrationMaster; + + event Migrate(address indexed _from, address indexed _to, uint256 _value); + + modifier onlyMigrationMaster { + require(msg.sender == migrationMaster); + _; + } + + /* + * Constructor. + */ + function QiibeeToken(address _migrationMaster) { + require(_migrationMaster != address(0)); + migrationMaster = _migrationMaster; } /** @@ -61,7 +76,7 @@ contract QiibeeToken is BurnableToken, PausableToken, VestedToken, MintableToken ); return mint(_to, _value); //mint tokens - } + } /** @dev Overrides VestedToken#grantVestedTokens(). Only owner can call it. @@ -77,4 +92,46 @@ contract QiibeeToken is BurnableToken, PausableToken, VestedToken, MintableToken ) onlyOwner public { super.grantVestedTokens(_to, _value, _start, _cliff, _vesting, _revokable, _burnsOnRevoke); } + + /** + @dev Set address of migration agent contract and enable migration process. + @param _agent The address of the MigrationAgent contract + */ + function setMigrationAgent(address _agent) public onlyMigrationMaster { + require(migrationAgent == address(0)); + migrationAgent = _agent; + } + + /** + @dev Migrates the tokens to the target token through the MigrationAgent. + @param _value The amount of tokens (in atto) to be migrated. + */ + function migrate(uint256 _value) public whenNotPaused { + require(migrationAgent != address(0)); + require(_value != 0); + require(_value <= balances[msg.sender]); + + balances[msg.sender] -= _value; + totalSupply -= _value; + totalMigrated += _value; + MigrationAgentInterface(migrationAgent).migrateFrom(msg.sender, _value); + Migrate(msg.sender, migrationAgent, _value); + } + + /* + * @dev Changes the migration master. + * @param _master The address of the migration master. + */ + function setMigrationMaster(address _master) public onlyMigrationMaster { + require(_master != address(0)); + migrationMaster = _master; + } + + /* + * @dev Burns a specific amount of tokens. + * @param _value The amount of tokens to be burnt. + */ + function burn(uint256 _value) whenNotPaused public { + super.burn(_value); + } } diff --git a/contracts/WhitelistedCrowdsale.sol b/contracts/WhitelistedCrowdsale.sol index d52a0ea..5b5b175 100644 --- a/contracts/WhitelistedCrowdsale.sol +++ b/contracts/WhitelistedCrowdsale.sol @@ -14,9 +14,26 @@ contract WhitelistedCrowdsale is Crowdsale, Ownable { // list of addresses that can purchase before crowdsale opens mapping (address => bool) public whitelist; + /* + * @dev Constructor. + * @param _startTime see `startTimestamp` + * @param _endTime see `endTimestamp` + * @param _rate see `rate` on Crowdsale.sol + * @param _wallet see `wallet` + */ + function WhitelistedCrowdsale ( + uint256 _startTime, + uint256 _endTime, + uint256 _rate, + address _wallet + ) + Crowdsale(_startTime, _endTime, _rate, _wallet) + { + } + function addToWhitelist(address investor) public onlyOwner { - require(investor != address(0)); - whitelist[investor] = true; + require(investor != address(0)); + whitelist[investor] = true; } // @return true if investor is whitelisted diff --git a/contracts/test-helpers/CrowdsaleImpl.sol b/contracts/test-helpers/CrowdsaleImpl.sol deleted file mode 100644 index bbf7c44..0000000 --- a/contracts/test-helpers/CrowdsaleImpl.sol +++ /dev/null @@ -1,22 +0,0 @@ -pragma solidity ^0.4.11; - - -import '../Crowdsale.sol'; - - -contract CrowdsaleImpl is Crowdsale { - - function CrowdsaleImpl ( - uint256 _startTime, - uint256 _endTime, - uint256 _goal, - uint256 _cap, - uint256 _maxGasPrice, - uint256 _minBuyingRequestInterval, - address _wallet - ) - Crowdsale(_startTime, _endTime, _goal, _cap, _maxGasPrice, _minBuyingRequestInterval, _wallet) - { - } - -} diff --git a/contracts/test-helpers/QiibeePresaleImpl.sol b/contracts/test-helpers/QiibeePresaleImpl.sol deleted file mode 100644 index 6efef7f..0000000 --- a/contracts/test-helpers/QiibeePresaleImpl.sol +++ /dev/null @@ -1,22 +0,0 @@ -pragma solidity ^0.4.11; - - -import '../QiibeePresale.sol'; - - -contract QiibeePresaleImpl is QiibeePresale { - - function QiibeePresaleImpl ( - uint256 _startTime, - uint256 _endTime, - uint256 _goal, - uint256 _cap, - uint256 _maxGasPrice, - uint256 _minBuyingRequestInterval, - address _wallet - ) - QiibeePresale(_startTime, _endTime, _goal, _cap, _maxGasPrice, _minBuyingRequestInterval, _wallet) - { - } - -} diff --git a/contracts/test-helpers/WhitelistedCrowdsaleImpl.sol b/contracts/test-helpers/WhitelistedCrowdsaleImpl.sol deleted file mode 100644 index f8e7b51..0000000 --- a/contracts/test-helpers/WhitelistedCrowdsaleImpl.sol +++ /dev/null @@ -1,20 +0,0 @@ -pragma solidity ^0.4.11; - - -import '../WhitelistedCrowdsale.sol'; - - -contract WhitelistedCrowdsaleImpl is WhitelistedCrowdsale { - - function WhitelistedCrowdsaleImpl ( - uint256 _startTime, - uint256 _endTime, - uint256 _rate, - address _wallet - ) - Crowdsale(_startTime, _endTime, _rate, _wallet) - WhitelistedCrowdsale() - { - } - -} diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index dc75622..a4b9662 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -4,15 +4,19 @@ const address = require(path.resolve( __dirname, "../private.js" )).ADDRESS; const QiibeeCrowdsale = artifacts.require("./QiibeeCrowdsale.sol"); const QiibeePresale = artifacts.require("./QiibeePresale.sol"); const QiibeeToken = artifacts.require("./QiibeeToken.sol"); +const MigrationAgent = artifacts.require("./MigrationAgent.sol"); +// const MyContract = artifacts.require("./MyContract.sol"); +// const QiibeeMigrationToken = artifacts.require("./QiibeeMigrationToken.sol"); module.exports = function(deployer) { - let startTime; // blockchain block number (in timestamp) where the crowdsale will commence. - let endTime; // blockchain block number (in timestamp) where it will end. + let startTimePresale, startTimeCrowdsale; // blockchain block number (in timestamp) where the crowdsale will commence. + let endTimePresale, endTimeCrowdsale; // blockchain block number (in timestamp) where it will end. let wallet; // the address that will hold the fund. Recommended to use a multisig one for security. const rate = new web3.BigNumber(3000) // rate of ether to Qiibee Coin in ether: 3000 qbx are 1 ether const goal = new web3.BigNumber(2000000000000000000) // minimum amount of qbx to be sold 1500 qbx const cap = new web3.BigNumber(8000000000000000000) // max amount of tokens (in atto) to be sold 10bn 6000qbx + const distributionCap = new web3.BigNumber(8000000000000000000) // max amount of tokens (in atto) to be sold 10bn 6000qbx const minInvest = new web3.BigNumber(1000000000000000000) // max amount of tokens (in atto) to be sold 400qbx const maxCumulativeInvest = new web3.BigNumber(3000000000000000000) // max amount of tokens (in atto) to be sold 4000qbx const maxGasPrice = new web3.BigNumber(5000000000000000000) // max amount og gas allowed per transaction 50 gwei @@ -20,18 +24,30 @@ module.exports = function(deployer) { const presalecap = new web3.BigNumber(8000000000000000000) // max amount of tokens to be sold if (process.argv.toString().indexOf('ropsten') !== -1) { - startTime = 1509866990 + 500; - endTime = startTime + 3600000; - wallet = address; + startTimePresale = 1511085894 + 500; + endTimePresale = startTimePresale + 3600; //1 hour + + startTimeCrowdsale = 1511085894 + 1500; //10 min + endTimeCrowdsale = startTimeCrowdsale + 3600; //1 hour + + wallet = ''; console.log('Using ropsten network. Wallet address: ', wallet); } else { - startTime = web3.eth.getBlock('latest').timestamp + 300; - endTime = startTime + 3600000; + startTimePresale = web3.eth.getBlock('latest').timestamp + 60; + endTimePresale = startTimePresale + 864000; //10 days + + startTimeCrowdsale = endTimePresale + 432000; //5days + endTimeCrowdsale = startTimeCrowdsale + 864000; //10 days + wallet = web3.eth.accounts[0]; console.log('Using testrpc network. Wallet address: ', wallet); } - // deployer.deploy(QiibeeToken); - // deployer.deploy(QiibeeCrowdsale, startTime, endTime, rate, goal, cap, minInvest, maxCumulativeInvest, maxGasPrice, minBuyingRequestInterval, wallet); - deployer.deploy(QiibeePresale, startTime, endTime, goal, presalecap, maxGasPrice, minBuyingRequestInterval, wallet); + // deployer.deploy(QiibeeToken, web3.eth.accounts[9]).then(function(hola){ + // QiibeeToken.deployed().then(function(token){ + // deployer.deploy(MigrationAgent, token.address); + // }) + // }); + // deployer.deploy(QiibeePresale, startTimePresale, endTimePresale, rate, presalecap, distributionCap, maxGasPrice, minBuyingRequestInterval, wallet); + // deployer.deploy(QiibeeCrowdsale, startTimeCrowdsale, endTimeCrowdsale, rate, goal, cap, minInvest, maxCumulativeInvest, maxGasPrice, minBuyingRequestInterval, wallet); }; diff --git a/private.js b/private.js index e9ba921..d09c135 100644 --- a/private.js +++ b/private.js @@ -1,4 +1,4 @@ module.exports = { - ADDRESS: '0x7Ba631Ce4B83a05fcee8154B0Cf6765F1Fc417d4', - MNEMONIC_PHRASE: 'exhibit salmon capital index grunt deb ris lunar burst initial broccoli salute involve' + ADDRESS: '0xE4864a92f06705DEFfDDde074E9FF150C25A28EC', + MNEMONIC_PHRASE: 'exhibit salmon capital index grunt debris lunar burst initial broccoli salute involve' } diff --git a/scripts/ci.sh b/scripts/ci.sh index ff8e3d9..8b7beb7 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -3,9 +3,8 @@ set -e if [ "$SOLIDITY_COVERAGE" = true ]; then - yarn run coveralls + npm run test && cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js else + yarn test test/QiibeeToken.js test/QiibeeCrowdsale.js test/QiibeePresale.js test/WhitelistedCrowdsale.js GEN_TESTS_TIMEOUT=400 GEN_TESTS_QTY=40 yarn test test/QiibeePresaleGenTest.js test/QiibeeCrowdsaleGenTest.js - yarn test test/Crowdsale.js - yarn test test/QiibeeToken.js test/QiibeeCrowdsale.js test/WhitelistedCrowdsale.js fi diff --git a/scripts/coveralls.sh b/scripts/coveralls.sh deleted file mode 100755 index 5fa9546..0000000 --- a/scripts/coveralls.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /bin/bash - -npm run test && cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js diff --git a/scripts/test.sh b/scripts/test.sh index 91a1e26..2de23de 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -39,7 +39,7 @@ start_testrpc() { --account="0x35b5042e809eab0db3252bad02b67436f64453072128ee91c1d4605de70b27c1,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ > /dev/null & else - node_modules/.bin/testrpc --gasLimit 6000000 \ + node_modules/.bin/testrpc --gasLimit 1000006000000 \ --account="0xe8280389ca1303a2712a874707fdd5d8ae0437fab9918f845d26fd9919af5a92,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ --account="0xed095a912033d26dc444d2675b33414f0561af170d58c33f394db8812c87a764,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ --account="0xf5556ca108835f04cd7d29b4ac66f139dc12b61396b147674631ce25e6e80b9b,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ diff --git a/scripts/testrpc.sh b/scripts/testrpc.sh new file mode 100755 index 0000000..1bd90a5 --- /dev/null +++ b/scripts/testrpc.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +node_modules/.bin/testrpc --gasLimit 6000000 \ +--account="0xe8280389ca1303a2712a874707fdd5d8ae0437fab9918f845d26fd9919af5a92,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ +--account="0xed095a912033d26dc444d2675b33414f0561af170d58c33f394db8812c87a764,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ +--account="0xf5556ca108835f04cd7d29b4ac66f139dc12b61396b147674631ce25e6e80b9b,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ +--account="0xd1bea55dd05b35be047e409617bc6010b0363f22893b871ceef2adf8e97b9eb9,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ +--account="0xfc452929dc8ffd956ebab936ed0f56d71a8c537b0393ea9da4807836942045c5,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ +--account="0x12b8b2fe49596ab7f439d324797f4b5457b5bd34e9860b08828e4b01af228d93,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ +--account="0x2ed88e3846387d0ae4cca96637df48c201c86079be64d0a17bf492058db6c6eb,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ +--account="0x8c6690649d0b31790fceddd6a59decf2b03686bed940a9b85e8105c5e82f7a86,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ +--account="0xf809d1a2969bec37e7c14628717092befa82156fb2ebf935ac5420bc522f0d29,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ +--account="0x38062255973f02f1b320d8c7762dd286946b3e366f73076995dc859a6346c2ec,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ +--account="0x35b5042e809eab0db3252bad02b67436f64453072128ee91c1d4605de70b27c1,10000000000000000000000000000000000000000000000000000000000000000000000000000000" \ diff --git a/test/Crowdsale.js b/test/Crowdsale.js deleted file mode 100644 index 909d4bf..0000000 --- a/test/Crowdsale.js +++ /dev/null @@ -1,345 +0,0 @@ -/* - * This are tests taken from RefundableCrowdsale.sol, CappedCrowdsale.sol from Open Zeppelin - * and adapted to the Crowdsale.sol. - */ - -const Crowdsale = artifacts.require('CrowdsaleImpl.sol'); -const QiibeeToken = artifacts.require('QiibeeToken.sol'); - -const latestTime = require('./helpers/latestTime'); -const {increaseTimeTestRPCTo} = require('./helpers/increaseTime'); -const { duration } = require('./helpers/increaseTime'); -const help = require('./helpers.js'); -const BigNumber = web3.BigNumber; - -require('chai'). - use(require('chai-bignumber')(BigNumber)). - should(); - -function assertExpectedException(e) { - let isKnownException = help.isInvalidOpcodeEx(e); - if (!isKnownException) { - throw(e); - } -} - -const value = help.toWei(1); - -contract('Crowdsale', function ([owner, wallet, investor]) { - - const defaultTimeDelta = duration.days(1); // time delta used in time calculations (for start, end1 & end2) - const defaults = { - goal: new BigNumber(help.toWei(800)), - lessThanGoal: new BigNumber(help.toWei(750)), - cap: new BigNumber(help.toWei(1800)), - lessThanCap: new BigNumber(help.toWei(1000)), - maxGasPrice: new BigNumber(5000000000000000000), - minBuyingRequestInterval: 600, - wallet: wallet - }; - - async function createCrowdsale(params) { - const startTime = params.startTime === undefined ? (latestTime() + defaultTimeDelta) : params.startTime, - endTime = params.endTime === undefined ? (startTime + duration.weeks(1)) : params.endTime, - goal = params.goal === undefined ? defaults.goal : params.goal, - cap = params.cap === undefined ? defaults.cap : params.cap, - maxGasPrice = params.maxGasPrice === undefined ? defaults.maxGasPrice : params.maxGasPrice, - minBuyingRequestInterval = params.minBuyingRequestInterval === undefined ? defaults.minBuyingRequestInterval : params.minBuyingRequestInterval, - wallet = params.wallet === undefined ? defaults.wallet : params.foundationWallet; - - return await Crowdsale.new(startTime, endTime, goal, cap, maxGasPrice, minBuyingRequestInterval, wallet, {from: owner}); - } - - describe('create crowdsale tests', function () { - - it('can NOT create crowdsale with endTime bigger than startTime', async function () { - const startTime = latestTime() + duration.weeks(1), - endTime = startTime - duration.weeks(1); - try { - await createCrowdsale({startTime: startTime, endTime: endTime}); - } catch(e) { - assertExpectedException(e); - } - }); - - it('can NOT create crowdsale with zero minBuyingRequestInterval', async function () { - try { - await createCrowdsale({minBuyingRequestInterval: 0}); - } catch(e) { - assertExpectedException(e); - } - }); - - it('can NOT create crowdsale with zero goal', async function () { - try { - await createCrowdsale({goal: 0}); - } catch(e) { - assertExpectedException(e); - } - }); - - it('can NOT create crowdsale with zero wallet', async function () { - try { - await createCrowdsale({goal: help.zeroAddress}); - } catch(e) { - assertExpectedException(e); - } - }); - - }); - - it('should be token owner', async function () { - const crowdsale = await createCrowdsale({}), - token = QiibeeToken.at(await crowdsale.token()), - owner = await token.owner(); - assert.equal(owner, crowdsale.address); - }); - - it('should be ended only after end', async function () { - const crowdsale = await createCrowdsale({}); - let ended = await crowdsale.hasEnded(); - assert.equal(ended, false); - await increaseTimeTestRPCTo(await crowdsale.endTime() + duration.seconds(1)); - ended = await crowdsale.hasEnded(); - assert.equal(ended, true); - }); - - it('should fail creating crowdsale with zero maxGasPrice', async function () { - try { - await createCrowdsale({maxGasPrice: 0}); - } catch (e) { - assertExpectedException(e); - } - }); - - describe('accepting payments', function () { - - it('should reject payments before start', async function () { - const crowdsale = await createCrowdsale({}); - try { - await crowdsale.send(value); - } catch (e) { - assertExpectedException(e); - } - try { - await crowdsale.buyTokens(investor, {from: investor, value: value}); - } catch(e) { - assertExpectedException(e); - } - }); - - it('should reject payments if beneficiary address is zero', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - try { - await crowdsale.buyTokens(help.zeroAddress, {value: value, from: investor}); - } catch (e) { - assertExpectedException(e); - } - }); - - it('should accept payments after start', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - await crowdsale.sendTransaction({value: value, from: investor}); - await crowdsale.buyTokens(investor, {value: value, from: investor}); - }); - - it('should reject payments after end', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.endTime() + duration.seconds(1)); - try { - await crowdsale.sendTransaction({value: value, from: investor}); - } catch (e) { - assertExpectedException(e); - } - try { - await crowdsale.buyTokens(investor, {value: value, from: investor}); - } catch(e) { - assertExpectedException(e); - } - }); - - }); - - describe('finalize crowdsale tests', function () { - - it('should reject finalize if crowdsale is already finalized', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - await crowdsale.sendTransaction({value: defaults.goal, from: investor}); - await increaseTimeTestRPCTo(await crowdsale.endTime() + duration.seconds(1)); - await crowdsale.finalize({from: owner}); - try { - await crowdsale.finalize({from: owner}); - } catch(e) { - assertExpectedException(e); - } - }); - - it('should reject finalize if cap not reached and now < endTime', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - await crowdsale.sendTransaction({value: defaults.goal, from: investor}); - try { - await crowdsale.finalize({from: owner}); - } catch(e) { - assertExpectedException(e); - } - }); - - it('should finalize if cap reached even though now < endTime', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - await crowdsale.sendTransaction({value: defaults.cap, from: investor}); - try { - await crowdsale.finalize({from: owner}); - } catch(e) { - assertExpectedException(e); - } - }); - - }); - // RefundableCrowdsale.sol - describe('refundable crowdsale tests', function () { - - it('can NOT create crowdsale with goal less than zero', async function () { - try { - await createCrowdsale({goal: 0}); - } catch(e) { - assertExpectedException(e); - } - }); - - it('should deny refunds before end', async function () { - const crowdsale = await createCrowdsale({}); - try { - await crowdsale.claimRefund({from: investor}); - } catch (e) { - assertExpectedException(e); - } - await increaseTimeTestRPCTo(await crowdsale.startTime()); - try { - await crowdsale.claimRefund({from: investor}); - } catch (e) { - assertExpectedException(e); - } - }); - - it('should deny refunds after end if goal was reached', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - await crowdsale.sendTransaction({value: defaults.goal, from: investor}); - await increaseTimeTestRPCTo(await crowdsale.endTime() + duration.seconds(1)); - try { - await crowdsale.claimRefund({from: investor}); - } catch (e) { - assertExpectedException(e); - } - }); - - it('should allow refunds after end if goal was not reached', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - await crowdsale.sendTransaction({value: defaults.lessThanGoal, from: investor}); - await increaseTimeTestRPCTo(await crowdsale.endTime() + duration.seconds(1)); - - await crowdsale.finalize({from: owner}); - - const pre = web3.eth.getBalance(investor); - await crowdsale.claimRefund({from: investor, gasPrice: 0}); - const post = web3.eth.getBalance(investor); - post.minus(pre).should.be.bignumber.equal(defaults.lessThanGoal); - }); - - it('should forward funds to wallet after end if goal was reached', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - await crowdsale.sendTransaction({value: defaults.goal, from: investor}); - await increaseTimeTestRPCTo(await crowdsale.endTime() + duration.seconds(1)); - - const pre = web3.eth.getBalance(wallet); - await crowdsale.finalize({from: owner}); - const post = web3.eth.getBalance(wallet); - - post.minus(pre).should.be.bignumber.equal(defaults.goal); - }); - - }); - - // CappedCrowdsale.sol - describe('capped crowdsale tests', function () { - it('should fail creating crowdsale with zero cap', async function () { - try { - await createCrowdsale({cap: 0}); - } catch (e) { - assertExpectedException(e); - } - }); - - describe('accepting payments', function () { - - it('should accept payments within cap', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - await crowdsale.sendTransaction({value: defaults.cap.minus(defaults.lessThanCap), from: investor}); - await crowdsale.sendTransaction({value: defaults.lessThanCap, from: investor}); - }); - - it('should reject payments outside cap', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - await crowdsale.sendTransaction({value: defaults.cap, from: investor}); - - try { - await crowdsale.sendTransaction({value: 1, from: investor}); - } catch (e) { - assertExpectedException(e); - } - }); - - it('should reject payments that exceed cap', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - try { - await crowdsale.sendTransaction({value: defaults.cap.plus(1), from: investor}); - } catch (e) { - assertExpectedException(e); - } - }); - - }); - - describe('ending', function () { - - it('should not be ended if under cap', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - let hasEnded = await crowdsale.hasEnded(); - hasEnded.should.equal(false); - await crowdsale.sendTransaction({value: defaults.lessThanCap, from: investor}); - hasEnded = await crowdsale.hasEnded(); - hasEnded.should.equal(false); - }); - - it('should not be ended if just under cap', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - await crowdsale.sendTransaction({value: defaults.cap.minus(1), from: investor}); - let hasEnded = await crowdsale.hasEnded(); - hasEnded.should.equal(false); - }); - - it('should be ended if cap reached', async function () { - const crowdsale = await createCrowdsale({}); - await increaseTimeTestRPCTo(await crowdsale.startTime()); - await crowdsale.sendTransaction({value: defaults.cap, from: investor}); - let hasEnded = await crowdsale.hasEnded(); - hasEnded.should.equal(true); - }); - - }); - - }); - -}); diff --git a/test/QiibeeCrowdsale.js b/test/QiibeeCrowdsale.js index 03f60e7..1780758 100644 --- a/test/QiibeeCrowdsale.js +++ b/test/QiibeeCrowdsale.js @@ -51,11 +51,7 @@ contract('QiibeeCrowdsale', function ([owner, wallet]) { } it('can create a qiibee crowdsale', async function () { - try { - await createCrowdsale({}); - } catch (e) { - assertExpectedException(e); - } + await createCrowdsale({}); }); it('should fail creating qiibee crowdsale with zero rate', async function () { @@ -90,4 +86,20 @@ contract('QiibeeCrowdsale', function ([owner, wallet]) { } }); + it('should fail creating qiibee crowdsale with zero maxGasPrice', async function () { + try { + await createCrowdsale({maxGasPrice: 0}); + } catch (e) { + assertExpectedException(e); + } + }); + + it('should fail creating qiibee crowdsale with zero minBuyingRequestInterval', async function () { + try { + await createCrowdsale({minBuyingRequestInterval: 0}); + } catch (e) { + assertExpectedException(e); + } + }); + }); diff --git a/test/QiibeeCrowdsaleGenTest.js b/test/QiibeeCrowdsaleGenTest.js index 09b76c3..e2c6b2e 100644 --- a/test/QiibeeCrowdsaleGenTest.js +++ b/test/QiibeeCrowdsaleGenTest.js @@ -38,8 +38,8 @@ contract('QiibeeCrowdsale property-based test', function(accounts) { let checkCrowdsaleState = async function(state, crowdsaleData, crowdsale) { assert.equal(gen.getAccount(state.wallet), await crowdsale.wallet()); assert.equal(state.crowdsalePaused, await crowdsale.paused()); - let tokensInPurchases = sumBigNumbers(_.map(state.purchases, (p) => p.tokens)); + tokensInPurchases.should.be.bignumber.equal(help.fromAtto(await crowdsale.tokensSold())); help.debug(colors.yellow('checking purchases total wei, purchases:', JSON.stringify(state.purchases))); @@ -69,7 +69,8 @@ contract('QiibeeCrowdsale property-based test', function(accounts) { let {rate, goal, cap, minInvest, maxCumulativeInvest, maxGasPrice, minBuyingRequestInterval, owner} = input.crowdsale, ownerAddress = gen.getAccount(input.crowdsale.owner), - foundationWallet = gen.getAccount(input.crowdsale.foundationWallet); + foundationWallet = gen.getAccount(input.crowdsale.foundationWallet), + migrationMaster = gen.getAccount(input.crowdsale.foundationWallet); let shouldThrow = (rate == 0) || (latestTime() >= startTime) || @@ -117,7 +118,13 @@ contract('QiibeeCrowdsale property-based test', function(accounts) { assert.equal(false, shouldThrow, 'create Crowdsale should have thrown but it did not'); - let token = QiibeeToken.at(await crowdsale.token()); + // let token = QiibeeToken.at(await crowdsale.token()); + let token = await QiibeeToken.new(migrationMaster, {from: ownerAddress}); + await token.pause({from: ownerAddress}); + + //set token to presale + await crowdsale.setToken(token.address, {from: ownerAddress}); + await token.transferOwnership(crowdsale.address,{ from: ownerAddress}); eventsWatcher = crowdsale.allEvents(); eventsWatcher.watch(function(error, log){ @@ -193,13 +200,13 @@ contract('QiibeeCrowdsale property-based test', function(accounts) { commands: [ { type: 'waitTime','seconds':duration.days(1)}, { type: 'fundCrowdsaleBelowCap','account':0,'finalize':false}, - { type: 'buyTokens', beneficiary: 2, account: 2, eth: 100 }, { type: 'waitTime','seconds':duration.minutes(12)}, - { type: 'buyTokens', beneficiary: 4, account: 4, eth: 500000 }, + { type: 'buyTokens', beneficiary: 4, account: 4, eth: 205000 }, ], - crowdsale: { + crowdsale: + { rate: 6000, goal: 36000, cap: 240000, - minInvest: 6000, maxCumulativeInvest: 240000, + minInvest: 6000, maxCumulativeInvest: 250000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, owner: 0, foundationWallet: 10 } @@ -377,11 +384,11 @@ contract('QiibeeCrowdsale property-based test', function(accounts) { { type: 'checkRate', fromAccount: 3 }, { type: 'waitTime','seconds':duration.days(1)}, { type: 'fundCrowdsaleBelowCap','account':3,'finalize':false}, - { type: 'buyTokens', beneficiary: 3, account: 4, eth: 10 }, - { type: 'buyTokens', beneficiary: 3, account: 5, eth: 4 }, + { type: 'buyTokens', beneficiary: 3, account: 4, eth: 6000 }, + { type: 'buyTokens', beneficiary: 3, account: 5, eth: 7000 }, { type: 'waitTime','seconds':duration.days(1)}, - { type: 'buyTokens', beneficiary: 3, account: 6, eth: 50000 }, - { type: 'finalizeCrowdsale', fromAccount: 2 } + { type: 'buyTokens', beneficiary: 3, account: 6, eth: 60000 }, + { type: 'finalizeCrowdsale', fromAccount: 0 } ], crowdsale: { rate: 6000, goal: 36000, cap: 240000, @@ -444,9 +451,9 @@ contract('QiibeeCrowdsale property-based test', function(accounts) { let crowdsaleAndCommands = { commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'finalizeCrowdsale', fromAccount: 1 }, + { type: 'finalizeCrowdsale', fromAccount: 0 }, { type: 'waitTime','seconds':duration.days(5)}, - { type: 'finalizeCrowdsale', fromAccount: 1 }, + { type: 'finalizeCrowdsale', fromAccount: 0 }, ], crowdsale: { rate: 6000, goal: 36000, cap: 240000, @@ -491,7 +498,7 @@ contract('QiibeeCrowdsale property-based test', function(accounts) { await runGeneratedCrowdsaleAndCommands({ commands: [ { type: 'fundCrowdsaleBelowCap','account':0,'finalize':true}, - { type: 'finalizeCrowdsale','fromAccount':3} + { type: 'finalizeCrowdsale','fromAccount':0} ], crowdsale: { rate: 6000, goal: 36000, cap: 240000, @@ -501,6 +508,22 @@ contract('QiibeeCrowdsale property-based test', function(accounts) { } }); }); + + it('should handle exception fine when trying to finalize not being an owner', async function() { + await runGeneratedCrowdsaleAndCommands({ + commands: [ + { type: 'fundCrowdsaleBelowCap','account':0,'finalize':true}, + { type: 'finalizeCrowdsale','fromAccount':1} + ], + crowdsale: { + rate: 6000, goal: 36000, cap: 240000, + minInvest: 6000, maxCumulativeInvest: 240000, + maxGasPrice: 50000000000, minBuyingRequestInterval: 600, + owner: 0, foundationWallet: 10 + } + }); + }); + }); describe('burn tokens tests', function () { diff --git a/test/QiibeePresale.js b/test/QiibeePresale.js new file mode 100644 index 0000000..889c751 --- /dev/null +++ b/test/QiibeePresale.js @@ -0,0 +1,99 @@ +/* + * This are tests taken from RefundableCrowdsale.sol, CappedCrowdsale.sol from Open Zeppelin + * and adapted to the Crowdsale. + */ + +const QiibeePresale = artifacts.require('QiibeePresale.sol'); +const QiibeeToken = artifacts.require('QiibeeToken.sol'); + +const latestTime = require('./helpers/latestTime'); +const { increaseTimeTestRPCTo, duration } = require('./helpers/increaseTime'); +const help = require('./helpers.js'); +const BigNumber = web3.BigNumber; + +require('chai'). + use(require('chai-bignumber')(BigNumber)). + should(); + +function assertExpectedException(e) { + let isKnownException = help.isInvalidOpcodeEx(e); + if (!isKnownException) { + throw(e); + } +} + +contract('QiibeePresale', function ([owner, wallet, migrationMaster]) { + + const defaultTimeDelta = duration.days(1); // time delta used in time calculations (for start, end1 & end2) + const defaults = { + rate: 6000, + goal: new BigNumber(help.toWei(800)), + cap: new BigNumber(help.toWei(1800)), + distributionCap: new BigNumber(help.toAtto(100)), + maxGasPrice: new BigNumber(5000000000000000000), + minBuyingRequestInterval: 600, + wallet: wallet + }; + + async function createPresale(params) { + const startTime = params.start === undefined ? (latestTime() + defaultTimeDelta) : params.start, + endTime = params.endTime === undefined ? (startTime + duration.weeks(1)) : params.endTime, + rate = params.rate === undefined ? defaults.rate : params.rate, + cap = params.cap === undefined ? defaults.cap : params.cap, + distributionCap = params.distributionCap === undefined ? defaults.distributionCap : params.distributionCap, + maxGasPrice = params.maxGasPrice === undefined ? defaults.maxGasPrice : params.maxGasPrice, + minBuyingRequestInterval = params.minBuyingRequestInterval === undefined ? defaults.minBuyingRequestInterval : params.minBuyingRequestInterval, + wallet = params.wallet === undefined ? defaults.wallet : params.foundationWallet; + + return await QiibeePresale.new(startTime, endTime, rate, cap, distributionCap, maxGasPrice, minBuyingRequestInterval, wallet, {from: owner}); + } + + it('can create a qiibee presale', async function () { + await createPresale({}); + }); + + it('should fail creating qiibee presale with zero rate', async function () { + try { + await createPresale({rate: 0}); + } catch (e) { + assertExpectedException(e); + } + }); + + it('should fail creating qiibee presale with zero distributionCap', async function () { + try { + await createPresale({distributionCap: 0}); + } catch (e) { + assertExpectedException(e); + } + }); + + it('should fail creating qiibee presale with zero maxGasPrice', async function () { + try { + await createPresale({maxGasPrice: 0}); + } catch (e) { + assertExpectedException(e); + } + }); + + it('should fail creating qiibee presale with zero minBuyingRequestInterval', async function () { + try { + await createPresale({minBuyingRequestInterval: 0}); + } catch (e) { + assertExpectedException(e); + } + }); + + it('should fail setting token after startTime', async function () { + let presale = await createPresale({}); + let token = await QiibeeToken.new(migrationMaster); + + await increaseTimeTestRPCTo(await presale.startTime()); + try { + await presale.setToken(token.address); + } catch (e) { + assertExpectedException(e); + } + }); + +}); diff --git a/test/QiibeePresaleGenTest.js b/test/QiibeePresaleGenTest.js index e0fd488..c3e109a 100644 --- a/test/QiibeePresaleGenTest.js +++ b/test/QiibeePresaleGenTest.js @@ -42,68 +42,75 @@ contract('QiibeePresale property-based test', function(accounts) { let tokensInPurchases = sumBigNumbers(_.map(state.purchases, (p) => p.tokens)); tokensInPurchases.should.be.bignumber.equal(help.fromAtto(await presale.tokensSold())); - help.debug(colors.yellow('checking purchases total wei, purchases:', JSON.stringify(state.purchases))); + help.debug('checking purchases total wei, purchases:', JSON.stringify(state.purchases)); let weiInPurchases = sumBigNumbers(_.map(state.purchases, (p) => p.wei)); weiInPurchases.should.be.bignumber.equal(await presale.weiRaised()); assert.equal(state.presaleFinalized, await presale.isFinalized()); - - // if (state.presaleFinalized) { - // assert.equal(state.goalReached, await presale.goalReached()); - // } }; let runGeneratedPresaleAndCommands = async function(input) { await increaseTimeTestRPC(60); let startTime = latestTime() + duration.days(1); let endTime = startTime + duration.days(1); - help.debug(colors.yellow('presaleTestInput data:\n', JSON.stringify(input), startTime, endTime)); + help.debug('presaleTestInput data:\n', JSON.stringify(input), startTime, endTime); - let {maxGasPrice, minBuyingRequestInterval, goal, cap, owner} = input.presale, + let {rate, maxGasPrice, minBuyingRequestInterval, cap, distributionCap, owner} = input.presale, ownerAddress = gen.getAccount(input.presale.owner), - foundationWallet = gen.getAccount(input.presale.foundationWallet); + foundationWallet = gen.getAccount(input.presale.foundationWallet), + migrationMaster = gen.getAccount(input.presale.foundationWallet); let shouldThrow = (latestTime() >= startTime) || (startTime >= endTime) || + (rate == 0) || (maxGasPrice == 0) || (minBuyingRequestInterval < 0) || - (goal == 0) || (cap == 0) || - (goal >= cap) || + (distributionCap == 0) || (ownerAddress == 0) || (foundationWallet == 0); var eventsWatcher; try { - let presaleData = { startTime: startTime, endTime: endTime, + vestFromTime: 1530316800, maxGasPrice: new BigNumber(maxGasPrice), minBuyingRequestInterval: minBuyingRequestInterval, - goal: new BigNumber(help.toWei(goal)), + rate: rate, cap: new BigNumber(help.toWei(cap)), + distributionCap: new BigNumber(help.toAtto(distributionCap)), foundationWallet: gen.getAccount(input.presale.foundationWallet), }; + let presale = await QiibeePresale.new( presaleData.startTime, presaleData.endTime, - presaleData.goal, + presaleData.rate, presaleData.cap, + presaleData.distributionCap, presaleData.maxGasPrice, presaleData.minBuyingRequestInterval, presaleData.foundationWallet, {from: ownerAddress} ); - let token = QiibeeToken.at(await presale.token()); + + let token = await QiibeeToken.new(migrationMaster, {from: ownerAddress}); + await token.pause({from: ownerAddress}); + + //set token to presale + await presale.setToken(token.address, {from: ownerAddress}); + await token.transferOwnership(presale.address,{ from: ownerAddress}); + assert.equal(false, shouldThrow, 'create Presale should have thrown but it did not'); eventsWatcher = presale.allEvents(); eventsWatcher.watch(function(error, log){ if (LOG_EVENTS) console.log('Event:', log.event, ':',log.args); }); - help.debug(colors.yellow('created presale at address ', presale.address)); + help.debug('created presale at address ', presale.address); var state = { presaleData: presaleData, presaleContract: presale, @@ -114,10 +121,10 @@ contract('QiibeePresale property-based test', function(accounts) { purchases: [], weiRaised: zero, tokensSold: zero, + tokensDistributed: zero, tokenPaused: true, presaleFinalized: false, presalePaused: false, - goalReached: false, presaleSupply: zero, burnedTokens: zero, owner: owner, @@ -155,6 +162,159 @@ contract('QiibeePresale property-based test', function(accounts) { return true; }; + it('executes a normal TGE fine', async function() { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'waitTime','seconds':duration.days(1)}, + { type: 'addAccredited', investor: 4, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 6000, maxCumulativeInvest: 240000, fromAccount: 0 }, + { type: 'addAccredited', investor: 5, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 8000, maxCumulativeInvest: 240000, fromAccount: 0 }, + { type: 'presaleBuyTokens', beneficiary: 3, account: 4, eth: 6000 }, + { type: 'presaleBuyTokens', beneficiary: 3, account: 5, eth: 7000 }, + { type: 'presaleBuyTokens', beneficiary: 3, account: 5, eth: 8000 }, + { type: 'presaleBuyTokens', beneficiary: 4, account: 1, eth: 7000 }, + { type: 'waitTime','seconds':duration.days(1)}, + { type: 'presaleBuyTokens', beneficiary: 3, account: 6, eth: 60000 }, + { type: 'distributeTokens', beneficiary: 4, amount: 70000, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + { type: 'distributeTokens', beneficiary: 4, amount: 5000, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + { type: 'distributeTokens', beneficiary: 4, amount: 1, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + { type: 'finalizePresale', fromAccount: 0 } + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000, foundationWallet: 10, owner: 0 + } + }); + }); + + describe('tokens distribution', function () { + + it('can distribute tokens after fundraising has finished (now >= endTime)', async function () { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'waitTime','seconds':duration.days(2)}, + { type: 'distributeTokens', beneficiary: 4, amount: 1, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + { type: 'finalizePresale', fromAccount: 0 } + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }); + }); + + it('can distribute non-vested tokens', async function () { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'waitTime','seconds':duration.days(2)}, + { type: 'distributeTokens', beneficiary: 4, amount: 1, cliff: 0, vesting: 0, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }); + }); + + it('can distribute tokens after fundraising has finished (weiRaised = cap)', async function () { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'waitTime','seconds':duration.days(1)}, + { type: 'addAccredited', investor: 4, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 240000, fromAccount: 0 }, + { type: 'presaleBuyTokens', account: 4, beneficiary: 5, eth: 240000 }, + { type: 'distributeTokens', beneficiary: 4, amount: 1, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }); + }); + + it('can NOT distribute tokens if presale has finished', async function () { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'waitTime','seconds':duration.days(3)}, + { type: 'finalizePresale', fromAccount: 0 }, + { type: 'distributeTokens', beneficiary: 1, amount: 1, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }); + }); + + it('can NOT distribute tokens with zero beneficiary', async function () { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'waitTime','seconds':duration.days(2)}, + { type: 'distributeTokens', beneficiary: 'zero', amount: 1, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }); + }); + + it('can NOT distribute 0 tokens', async function () { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'waitTime','seconds':duration.days(2)}, + { type: 'distributeTokens', beneficiary: 1, amount: 0, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }); + }); + + it('can NOT distribute tokens with vesting less than cliff', async function () { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'waitTime','seconds':duration.days(2)}, + { type: 'distributeTokens', beneficiary: 1, amount: 1, cliff: 6000, vesting: 5000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }); + }); + + it('can NOT distribute tokens before or during fundraising', async function () { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'distributeTokens', beneficiary: 4, amount: 1, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + { type: 'waitTime','seconds':duration.days(1)}, + { type: 'distributeTokens', beneficiary: 4, amount: 1, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }); + }); + + it('can NOT distribute more tokens than the distribution cap', async function () { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'waitTime','seconds':duration.days(2)}, + { type: 'distributeTokens', beneficiary: 4, amount: 75000000, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + { type: 'distributeTokens', beneficiary: 4, amount: 1, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 0 }, + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }); + }); + + it('can NOT distribute tokens if not owner', async function () { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'waitTime','seconds':duration.days(2)}, + { type: 'distributeTokens', beneficiary: 4, amount: 1, cliff: 6000, vesting: 6000, revokable: false, burnsOnRevoke: false, fromAccount: 1 }, + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }); + }); + + }); + describe('buying tokens', function () { it('should NOT allow non-accredited investors to invest', async function () { @@ -164,7 +324,7 @@ contract('QiibeePresale property-based test', function(accounts) { { type: 'presaleSendTransaction', account: 4, eth: 1 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -173,11 +333,11 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, + { type: 'addAccredited', investor: 4, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, { type: 'presaleSendTransaction', account: 4, eth: 1 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -186,11 +346,11 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, + { type: 'addAccredited', investor: 4, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, { type: 'presaleSendTransaction', account: 'zero', eth: 1 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -199,11 +359,11 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 0, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, + { type: 'addAccredited', investor: 4, cliff: 0, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, { type: 'presaleSendTransaction', account: 4, eth: 1 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -212,11 +372,11 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 0, vesting: 0, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, + { type: 'addAccredited', investor: 4, cliff: 0, vesting: 0, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, { type: 'presaleSendTransaction', account: 4, eth: 1 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -225,11 +385,11 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 0, vesting: 0, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, + { type: 'addAccredited', investor: 4, cliff: 0, vesting: 0, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, { type: 'presaleBuyTokens', account: 4, beneficiary: 5, eth: 1 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -238,11 +398,11 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, + { type: 'addAccredited', investor: 4, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, { type: 'presaleBuyTokens', beneficiary: 3, account: 4, eth: 3 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -251,11 +411,23 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, + { type: 'addAccredited', investor: 4, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, { type: 'presaleBuyTokens', beneficiary: 3, account: 4, eth: 0.5 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }); + }); + + it('should NOT allow buying tokens with zero beneficiary address', async function () { + await runGeneratedPresaleAndCommands({ + commands: [ + { type: 'waitTime','seconds':duration.days(1)}, + { type: 'presaleBuyTokens', account: 4, beneficiary: 'zero', eth: 1 }, + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -271,7 +443,7 @@ contract('QiibeePresale property-based test', function(accounts) { { type: 'addAccredited', investor: 4, rate: 0, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -280,10 +452,10 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 600, vesting: 100, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, + { type: 'addAccredited', investor: 4, cliff: 600, vesting: 100, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -292,10 +464,10 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 0, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, + { type: 'addAccredited', investor: 4, cliff: 0, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -304,10 +476,10 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 0, fromAccount: 0 }, + { type: 'addAccredited', investor: 4, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 0, fromAccount: 0 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -316,10 +488,10 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 0, maxCumulativeInvest: 2, fromAccount: 0 }, + { type: 'addAccredited', investor: 4, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 0, maxCumulativeInvest: 2, fromAccount: 0 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -328,10 +500,10 @@ contract('QiibeePresale property-based test', function(accounts) { await runGeneratedPresaleAndCommands({ commands: [ { type: 'waitTime','seconds':duration.days(1)}, - { type: 'addAccredited', investor: 4, rate: 6000, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 2 }, + { type: 'addAccredited', investor: 4, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 2 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -343,7 +515,7 @@ contract('QiibeePresale property-based test', function(accounts) { { type: 'addAccredited', investor: 'zero', rate: 6000, cliff: 600, vesting: 600, revokable: false, burnsOnTokens: false, minInvest: 1, maxCumulativeInvest: 2, fromAccount: 0 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -355,7 +527,7 @@ contract('QiibeePresale property-based test', function(accounts) { { type: 'removeAccredited', investor: 4, fromAccount: 0 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -367,7 +539,7 @@ contract('QiibeePresale property-based test', function(accounts) { { type: 'removeAccredited', investor: 4, fromAccount: 2 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -379,7 +551,7 @@ contract('QiibeePresale property-based test', function(accounts) { { type: 'removeAccredited', investor: 'zero', fromAccount: 0 }, ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }); }); @@ -407,10 +579,24 @@ contract('QiibeePresale property-based test', function(accounts) { let presaleAndCommands = { commands: [ { type: 'waitTime','seconds':duration.days(4)}, - { type: 'finalizePresale', fromAccount: 1 } + { type: 'finalizePresale', fromAccount: 0 } + ], + presale: { + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 + } + }; + + await runGeneratedPresaleAndCommands(presaleAndCommands); + }); + + it('should NOT finish presale if called by non-owner', async function() { + let presaleAndCommands = { + commands: [ + { type: 'waitTime','seconds':duration.days(4)}, + { type: 'finalizePresale', fromAccount: 0 } ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 1 } }; @@ -421,11 +607,11 @@ contract('QiibeePresale property-based test', function(accounts) { let presaleAndCommands = { commands: [ { type: 'waitTime','seconds':duration.days(4)}, - { type: 'finalizePresale', fromAccount: 1 }, - { type: 'finalizePresale', fromAccount: 2 } + { type: 'finalizePresale', fromAccount: 0 }, + { type: 'finalizePresale', fromAccount: 0 } ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }; @@ -436,10 +622,10 @@ contract('QiibeePresale property-based test', function(accounts) { let presaleAndCommands = { commands: [ { type: 'waitTime','seconds':duration.minutes(60)}, - { type: 'finalizePresale', fromAccount: 1 } + { type: 'finalizePresale', fromAccount: 0 } ], presale: { - maxGasPrice: 50000000000, minBuyingRequestInterval: 600, goal: 36000, cap: 240000, foundationWallet: 10, owner: 0 + rate: 6000, maxGasPrice: 50000000000, minBuyingRequestInterval: 600, cap: 240000, distributionCap: 75000000, foundationWallet: 10, owner: 0 } }; diff --git a/test/QiibeeToken.js b/test/QiibeeToken.js index 2ca7362..4b4205f 100644 --- a/test/QiibeeToken.js +++ b/test/QiibeeToken.js @@ -6,14 +6,17 @@ require('chai') .use(require('chai-bignumber')(BigNumber)) .should(); -var QiibeeToken = artifacts.require('../QiibeeToken.sol'); +var QiibeeToken = artifacts.require('QiibeeToken.sol'); +var QiibeeMigrationToken = artifacts.require('QiibeeMigrationToken.sol'); +var MigrationAgent = artifacts.require('MigrationAgent.sol'); contract('qiibeeToken', function(accounts) { - let token; + let token, targetToken, migrationAgent, + migrationMaster = accounts[5]; beforeEach(async function() { - token = await QiibeeToken.new(); + token = await QiibeeToken.new(migrationMaster); await token.mint(accounts[0], web3.toWei(1), {from: await token.owner()}); await token.mint(accounts[1], web3.toWei(1), {from: await token.owner()}); }); @@ -24,6 +27,14 @@ contract('qiibeeToken', function(accounts) { assert.equal(18, await token.DECIMALS()); }); + it('can NOT create a qiibee token with no migration master', async function() { + try { + await QiibeeToken.new(migrationMaster); + } catch (error) { + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + it('can burn tokens', async function() { let totalSupply = await token.totalSupply.call(); new BigNumber(0).should.be.bignumber.equal(await token.balanceOf(accounts[2])); @@ -119,4 +130,190 @@ contract('qiibeeToken', function(accounts) { }); + describe('creating migration agent', async () => { + + beforeEach(async function() { + migrationAgent = await MigrationAgent.new(token.address); + targetToken = await QiibeeMigrationToken.new(migrationAgent.address); + assert.equal(help.zeroAddress, await token.migrationAgent()); + }); + + it('can NOT create migration agent if it has already been created', async () => { + await token.setMigrationAgent(migrationAgent.address, {from: migrationMaster}); + try { + migrationAgent = await MigrationAgent.new(token.address); + } catch(error) { + assert.notEqual(help.zeroAddress, await token.migrationAgent()); + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + it('can set a MigrationAgent', async () => { + await token.setMigrationAgent(migrationAgent.address, {from: migrationMaster}); + assert.notEqual(help.zeroAddress, await token.migrationAgent()); + }); + + it('can NOT set a MigrationAgent if not master', async () => { + try { + await token.setMigrationAgent(migrationAgent.address, {from: accounts[1]}); + } catch(error) { + assert.equal(help.zeroAddress, await token.migrationAgent()); + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + it('can NOT set a MigrationAgent if already set', async () => { + await token.setMigrationAgent(migrationAgent.address, {from: migrationMaster}); + try { + await token.setMigrationAgent(migrationAgent.address, {from: migrationMaster}); + } catch(error) { + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + it('can NOT set a MigrationAgent if address is zero', async () => { + try { + await token.setMigrationAgent(help.zeroAddress, {from: migrationMaster}); + } catch(error) { + assert.notEqual(help.zeroAddress, await token.migrationAgent()); + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + }); + + describe('other migrating tests', async () => { + + it('can NOT migrate tokens if migration has not been set', async () => { + try { + await token.migrate(web3.toWei(1), {from: accounts[1]}); + } catch(error) { + assert.equal(help.zeroAddress, await token.migrationAgent()); + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + it('can NOT migrate tokens if target token has not been set to migration agent', async () => { + migrationAgent = await MigrationAgent.new(token.address); + await token.setMigrationAgent(migrationAgent.address, {from: migrationMaster}); + + try { + await token.migrate(web3.toWei(1), {from: accounts[1]}); + } catch(error) { + assert.notEqual(help.zeroAddress, await token.migrationAgent()); + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + it('can NOT finalize migration if target token has not been set', async () => { + migrationAgent = await MigrationAgent.new(token.address); + try { + await migrationAgent.finalizeMigration(); + } catch(error) { + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + it('can change migration master', async () => { + await token.setMigrationMaster(accounts[2], {from: migrationMaster}); + assert.equal(accounts[2], await token.migrationMaster()); + }); + + it('can NOT change migration master if not migration master', async () => { + try { + await token.setMigrationMaster(accounts[2], {from: accounts[2]}); + } catch(error) { + assert.equal(migrationMaster, await token.migrationMaster()); + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + }); + + describe('migrating tokens', async () => { + + beforeEach(async function() { + migrationAgent = await MigrationAgent.new(token.address); + targetToken = await QiibeeMigrationToken.new(migrationAgent.address); + await migrationAgent.setTargetToken(targetToken.address); + await token.setMigrationAgent(migrationAgent.address, {from: migrationMaster}); + assert.notEqual(help.zeroAddress, await token.migrationAgent()); + }); + + it('can migrate tokens', async () => { + await token.migrate(web3.toWei(1), {from: accounts[1]}); + assert.notEqual(help.zeroAddress, await token.migrationAgent()); + (await token.balanceOf(accounts[1])).should.be.bignumber.equal(new BigNumber(0)); + (await token.totalMigrated()).should.be.bignumber.equal(web3.toWei(1)); + }); + + it('can NOT migrate 0 tokens', async () => { + try { + await token.migrate(web3.toWei(0), {from: accounts[1]}); + } catch(error) { + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + it('can NOT migrate more tokens than my balance', async () => { + try { + await token.migrate(web3.toWei(2), {from: accounts[1]}); + } catch(error) { + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + it('can NOT migrate from another source', async () => { + try { + await migrationAgent.migrateFrom(accounts[0], web3.toWei(1), {from: accounts[0]}); + } catch(error) { + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + it('can finalize migration', async () => { + await token.migrate(web3.toWei(1), {from: accounts[0]}); + await token.migrate(web3.toWei(1), {from: accounts[1]}); + await migrationAgent.finalizeMigration(); + }); + + it('can NOT finalize migration until all tokens have been migrated', async () => { + await token.migrate(web3.toWei(1), {from: accounts[0]}); + try { + await migrationAgent.finalizeMigration({from: accounts[0]}); + } catch(error) { + assert.notEqual(help.zeroAddress, await migrationAgent.qbxSourceToken()); + assert.notEqual(help.zeroAddress, await migrationAgent.qbxTargetToken()); + assert.notEqual(help.zeroAddress, await migrationAgent.tokenSupply()); + if (!help.isInvalidOpcodeEx(error)) throw error; + } + await token.migrate(web3.toWei(1), {from: accounts[1]}); + await migrationAgent.finalizeMigration({from: accounts[0]}); + assert.equal(help.zeroAddress, await migrationAgent.qbxSourceToken()); + assert.equal(help.zeroAddress, await migrationAgent.qbxTargetToken()); + new BigNumber(0).should.be.bignumber.equal(await migrationAgent.tokenSupply()); + }); + + it('can NOT finalize migration if non-owner', async () => { + await token.migrate(web3.toWei(1), {from: accounts[0]}); + await token.migrate(web3.toWei(1), {from: accounts[1]}); + try { + await migrationAgent.finalizeMigration({from: accounts[1]}); + } catch(error) { + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + it('force safetyInvariantCheck to fail', async () => { + await token.mint(accounts[0], web3.toWei(1), {from: await token.owner()}); + try { + await token.migrate(web3.toWei(1), {from: accounts[1]}); + } catch(error) { + (await token.totalMigrated()).should.be.bignumber.equal(0); + if (!help.isInvalidOpcodeEx(error)) throw error; + } + }); + + }); + }); diff --git a/test/WhitelistedCrowdsale.js b/test/WhitelistedCrowdsale.js index e146d46..db9cf67 100644 --- a/test/WhitelistedCrowdsale.js +++ b/test/WhitelistedCrowdsale.js @@ -1,4 +1,4 @@ -const WhitelistedCrowdsale = artifacts.require('../contracts/WhitelistedCrowdsaleImpl.sol'); +const WhitelistedCrowdsale = artifacts.require('../contracts/WhitelistedCrowdsale.sol'); const MintableToken = artifacts.require('MintableToken.sol'); const latestTime = require('./helpers/latestTime'); diff --git a/test/commands.js b/test/commands.js index 17cb34f..513f737 100644 --- a/test/commands.js +++ b/test/commands.js @@ -76,8 +76,10 @@ function getTokenBalance(state, account) { } async function runCheckRateCommand(command, state) { - let expectedRate = help.getCrowdsaleExpectedRate(state); - let rate = await state.crowdsaleContract.getRate(); + let expectedRate = state.crowdsaleData.rate; + let rate = await state.crowdsaleContract.rate(); + // let expectedRate = help.getCrowdsaleExpectedRate(state); + // let rate = await state.crowdsaleContract.getRate(); assert.equal(expectedRate, rate, 'expected rate is different! Expected: ' + expectedRate + ', actual: ' + rate + '. blocks: ' + web3.eth.blockTimestamp + ', start/initialRate/preferentialRate: ' + state.crowdsaleData.startTime + '/' + state.crowdsaleData.rate + '/' + state.crowdsaleData.preferentialRate); @@ -112,7 +114,8 @@ async function runBuyTokensCommand(command, state) { nextTime = latestTime(), account = gen.getAccount(command.account), beneficiaryAccount = gen.getAccount(command.beneficiary), - rate = help.getCrowdsaleExpectedRate(state, account, weiCost), + rate = state.crowdsaleData.rate, + // rate = help.getCrowdsaleExpectedRate(state, account, weiCost), tokens = new BigNumber(command.eth).mul(rate), hasZeroAddress = _.some([account, beneficiaryAccount], isZeroAddress), newBalance = getBalance(state, command.beneficiary).plus(weiCost); @@ -163,7 +166,7 @@ async function runBuyTokensCommand(command, state) { state = decreaseEthBalance(state, command.account, weiCost); state = decreaseEthBalance(state, command.account, help.txGasCost(tx)); } catch(e) { - help.debug(colors.red('FAILURE buying tokens, gasExceeded:', gasExceeded, ', minNotReached:', minNotReached, ', maxExceeded:', maxExceeded, ', frequencyExceeded:', frequencyExceeded, ', capExceeded: ', capExceeded)); + help.debug(colors.yellow('FAILURE buying tokens, gasExceeded:', gasExceeded, ', minNotReached:', minNotReached, ', maxExceeded:', maxExceeded, ', frequencyExceeded:', frequencyExceeded, ', capExceeded: ', capExceeded)); state = trackGasFromLastBlock(state, command.account); assertExpectedException(e, shouldThrow, hasZeroAddress, state, command); } @@ -178,7 +181,8 @@ async function runSendTransactionCommand(command, state) { nextTime = latestTime(), account = gen.getAccount(command.account), beneficiaryAccount = gen.getAccount(command.beneficiary), - rate = help.getCrowdsaleExpectedRate(state, account, weiCost), + rate = state.crowdsaleData.rate, + // rate = help.getCrowdsaleExpectedRate(state, account, weiCost), tokens = new BigNumber(command.eth).mul(rate), hasZeroAddress = _.some([account, beneficiaryAccount], isZeroAddress), newBalance = getBalance(state, command.beneficiary).plus(weiCost); @@ -229,7 +233,7 @@ async function runSendTransactionCommand(command, state) { state = decreaseEthBalance(state, command.account, weiCost); state = decreaseEthBalance(state, command.account, help.txGasCost(tx)); } catch(e) { - help.debug(colors.red('FAILURE buying tokens, gasExceeded:', gasExceeded, ', minNotReached:', minNotReached, ', maxExceeded:', maxExceeded, ', frequencyExceeded:', frequencyExceeded, ', capExceeded: ', capExceeded)); + help.debug(colors.yellow('FAILURE buying tokens, gasExceeded:', gasExceeded, ', minNotReached:', minNotReached, ', maxExceeded:', maxExceeded, ', frequencyExceeded:', frequencyExceeded, ', capExceeded: ', capExceeded)); state = trackGasFromLastBlock(state, command.account); assertExpectedException(e, shouldThrow, hasZeroAddress, state, command); @@ -239,13 +243,13 @@ async function runSendTransactionCommand(command, state) { async function runPresaleBuyTokensCommand(command, state) { let presale = state.presaleData, - { startTime, endTime} = presale, + { startTime, endTime, vestFromTime} = presale, weiCost = web3.toWei(command.eth, 'ether'), nextTime = latestTime(), account = gen.getAccount(command.account), beneficiaryAccount = gen.getAccount(command.beneficiary), accredited = state.accredited[command.account], - rate = accredited ? accredited.rate : null, + rate = state.presaleData.rate, tokens = accredited ? new BigNumber(command.eth).mul(rate) : null, hasZeroAddress = _.some([account, beneficiaryAccount], isZeroAddress), newBalance = getBalance(state, command.beneficiary).plus(weiCost); @@ -264,7 +268,6 @@ async function runPresaleBuyTokensCommand(command, state) { presale.cap == 0 || presale.maxGasPrice == 0 || presale.minBuyingRequestInterval == 0 || - presale.goal.gt(presale.cap) || state.presaleFinalized || hasZeroAddress || weiCost == 0 || @@ -278,8 +281,6 @@ async function runPresaleBuyTokensCommand(command, state) { const tx = await state.presaleContract.buyTokens(beneficiaryAccount, {value: weiCost, from: account, gasPrice: (command.gasPrice ? command.gasPrice : state.presaleData.maxGasPrice)}); assert.equal(false, shouldThrow, 'buyTokens should have thrown but it didn\'t'); - help.debug(colors.green('SUCCESS buying tokens, rate:', rate, 'eth:', command.eth, 'endBlocks:', presale.endTime, 'blockTimestamp:', nextTime)); - state.purchases = _.concat(state.purchases, {tokens: tokens, rate: rate, wei: weiCost, beneficiary: command.beneficiary, account: command.account} ); @@ -293,7 +294,7 @@ async function runPresaleBuyTokensCommand(command, state) { if (accredited.cliff > 0 && accredited.vesting >= accredited.cliff) { (await state.token.tokenGrantsCount(beneficiaryAccount)).should.be.bignumber.gt(new BigNumber(0)); new BigNumber(0).should.be.bignumber.equal(await state.token.transferableTokens(beneficiaryAccount, nextTime)); - const timeAfterVested = endTime + accredited.cliff + accredited.vesting; + const timeAfterVested = vestFromTime + accredited.cliff + accredited.vesting; const supply = new BigNumber(help.toAtto(state.tokenBalances[command.beneficiary])); supply.should.be.bignumber.equal(await state.token.transferableTokens(beneficiaryAccount, timeAfterVested)); } else { @@ -302,8 +303,10 @@ async function runPresaleBuyTokensCommand(command, state) { state = decreaseEthBalance(state, command.account, weiCost); state = decreaseEthBalance(state, command.account, help.txGasCost(tx)); + help.debug(colors.green('SUCCESS buying tokens, rate:', rate, 'eth:', command.eth, 'endBlocks:', presale.endTime, 'blockTimestamp:', nextTime)); + } catch(e) { - help.debug(colors.red('FAILURE buying tokens, gasExceeded:', gasExceeded, ', minNotReached:', minNotReached, ', maxExceeded:', maxExceeded, ', frequencyExceeded:', frequencyExceeded, ', capExceeded: ', capExceeded)); + help.debug(colors.yellow('FAILURE buying tokens, gasExceeded:', gasExceeded, ', minNotReached:', minNotReached, ', maxExceeded:', maxExceeded, ', frequencyExceeded:', frequencyExceeded, ', capExceeded: ', capExceeded)); state = trackGasFromLastBlock(state, command.account); assertExpectedException(e, shouldThrow, hasZeroAddress, state, command); } @@ -312,12 +315,12 @@ async function runPresaleBuyTokensCommand(command, state) { async function runPresaleSendTransactionCommand(command, state) { let presale = state.presaleData, - { startTime, endTime} = presale, + { startTime, endTime, vestFromTime} = presale, weiCost = web3.toWei(command.eth, 'ether'), nextTime = latestTime(), account = gen.getAccount(command.account), accredited = state.accredited[command.account], - rate = accredited ? accredited.rate : null, + rate = state.presaleData.rate, tokens = accredited ? new BigNumber(command.eth).mul(rate) : null, hasZeroAddress = _.some([account], isZeroAddress), newBalance = getBalance(state, command.account).plus(weiCost); @@ -332,11 +335,9 @@ async function runPresaleSendTransactionCommand(command, state) { let shouldThrow = (!inTGE) || !accredited || state.presalePaused || - presale.goal == 0 || presale.cap == 0 || presale.maxGasPrice == 0 || presale.minBuyingRequestInterval == 0 || - presale.goal.gt(presale.cap) || state.presaleFinalized || hasZeroAddress || weiCost == 0 || @@ -366,7 +367,7 @@ async function runPresaleSendTransactionCommand(command, state) { }); (await state.token.tokenGrantsCount(account)).should.be.bignumber.equal(new BigNumber(purchases.length)); new BigNumber(0).should.be.bignumber.equal(await state.token.transferableTokens(account, nextTime)); - const timeAfterVested = endTime + accredited.cliff + accredited.vesting; + const timeAfterVested = vestFromTime + accredited.cliff + accredited.vesting; const supply = new BigNumber(help.toAtto(state.tokenBalances[command.account])); supply.should.be.bignumber.equal(await state.token.transferableTokens(account, timeAfterVested)); @@ -381,7 +382,63 @@ async function runPresaleSendTransactionCommand(command, state) { } catch(e) { state = trackGasFromLastBlock(state, command.account); assertExpectedException(e, shouldThrow, hasZeroAddress, state, command); - help.debug(colors.red('FAILURE buying tokens, gasExceeded:', gasExceeded, ', minNotReached:', minNotReached, ', maxExceeded:', maxExceeded, ', frequencyExceeded:', frequencyExceeded, ', capExceeded: ', capExceeded)); + help.debug(colors.yellow('FAILURE buying tokens, gasExceeded:', gasExceeded, ', minNotReached:', minNotReached, ', maxExceeded:', maxExceeded, ', frequencyExceeded:', frequencyExceeded, ', capExceeded: ', capExceeded)); + } + return state; +} + +async function runDistributeTokensCommand(command, state) { + let presale = state.presaleData, + { vestFromTime, endTime} = presale, + nextTime = latestTime(), + account = gen.getAccount(command.fromAccount), + beneficiary = gen.getAccount(command.beneficiary), + amount = command.amount, + cliff = command.cliff, + vesting = command.vesting, + revokable = command.revokable, + burnsOnRevoke = command.burnsOnRevoke, + hasZeroAddress = _.some([account, beneficiary], isZeroAddress); + + let afterFundraising = (nextTime >= endTime || state.weiRaised >= presale.cap), + capExceeded = state.tokensDistributed.plus(new BigNumber(help.toAtto(command.amount))).gt(presale.distributionCap); + + let shouldThrow = !afterFundraising || + state.presalePaused || + amount == 0 || + vesting < cliff || + state.presaleFinalized || + hasZeroAddress || + capExceeded || + command.fromAccount != state.owner; + + try { + await state.presaleContract.distributeTokens(beneficiary, help.toAtto(amount), cliff, vesting, revokable, burnsOnRevoke, {from: account, gasPrice: (command.gasPrice ? command.gasPrice : state.presaleData.maxGasPrice)}); + + assert.equal(false, shouldThrow, 'distribute tokens should have thrown but it didn\'t'); + + // state.purchases = _.concat(state.purchases, + // {tokens: tokens, rate: rate, wei: weiCost, beneficiary: command.beneficiary, account: command.account} + // ); + + state.tokensDistributed = state.tokensDistributed.plus(new BigNumber(help.toAtto(amount))); + state.presaleSupply = state.presaleSupply.plus(new BigNumber(help.toAtto(amount))); + state.tokenBalances[command.beneficiary] = getTokenBalance(state, command.beneficiary).plus(amount); + + if (cliff > 0 && vesting >= cliff) { + (await state.token.tokenGrantsCount(beneficiary)).should.be.bignumber.gt(new BigNumber(0)); + new BigNumber(0).should.be.bignumber.equal(await state.token.transferableTokens(beneficiary, nextTime)); + const timeAfterVested = vestFromTime + cliff + vesting; + const supply = new BigNumber(help.toAtto(state.tokenBalances[command.beneficiary])); + supply.should.be.bignumber.equal(await state.token.transferableTokens(beneficiary, timeAfterVested)); + } else { + new BigNumber(0).should.be.bignumber.equal(await state.token.tokenGrantsCount(beneficiary)); + } + help.debug(colors.green('SUCCESS distributing tokens')); + + } catch(e) { + help.debug(colors.yellow('FAILURE distributing tokens, capExceeded: ', capExceeded)); + assertExpectedException(e, shouldThrow, hasZeroAddress, state, command); } return state; } @@ -389,7 +446,6 @@ async function runPresaleSendTransactionCommand(command, state) { async function runAddAccreditedCommand(command, state) { let account = gen.getAccount(command.fromAccount), investor = gen.getAccount(command.investor), - rate = command.rate, cliff = command.cliff, vesting = command.vesting, revokable = command.revokable, @@ -400,21 +456,20 @@ async function runAddAccreditedCommand(command, state) { let hasZeroAddress = _.some([account, investor], isZeroAddress); let shouldThrow = hasZeroAddress || - rate == 0 || vesting < cliff || minInvest == 0 || maxCumulativeInvest == 0 || command.fromAccount != state.owner; try { - await state.presaleContract.addAccreditedInvestor(investor, rate, cliff, vesting, revokable, burnsOnTokens, help.toWei(minInvest), help.toWei(maxCumulativeInvest), {from: account}); + await state.presaleContract.addAccreditedInvestor(investor, cliff, vesting, revokable, burnsOnTokens, help.toWei(minInvest), help.toWei(maxCumulativeInvest), {from: account}); help.debug(colors.green('SUCCESS adding accredited investor')); assert.equal(false, shouldThrow, 'add to whitelist should have thrown but it did not'); - state.accredited[command.investor] = {rate: rate, cliff: cliff, vesting: vesting, revokable, burnsOnTokens, minInvest: help.toWei(minInvest), maxCumulativeInvest: help.toWei(maxCumulativeInvest)}; + state.accredited[command.investor] = {cliff: cliff, vesting: vesting, revokable, burnsOnTokens, minInvest: help.toWei(minInvest), maxCumulativeInvest: help.toWei(maxCumulativeInvest)}; } catch(e) { assertExpectedException(e, shouldThrow, hasZeroAddress, state, command); - help.debug(colors.red('FAILURE adding accredited investor')); + help.debug(colors.yellow('FAILURE adding accredited investor')); } return state; } @@ -436,7 +491,7 @@ async function runRemoveAccreditedCommand(command, state) { delete state.accredited[command.investor]; } catch(e) { assertExpectedException(e, shouldThrow, hasZeroAddress, state, command); - help.debug(colors.red('FAILURE removing accredited investor')); + help.debug(colors.yellow('FAILURE removing accredited investor')); } return state; } @@ -515,7 +570,7 @@ async function runPauseTokenCommand(command, state) { state.tokenPaused = command.pause; state = decreaseEthBalance(state, command.fromAccount, help.txGasCost(tx)); } catch(e) { - help.debug(colors.red('FAILURE pausing token, previous state:', state.tokenPaused, 'new state:', command.pause)); + help.debug(colors.yellow('FAILURE pausing token, previous state:', state.tokenPaused, 'new state:', command.pause)); state = trackGasFromLastBlock(state, command.fromAccount); assertExpectedException(e, shouldThrow, hasZeroAddress, state, command); } @@ -531,7 +586,8 @@ async function runFinalizeCrowdsaleCommand(command, state) { let shouldThrow = state.crowdsaleFinalized || state.crowdsalePaused || hasZeroAddress || - (nextTimestamp < state.crowdsaleData.endTime && !capReached); + (nextTimestamp < state.crowdsaleData.endTime && !capReached) || + command.fromAccount != state.owner; try { let goalReached = state.weiRaised.eq(state.crowdsaleData.goal), @@ -566,13 +622,14 @@ async function runFinalizeCrowdsaleCommand(command, state) { help.debug(colors.green('SUCCESS: finishing crowdsale on block', nextTimestamp, ', from address:', gen.getAccount(command.fromAccount), ', funded:', goalReached, 'gas used: ', tx.receipt.gasUsed)); } catch(e) { - help.debug(colors.red('FAILURE finishing crowdsale, on block', nextTimestamp, ', from address:', gen.getAccount(command.fromAccount), ', funded:')); + help.debug(colors.yellow('FAILURE finishing crowdsale, on block', nextTimestamp, ', from address:', gen.getAccount(command.fromAccount), ', funded: ', state.goalReached)); assertExpectedException(e, shouldThrow, hasZeroAddress, state, command); } return state; } async function runFinalizePresaleCommand(command, state) { + let nextTimestamp = latestTime(), account = gen.getAccount(command.fromAccount), hasZeroAddress = isZeroAddress(account), @@ -581,11 +638,11 @@ async function runFinalizePresaleCommand(command, state) { let shouldThrow = state.presaleFinalized || state.presalePaused || hasZeroAddress || - (nextTimestamp < state.presaleData.endTime && !capReached); + (nextTimestamp < state.presaleData.endTime && !capReached) || + command.fromAccount != state.owner; try { - let goalReached = state.weiRaised.eq(state.presaleData.goal), - tokenOwnerBeforeFinalize = await state.token.owner(), + let tokenOwnerBeforeFinalize = await state.token.owner(), tx = await state.presaleContract.finalize({from: account}); if (!help.inCoverage()) { // gas cannot be measuyellow correctly when running coverage assert(tx.receipt.gasUsed < 6700000, @@ -593,27 +650,23 @@ async function runFinalizePresaleCommand(command, state) { } state = decreaseEthBalance(state, command.fromAccount, help.txGasCost(tx)); let tokenOwnerAfterFinalize = await state.token.owner(); - if (goalReached) { - state = increaseEthBalance(state, state.wallet, state.weiRaised); //TODO: check this call - //check token ownership change - assert.notEqual(tokenOwnerBeforeFinalize, tokenOwnerAfterFinalize); - assert.equal(gen.getAccount(state.wallet), tokenOwnerAfterFinalize); - let totalSupply = new BigNumber(state.presaleData.FOUNDATION_SUPPLY); + state = increaseEthBalance(state, state.wallet, state.weiRaised); //TODO: check this call + //check token ownership change + assert.notEqual(tokenOwnerBeforeFinalize, tokenOwnerAfterFinalize); + assert.equal(gen.getAccount(state.wallet), tokenOwnerAfterFinalize); + + let totalTokens = state.tokensDistributed.plus(state.tokensSold); + new BigNumber(await state.token.totalSupply()).should.be.bignumber.equal(totalTokens); - totalSupply.should.be.bignumber.equal( - await state.token.totalSupply() - ); - } assert.equal(false, shouldThrow, 'finalizeCrowdsale should have thrown but it did not'); state.presaleFinalized = true; - state.goalReached = goalReached; state.tokenPaused = false; state.tokenOwner = state.wallet; //TODO: change state.owner or token owner?? - help.debug(colors.green('SUCCESS: finishing presale on block', nextTimestamp, ', from address:', gen.getAccount(command.fromAccount), ', funded:', goalReached, 'gas used: ', tx.receipt.gasUsed)); + help.debug(colors.green('SUCCESS: finishing presale on block', nextTimestamp, ', from address:', gen.getAccount(command.fromAccount), 'gas used: ', tx.receipt.gasUsed)); } catch(e) { - help.debug(colors.red('FAILURE finishing presale, on block', nextTimestamp, ', from address:', gen.getAccount(command.fromAccount), ', funded:')); + help.debug(colors.yellow('FAILURE finishing presale, on block', nextTimestamp, ', from address:', gen.getAccount(command.fromAccount), ', funded:')); assertExpectedException(e, shouldThrow, hasZeroAddress, state, command); } return state; @@ -672,7 +725,7 @@ async function runBurnTokensCommand(command, state) { state = decreaseEthBalance(state, command.account, help.txGasCost(tx)); } catch(e) { - help.debug(colors.red('FAILURE burning tokens, balance:', balance, 'tokens: ', command.tokens)); + help.debug(colors.yellow('FAILURE burning tokens, balance:', balance, 'tokens: ', command.tokens)); state = trackGasFromLastBlock(state, command.account); assertExpectedException(e, shouldThrow, hasZeroAddress, state, command); } @@ -685,7 +738,6 @@ async function runFundCrowdsaleBelowCap(command, state) { // unpause the crowdsale if needed if (state.crowdsalePaused) { state = await runPauseCrowdsaleCommand({pause: false, fromAccount: state.owner}, state); - } let goal = state.crowdsaleData.goal, @@ -713,8 +765,8 @@ async function runFundCrowdsaleBelowCap(command, state) { } state = await runFinalizeCrowdsaleCommand({fromAccount: from}, state); // verify that the crowdsale is finalized and funded - assert.equal(true, state.crowdsaleFinalized); - assert.equal(true, state.goalReached); + assert.equal(from == state.owner, state.crowdsaleFinalized); + assert.equal(from == state.owner, state.goalReached); } } @@ -739,6 +791,7 @@ const crowdsaleCommands = { const presaleCommands = { presaleBuyTokens: {gen: gen.presaleBuyTokensCommandGen, run: runPresaleBuyTokensCommand}, presaleSendTransaction: {gen: gen.presaleSendTransactionCommandGen, run: runPresaleSendTransactionCommand}, + distributeTokens: {gen: gen.distributeTokensCommandGen, run: runDistributeTokensCommand}, pausePresale: {gen: gen.pausePresaleCommandGen, run: runPausePresaleCommand}, addAccredited: { gen: gen.addAccreditedCommandGen, run: runAddAccreditedCommand}, removeAccredited: { gen: gen.removeAccreditedCommandGen, run: runRemoveAccreditedCommand}, diff --git a/test/generators.js b/test/generators.js index dd72a1d..4577004 100644 --- a/test/generators.js +++ b/test/generators.js @@ -26,8 +26,9 @@ module.exports = { presaleGen: jsc.record({ maxGasPrice: jsc.integer(0, 1000000000), minBuyingRequestInterval: jsc.integer(0, 10000), - goal: jsc.integer(0, 100000), + rate: jsc.integer(0, 2000), cap: jsc.integer(0, 100000), + distributionCap: jsc.integer(0, 10000000), foundationWallet: accountGen, owner: accountGen }), @@ -94,6 +95,17 @@ module.exports = { eth: jsc.integer(0, 1000000000) }), + distributeTokensCommandGen: jsc.record({ + type: jsc.constant('distributeTokens'), + fromAccount: accountGen, + beneficiary: accountGen, + amount: jsc.integer(0, 10000000), + cliff: jsc.integer(0, 20000), + vesting: jsc.integer(0, 20000), + revokable: jsc.bool, + burnsOnRevoke: jsc.bool, + }), + pauseCrowdsaleCommandGen: jsc.record({ type: jsc.constant('pauseCrowdsale'), pause: jsc.bool,