From c05918c3cc10d9394fd5fb95deb04036204ac896 Mon Sep 17 00:00:00 2001 From: Alejo Salles Date: Tue, 20 Feb 2018 18:05:53 -0300 Subject: [PATCH] Crowdsale refactor and add new models (#744) * Basic idea * Fine tuning idea * Add comments / tidy up Crowdsale base class * fixed TimedCrowdsale constructor * added simple crowdsale test * added HODL directory under home to store unused contracts. ugly hack to solve Crowdsale selection in tests, better way? * Capped no longer inherits from Timed, added capReached() method (replacing hasEnded()) * added SafeMath in TimedCrowdsale for safety, CHECK whether it is inherited from Crowdsale * several fixes related to separating Capped from Timed. functions renamed, mocks changed. Capped tests passing * added TimedCrowdsaleImpl.sol, TimedCrowdsale tests, passed * added Whitelisted implementation and test, passed. * removed unnecessary super constructor call in WhitelistedCrowdsale, removed unused dependencies in tests * renamed UserCappedCrowdsale to IndividuallyCappedCrowdsale, implemented IndividuallyCappedCrowdsaleImpl.sol and corresponding tests, passed. * homogeneized use of using SafeMath for uint256 across validation crowdsales. checked that it IS indeed inherited, but leaving it there as per Frans suggestion. * adding questions.md where I track questions, bugs and progress * modified VariablePriceCrowdsale, added Impl. * finished VariablePrice, fixed sign, added test, passing. * changed VariablePrice to IncreasingPrice, added corresponding require() * MintedCrowdsale done, mock implemented, test passing * PremintedCrowdsale done, mocks, tests passing * checked FinalizableCrowdsale * PostDeliveryCrowdsale done, mock, tests passing. * RefundableCrowdsale done. Detached Vault. modified mock and test, passing * renamed crowdsale-refactor to crowdsale in contracts and test * deleted HODL old contracts * polished variable names in tests * fixed typos and removed comments in tests * Renamed 'crowdsale-refactor' to 'crowdsale' in all imports * Fix minor param naming issues in Crowdsale functions and added documentation to Crowdsale.sol * Added documentation to Crowdsale extensions * removed residual comments and progress tracking files * added docs for validation crowdsales * Made user promises in PostDeliveryCrowdsale public so that users can query their promised token balance. * added docs for distribution crowdsales * renamed PremintedCrowdsale to AllowanceCrowdsale * added allowance check function and corresponding test. fixed filename in AllowanceCrowdsale mock. * spilt Crowdsale _postValidatePurchase in _postValidatePurchase and _updatePurchasingState. changed IndividuallyCappedCrowdsale accordingly. * polished tests for linter, salve Travis * polished IncreasingPriceCrowdsale.sol for linter. * renamed and polished for linter WhitelistedCrowdsale test. * fixed indentation in IncreasingPriceCrowdsaleImpl.sol for linter * fixed ignoring token.mint return value in MintedCrowdsale.sol * expanded docs throughout, fixed minor issues * extended test coverage for IndividuallyCappedCrowdsale * Extended WhitelistedCrwodsale test coverage * roll back decoupling of RefundVault in RefundableCrowdsale * moved cap exceedance checks in Capped and IndividuallyCapped crowdsales to _preValidatePurchase to save gas * revert name change, IndividuallyCapped to UserCapped * extended docs. * added crowd whitelisting with tests * added group capping, plus tests * added modifiers in TimedCrowdsale and WhitelistedCrowdsale * polished tests for linter * moved check of whitelisted to modifier, mainly for testing coverage * fixed minor ordering/polishingafter review * modified TimedCrowdsale modifier/constructor ordering * unchanged truffle-config.js * changed indentation of visibility modifier in mocks * changed naming of modifier and function to use Open/Closed for TimedCrowdsale * changed ordering of constructor calls in SampleCrowdsale * changed startTime and endTime to openingTime and closingTime throughout * fixed exceeding line lenght for linter * renamed _emitTokens to _deliverTokens * renamed addCrowdToWhitelist to addManyToWhitelist * renamed UserCappedCrowdsale to IndividuallyCappedCrowdsale --- contracts/crowdsale/CappedCrowdsale.sol | 35 ---- contracts/crowdsale/Crowdsale.sol | 151 ++++++++++++------ .../FinalizableCrowdsale.sol | 11 +- .../distribution/PostDeliveryCrowdsale.sol | 35 ++++ .../RefundableCrowdsale.sol | 28 +++- .../{ => distribution/utils}/RefundVault.sol | 13 +- .../crowdsale/emission/AllowanceCrowdsale.sol | 42 +++++ .../crowdsale/emission/MintedCrowdsale.sol | 22 +++ .../price/IncreasingPriceCrowdsale.sol | 52 ++++++ .../crowdsale/validation/CappedCrowdsale.sol | 43 +++++ .../IndividuallyCappedCrowdsale.sol | 76 +++++++++ .../crowdsale/validation/TimedCrowdsale.sol | 55 +++++++ .../validation/WhitelistedCrowdsale.sol | 58 +++++++ contracts/examples/SampleCrowdsale.sol | 13 +- contracts/mocks/AllowanceCrowdsaleImpl.sol | 21 +++ contracts/mocks/CappedCrowdsaleImpl.sol | 15 +- contracts/mocks/FinalizableCrowdsaleImpl.sol | 14 +- .../mocks/IncreasingPriceCrowdsaleImpl.sol | 24 +++ .../mocks/IndividuallyCappedCrowdsaleImpl.sol | 19 +++ contracts/mocks/MintedCrowdsaleImpl.sol | 19 +++ contracts/mocks/PostDeliveryCrowdsaleImpl.sol | 22 +++ contracts/mocks/RefundableCrowdsaleImpl.sol | 19 +-- contracts/mocks/TimedCrowdsaleImpl.sol | 21 +++ contracts/mocks/WhitelistedCrowdsaleImpl.sol | 19 +++ test/crowdsale/AllowanceCrowdsale.test.js | 68 ++++++++ test/crowdsale/CappedCrowdsale.test.js | 57 +++---- test/crowdsale/Crowdsale.test.js | 76 +-------- test/crowdsale/FinalizableCrowdsale.test.js | 16 +- .../IncreasingPriceCrowdsale.test.js | 92 +++++++++++ .../IndividuallyCappedCrowdsale.test.js | 107 +++++++++++++ test/crowdsale/MintedCrowdsale.test.js | 61 +++++++ test/crowdsale/PostDeliveryCrowdsale.test.js | 67 ++++++++ test/crowdsale/RefundableCrowdsale.test.js | 47 +++--- test/crowdsale/TimedCrowdsale.test.js | 62 +++++++ test/crowdsale/WhitelistedCrowdsale.test.js | 93 +++++++++++ test/examples/SampleCrowdsale.test.js | 33 ++-- 36 files changed, 1321 insertions(+), 285 deletions(-) delete mode 100644 contracts/crowdsale/CappedCrowdsale.sol rename contracts/crowdsale/{ => distribution}/FinalizableCrowdsale.sol (79%) create mode 100644 contracts/crowdsale/distribution/PostDeliveryCrowdsale.sol rename contracts/crowdsale/{ => distribution}/RefundableCrowdsale.sol (66%) rename contracts/crowdsale/{ => distribution/utils}/RefundVault.sol (86%) create mode 100644 contracts/crowdsale/emission/AllowanceCrowdsale.sol create mode 100644 contracts/crowdsale/emission/MintedCrowdsale.sol create mode 100644 contracts/crowdsale/price/IncreasingPriceCrowdsale.sol create mode 100644 contracts/crowdsale/validation/CappedCrowdsale.sol create mode 100644 contracts/crowdsale/validation/IndividuallyCappedCrowdsale.sol create mode 100644 contracts/crowdsale/validation/TimedCrowdsale.sol create mode 100644 contracts/crowdsale/validation/WhitelistedCrowdsale.sol create mode 100644 contracts/mocks/AllowanceCrowdsaleImpl.sol create mode 100644 contracts/mocks/IncreasingPriceCrowdsaleImpl.sol create mode 100644 contracts/mocks/IndividuallyCappedCrowdsaleImpl.sol create mode 100644 contracts/mocks/MintedCrowdsaleImpl.sol create mode 100644 contracts/mocks/PostDeliveryCrowdsaleImpl.sol create mode 100644 contracts/mocks/TimedCrowdsaleImpl.sol create mode 100644 contracts/mocks/WhitelistedCrowdsaleImpl.sol create mode 100644 test/crowdsale/AllowanceCrowdsale.test.js create mode 100644 test/crowdsale/IncreasingPriceCrowdsale.test.js create mode 100644 test/crowdsale/IndividuallyCappedCrowdsale.test.js create mode 100644 test/crowdsale/MintedCrowdsale.test.js create mode 100644 test/crowdsale/PostDeliveryCrowdsale.test.js create mode 100644 test/crowdsale/TimedCrowdsale.test.js create mode 100644 test/crowdsale/WhitelistedCrowdsale.test.js diff --git a/contracts/crowdsale/CappedCrowdsale.sol b/contracts/crowdsale/CappedCrowdsale.sol deleted file mode 100644 index 3fe7b9f32..000000000 --- a/contracts/crowdsale/CappedCrowdsale.sol +++ /dev/null @@ -1,35 +0,0 @@ -pragma solidity ^0.4.18; - -import "../math/SafeMath.sol"; -import "./Crowdsale.sol"; - - -/** - * @title CappedCrowdsale - * @dev Extension of Crowdsale with a max amount of funds raised - */ -contract CappedCrowdsale is Crowdsale { - using SafeMath for uint256; - - uint256 public cap; - - function CappedCrowdsale(uint256 _cap) public { - require(_cap > 0); - cap = _cap; - } - - // overriding Crowdsale#hasEnded to add cap logic - // @return true if crowdsale event has ended - function hasEnded() public view returns (bool) { - bool capReached = weiRaised >= cap; - return capReached || super.hasEnded(); - } - - // overriding Crowdsale#validPurchase to add extra cap logic - // @return true if investors can buy at the moment - function validPurchase() internal view returns (bool) { - bool withinCap = weiRaised.add(msg.value) <= cap; - return withinCap && super.validPurchase(); - } - -} diff --git a/contracts/crowdsale/Crowdsale.sol b/contracts/crowdsale/Crowdsale.sol index 242b67bf9..ea222999e 100644 --- a/contracts/crowdsale/Crowdsale.sol +++ b/contracts/crowdsale/Crowdsale.sol @@ -1,40 +1,38 @@ pragma solidity ^0.4.18; -import "../token/ERC20/MintableToken.sol"; +import "../token/ERC20/ERC20.sol"; import "../math/SafeMath.sol"; - /** * @title Crowdsale - * @dev Crowdsale is a base contract for managing a token crowdsale. - * Crowdsales have a start and end timestamps, where investors can make - * token purchases and the crowdsale will assign them tokens based - * on a token per ETH rate. Funds collected are forwarded to a wallet - * as they arrive. The contract requires a MintableToken that will be - * minted as contributions arrive, note that the crowdsale contract - * must be owner of the token in order to be able to mint it. + * @dev Crowdsale is a base contract for managing a token crowdsale, + * allowing investors to purchase tokens with ether. This contract implements + * such functionality in its most fundamental form and can be extended to provide additional + * functionality and/or custom behavior. + * The external interface represents the basic interface for purchasing tokens, and conform + * the base architecture for crowdsales. They are *not* intended to be modified / overriden. + * The internal interface conforms the extensible and modifiable surface of crowdsales. Override + * the methods to add functionality. Consider using 'super' where appropiate to concatenate + * behavior. */ + contract Crowdsale { using SafeMath for uint256; // The token being sold - MintableToken public token; + ERC20 public token; - // start and end timestamps where investments are allowed (both inclusive) - uint256 public startTime; - uint256 public endTime; - - // address where funds are collected + // Address where funds are collected address public wallet; - // how many token units a buyer gets per wei + // How many token units a buyer gets per wei uint256 public rate; - // amount of raised money in wei + // Amount of wei raised uint256 public weiRaised; /** - * event for token purchase logging + * Event for token purchase logging * @param purchaser who paid for the tokens * @param beneficiary who got the tokens * @param value weis paid for purchase @@ -42,66 +40,119 @@ contract Crowdsale { */ event TokenPurchase(address indexed purchaser, address indexed beneficiary, uint256 value, uint256 amount); - - function Crowdsale(uint256 _startTime, uint256 _endTime, uint256 _rate, address _wallet, MintableToken _token) public { - require(_startTime >= now); - require(_endTime >= _startTime); + /** + * @param _rate Number of token units a buyer gets per wei + * @param _wallet Address where collected funds will be forwarded to + * @param _token Address of the token being sold + */ + function Crowdsale(uint256 _rate, address _wallet, ERC20 _token) public { require(_rate > 0); require(_wallet != address(0)); require(_token != address(0)); - startTime = _startTime; - endTime = _endTime; rate = _rate; wallet = _wallet; token = _token; } - // fallback function can be used to buy tokens + // ----------------------------------------- + // Crowdsale external interface + // ----------------------------------------- + + /** + * @dev fallback function ***DO NOT OVERRIDE*** + */ function () external payable { buyTokens(msg.sender); } - // low level token purchase function - function buyTokens(address beneficiary) public payable { - require(beneficiary != address(0)); - require(validPurchase()); + /** + * @dev low level token purchase ***DO NOT OVERRIDE*** + * @param _beneficiary Address performing the token purchase + */ + function buyTokens(address _beneficiary) public payable { uint256 weiAmount = msg.value; + _preValidatePurchase(_beneficiary, weiAmount); // calculate token amount to be created - uint256 tokens = getTokenAmount(weiAmount); + uint256 tokens = _getTokenAmount(weiAmount); // update state weiRaised = weiRaised.add(weiAmount); - token.mint(beneficiary, tokens); - TokenPurchase(msg.sender, beneficiary, weiAmount, tokens); + _processPurchase(_beneficiary, tokens); + TokenPurchase(msg.sender, _beneficiary, weiAmount, tokens); - forwardFunds(); + _updatePurchasingState(_beneficiary, weiAmount); + + _forwardFunds(); + _postValidatePurchase(_beneficiary, weiAmount); } - // @return true if crowdsale event has ended - function hasEnded() public view returns (bool) { - return now > endTime; + // ----------------------------------------- + // Internal interface (extensible) + // ----------------------------------------- + + /** + * @dev Validation of an incoming purchase. Use require statemens to revert state when conditions are not met. Use super to concatenate validations. + * @param _beneficiary Address performing the token purchase + * @param _weiAmount Value in wei involved in the purchase + */ + function _preValidatePurchase(address _beneficiary, uint256 _weiAmount) internal { + require(_beneficiary != address(0)); + require(_weiAmount != 0); } - // Override this method to have a way to add business logic to your crowdsale when buying - function getTokenAmount(uint256 weiAmount) internal view returns(uint256) { - return weiAmount.mul(rate); + /** + * @dev Validation of an executed purchase. Observe state and use revert statements to undo rollback when valid conditions are not met. + * @param _beneficiary Address performing the token purchase + * @param _weiAmount Value in wei involved in the purchase + */ + function _postValidatePurchase(address _beneficiary, uint256 _weiAmount) internal { + // optional override } - // send ether to the fund collection wallet - // override to create custom fund forwarding mechanisms - function forwardFunds() internal { + /** + * @dev Source of tokens. Override this method to modify the way in which the crowdsale ultimately gets and sends its tokens. + * @param _beneficiary Address performing the token purchase + * @param _tokenAmount Number of tokens to be emitted + */ + function _deliverTokens(address _beneficiary, uint256 _tokenAmount) internal { + token.transfer(_beneficiary, _tokenAmount); + } + + /** + * @dev Executed when a purchase has been validated and is ready to be executed. Not necessarily emits/sends tokens. + * @param _beneficiary Address receiving the tokens + * @param _tokenAmount Number of tokens to be purchased + */ + function _processPurchase(address _beneficiary, uint256 _tokenAmount) internal { + _deliverTokens(_beneficiary, _tokenAmount); + } + + /** + * @dev Override for extensions that require an internal state to check for validity (current user contributions, etc.) + * @param _beneficiary Address receiving the tokens + * @param _weiAmount Value in wei involved in the purchase + */ + function _updatePurchasingState(address _beneficiary, uint256 _weiAmount) internal { + // optional override + } + + /** + * @dev Override to extend the way in which ether is converted to tokens. + * @param _weiAmount Value in wei to be converted into tokens + * @return Number of tokens that can be purchased with the specified _weiAmount + */ + function _getTokenAmount(uint256 _weiAmount) internal view returns (uint256) { + return _weiAmount.mul(rate); + } + + /** + * @dev Determines how ETH is stored/forwarded on purchases. + */ + function _forwardFunds() internal { wallet.transfer(msg.value); } - - // @return true if the transaction can buy tokens - function validPurchase() internal view returns (bool) { - bool withinPeriod = now >= startTime && now <= endTime; - bool nonZeroPurchase = msg.value != 0; - return withinPeriod && nonZeroPurchase; - } - } diff --git a/contracts/crowdsale/FinalizableCrowdsale.sol b/contracts/crowdsale/distribution/FinalizableCrowdsale.sol similarity index 79% rename from contracts/crowdsale/FinalizableCrowdsale.sol rename to contracts/crowdsale/distribution/FinalizableCrowdsale.sol index f75e9de39..472e2796a 100644 --- a/contracts/crowdsale/FinalizableCrowdsale.sol +++ b/contracts/crowdsale/distribution/FinalizableCrowdsale.sol @@ -1,16 +1,15 @@ pragma solidity ^0.4.18; -import "../math/SafeMath.sol"; -import "../ownership/Ownable.sol"; -import "./Crowdsale.sol"; - +import "../../math/SafeMath.sol"; +import "../../ownership/Ownable.sol"; +import "../validation/TimedCrowdsale.sol"; /** * @title FinalizableCrowdsale * @dev Extension of Crowdsale where an owner can do extra work * after finishing. */ -contract FinalizableCrowdsale is Crowdsale, Ownable { +contract FinalizableCrowdsale is TimedCrowdsale, Ownable { using SafeMath for uint256; bool public isFinalized = false; @@ -23,7 +22,7 @@ contract FinalizableCrowdsale is Crowdsale, Ownable { */ function finalize() onlyOwner public { require(!isFinalized); - require(hasEnded()); + require(hasClosed()); finalization(); Finalized(); diff --git a/contracts/crowdsale/distribution/PostDeliveryCrowdsale.sol b/contracts/crowdsale/distribution/PostDeliveryCrowdsale.sol new file mode 100644 index 000000000..3f45ad063 --- /dev/null +++ b/contracts/crowdsale/distribution/PostDeliveryCrowdsale.sol @@ -0,0 +1,35 @@ +pragma solidity ^0.4.18; + +import "../validation/TimedCrowdsale.sol"; +import "../../token/ERC20/ERC20.sol"; +import "../../math/SafeMath.sol"; + +/** + * @title PostDeliveryCrowdsale + * @dev Crowdsale that locks tokens from withdrawal until it ends. + */ +contract PostDeliveryCrowdsale is TimedCrowdsale { + using SafeMath for uint256; + + mapping(address => uint256) public balances; + + /** + * @dev Overrides parent by storing balances instead of issuing tokens right away. + * @param _beneficiary Token purchaser + * @param _tokenAmount Amount of tokens purchased + */ + function _processPurchase(address _beneficiary, uint256 _tokenAmount) internal { + balances[_beneficiary] = balances[_beneficiary].add(_tokenAmount); + } + + /** + * @dev Withdraw tokens only after crowdsale ends. + */ + function withdrawTokens() public { + require(hasClosed()); + uint256 amount = balances[msg.sender]; + require(amount > 0); + balances[msg.sender] = 0; + _deliverTokens(msg.sender, amount); + } +} diff --git a/contracts/crowdsale/RefundableCrowdsale.sol b/contracts/crowdsale/distribution/RefundableCrowdsale.sol similarity index 66% rename from contracts/crowdsale/RefundableCrowdsale.sol rename to contracts/crowdsale/distribution/RefundableCrowdsale.sol index 12f523b23..de1e08d13 100644 --- a/contracts/crowdsale/RefundableCrowdsale.sol +++ b/contracts/crowdsale/distribution/RefundableCrowdsale.sol @@ -1,9 +1,9 @@ pragma solidity ^0.4.18; -import "../math/SafeMath.sol"; +import "../../math/SafeMath.sol"; import "./FinalizableCrowdsale.sol"; -import "./RefundVault.sol"; +import "./utils/RefundVault.sol"; /** @@ -21,13 +21,19 @@ contract RefundableCrowdsale is FinalizableCrowdsale { // refund vault used to hold funds while crowdsale is running RefundVault public vault; + /** + * @dev Constructor, creates RefundVault. + * @param _goal Funding goal + */ function RefundableCrowdsale(uint256 _goal) public { require(_goal > 0); vault = new RefundVault(wallet); goal = _goal; } - // if crowdsale is unsuccessful, investors can claim refunds here + /** + * @dev Investors can claim refunds here if crowdsale is unsuccessful + */ function claimRefund() public { require(isFinalized); require(!goalReached()); @@ -35,11 +41,17 @@ contract RefundableCrowdsale is FinalizableCrowdsale { vault.refund(msg.sender); } + /** + * @dev Checks whether funding goal was reached. + * @return Whether funding goal was reached + */ function goalReached() public view returns (bool) { return weiRaised >= goal; } - // vault finalization task, called when owner calls finalize() + /** + * @dev vault finalization task, called when owner calls finalize() + */ function finalization() internal { if (goalReached()) { vault.close(); @@ -50,10 +62,10 @@ contract RefundableCrowdsale is FinalizableCrowdsale { super.finalization(); } - // We're overriding the fund forwarding from Crowdsale. - // In addition to sending the funds, we want to call - // the RefundVault deposit function - function forwardFunds() internal { + /** + * @dev Overrides Crowdsale fund forwarding, sending funds to vault. + */ + function _forwardFunds() internal { vault.deposit.value(msg.value)(msg.sender); } diff --git a/contracts/crowdsale/RefundVault.sol b/contracts/crowdsale/distribution/utils/RefundVault.sol similarity index 86% rename from contracts/crowdsale/RefundVault.sol rename to contracts/crowdsale/distribution/utils/RefundVault.sol index bf6000000..d29f9bcf7 100644 --- a/contracts/crowdsale/RefundVault.sol +++ b/contracts/crowdsale/distribution/utils/RefundVault.sol @@ -1,7 +1,7 @@ pragma solidity ^0.4.18; -import "../math/SafeMath.sol"; -import "../ownership/Ownable.sol"; +import "../../../math/SafeMath.sol"; +import "../../../ownership/Ownable.sol"; /** @@ -23,12 +23,18 @@ contract RefundVault is Ownable { event RefundsEnabled(); event Refunded(address indexed beneficiary, uint256 weiAmount); + /** + * @param _wallet Vault address + */ function RefundVault(address _wallet) public { require(_wallet != address(0)); wallet = _wallet; state = State.Active; } + /** + * @param investor Investor address + */ function deposit(address investor) onlyOwner public payable { require(state == State.Active); deposited[investor] = deposited[investor].add(msg.value); @@ -47,6 +53,9 @@ contract RefundVault is Ownable { RefundsEnabled(); } + /** + * @param investor Investor address + */ function refund(address investor) public { require(state == State.Refunding); uint256 depositedValue = deposited[investor]; diff --git a/contracts/crowdsale/emission/AllowanceCrowdsale.sol b/contracts/crowdsale/emission/AllowanceCrowdsale.sol new file mode 100644 index 000000000..fb34e00e1 --- /dev/null +++ b/contracts/crowdsale/emission/AllowanceCrowdsale.sol @@ -0,0 +1,42 @@ +pragma solidity ^0.4.18; + +import "../Crowdsale.sol"; +import "../../token/ERC20/ERC20.sol"; +import "../../math/SafeMath.sol"; + + +/** + * @title AllowanceCrowdsale + * @dev Extension of Crowdsale where tokens are held by a wallet, which approves an allowance to the crowdsale. + */ +contract AllowanceCrowdsale is Crowdsale { + using SafeMath for uint256; + + address public tokenWallet; + + /** + * @dev Constructor, takes token wallet address. + * @param _tokenWallet Address holding the tokens, which has approved allowance to the crowdsale + */ + function AllowanceCrowdsale(address _tokenWallet) public { + require(_tokenWallet != address(0)); + tokenWallet = _tokenWallet; + } + + /** + * @dev Checks the amount of tokens left in the allowance. + * @return Amount of tokens left in the allowance + */ + function remainingTokens() public view returns (uint256) { + return token.allowance(tokenWallet, this); + } + + /** + * @dev Overrides parent behavior by transferring tokens from wallet. + * @param _beneficiary Token purchaser + * @param _tokenAmount Amount of tokens purchased + */ + function _deliverTokens(address _beneficiary, uint256 _tokenAmount) internal { + token.transferFrom(tokenWallet, _beneficiary, _tokenAmount); + } +} diff --git a/contracts/crowdsale/emission/MintedCrowdsale.sol b/contracts/crowdsale/emission/MintedCrowdsale.sol new file mode 100644 index 000000000..fa78e773e --- /dev/null +++ b/contracts/crowdsale/emission/MintedCrowdsale.sol @@ -0,0 +1,22 @@ +pragma solidity ^0.4.18; + +import "../Crowdsale.sol"; +import "../../token/ERC20/MintableToken.sol"; + + +/** + * @title MintedCrowdsale + * @dev Extension of Crowdsale contract whose tokens are minted in each purchase. + * Token ownership should be transferred to MintedCrowdsale for minting. + */ +contract MintedCrowdsale is Crowdsale { + + /** + * @dev Overrides delivery by minting tokens upon purchase. + * @param _beneficiary Token purchaser + * @param _tokenAmount Number of tokens to be minted + */ + function _deliverTokens(address _beneficiary, uint256 _tokenAmount) internal { + require(MintableToken(token).mint(_beneficiary, _tokenAmount)); + } +} diff --git a/contracts/crowdsale/price/IncreasingPriceCrowdsale.sol b/contracts/crowdsale/price/IncreasingPriceCrowdsale.sol new file mode 100644 index 000000000..ae4ca4169 --- /dev/null +++ b/contracts/crowdsale/price/IncreasingPriceCrowdsale.sol @@ -0,0 +1,52 @@ +pragma solidity ^0.4.18; + +import "../validation/TimedCrowdsale.sol"; +import "../../math/SafeMath.sol"; + +/** + * @title IncreasingPriceCrowdsale + * @dev Extension of Crowdsale contract that increases the price of tokens linearly in time. + * Note that what should be provided to the constructor is the initial and final _rates_, that is, + * the amount of tokens per wei contributed. Thus, the initial rate must be greater than the final rate. + */ +contract IncreasingPriceCrowdsale is TimedCrowdsale { + using SafeMath for uint256; + + uint256 public initialRate; + uint256 public finalRate; + + /** + * @dev Constructor, takes intial and final rates of tokens received per wei contributed. + * @param _initialRate Number of tokens a buyer gets per wei at the start of the crowdsale + * @param _finalRate Number of tokens a buyer gets per wei at the end of the crowdsale + */ + function IncreasingPriceCrowdsale(uint256 _initialRate, uint256 _finalRate) public { + require(_initialRate >= _finalRate); + require(_finalRate > 0); + initialRate = _initialRate; + finalRate = _finalRate; + } + + /** + * @dev Returns the rate of tokens per wei at the present time. + * Note that, as price _increases_ with time, the rate _decreases_. + * @return The number of tokens a buyer gets per wei at a given time + */ + function getCurrentRate() public view returns (uint256) { + uint256 elapsedTime = now.sub(openingTime); + uint256 timeRange = closingTime.sub(openingTime); + uint256 rateRange = initialRate.sub(finalRate); + return initialRate.sub(elapsedTime.mul(rateRange).div(timeRange)); + } + + /** + * @dev Overrides parent method taking into account variable rate. + * @param _weiAmount The value in wei to be converted into tokens + * @return The number of tokens _weiAmount wei will buy at present time + */ + function _getTokenAmount(uint256 _weiAmount) internal view returns (uint256) { + uint256 currentRate = getCurrentRate(); + return currentRate.mul(_weiAmount); + } + +} diff --git a/contracts/crowdsale/validation/CappedCrowdsale.sol b/contracts/crowdsale/validation/CappedCrowdsale.sol new file mode 100644 index 000000000..eafd1fe4d --- /dev/null +++ b/contracts/crowdsale/validation/CappedCrowdsale.sol @@ -0,0 +1,43 @@ +pragma solidity ^0.4.18; + +import "../../math/SafeMath.sol"; +import "../Crowdsale.sol"; + + +/** + * @title CappedCrowdsale + * @dev Crowdsale with a limit for total contributions. + */ +contract CappedCrowdsale is Crowdsale { + using SafeMath for uint256; + + uint256 public cap; + + /** + * @dev Constructor, takes maximum amount of wei accepted in the crowdsale. + * @param _cap Max amount of wei to be contributed + */ + function CappedCrowdsale(uint256 _cap) public { + require(_cap > 0); + cap = _cap; + } + + /** + * @dev Checks whether the cap has been reached. + * @return Whether the cap was reached + */ + function capReached() public view returns (bool) { + return weiRaised >= cap; + } + + /** + * @dev Extend parent behavior requiring purchase to respect the funding cap. + * @param _beneficiary Token purchaser + * @param _weiAmount Amount of wei contributed + */ + function _preValidatePurchase(address _beneficiary, uint256 _weiAmount) internal { + super._preValidatePurchase(_beneficiary, _weiAmount); + require(weiRaised.add(_weiAmount) <= cap); + } + +} diff --git a/contracts/crowdsale/validation/IndividuallyCappedCrowdsale.sol b/contracts/crowdsale/validation/IndividuallyCappedCrowdsale.sol new file mode 100644 index 000000000..59c3a25f9 --- /dev/null +++ b/contracts/crowdsale/validation/IndividuallyCappedCrowdsale.sol @@ -0,0 +1,76 @@ +pragma solidity ^ 0.4.18; + +import "../../math/SafeMath.sol"; +import "../Crowdsale.sol"; +import "../../ownership/Ownable.sol"; + + +/** + * @title IndividuallyCappedCrowdsale + * @dev Crowdsale with per-user caps. + */ +contract IndividuallyCappedCrowdsale is Crowdsale, Ownable { + using SafeMath for uint256; + + mapping(address => uint256) public contributions; + mapping(address => uint256) public caps; + + /** + * @dev Sets a specific user's maximum contribution. + * @param _beneficiary Address to be capped + * @param _cap Wei limit for individual contribution + */ + function setUserCap(address _beneficiary, uint256 _cap) external onlyOwner { + caps[_beneficiary] = _cap; + } + + /** + * @dev Sets a group of users' maximum contribution. + * @param _beneficiaries List of addresses to be capped + * @param _cap Wei limit for individual contribution + */ + function setGroupCap(address[] _beneficiaries, uint256 _cap) external onlyOwner { + for (uint256 i = 0; i < _beneficiaries.length; i++) { + caps[_beneficiaries[i]] = _cap; + } + } + + /** + * @dev Returns the cap of a specific user. + * @param _beneficiary Address whose cap is to be checked + * @return Current cap for individual user + */ + function getUserCap(address _beneficiary) public view returns (uint256) { + return caps[_beneficiary]; + } + + /** + * @dev Returns the amount contributed so far by a sepecific user. + * @param _beneficiary Address of contributor + * @return User contribution so far + */ + function getUserContribution(address _beneficiary) public view returns (uint256) { + return contributions[_beneficiary]; + } + + /** + * @dev Extend parent behavior requiring purchase to respect the user's funding cap. + * @param _beneficiary Token purchaser + * @param _weiAmount Amount of wei contributed + */ + function _preValidatePurchase(address _beneficiary, uint256 _weiAmount) internal { + super._preValidatePurchase(_beneficiary, _weiAmount); + require(contributions[_beneficiary].add(_weiAmount) <= caps[_beneficiary]); + } + + /** + * @dev Extend parent behavior to update user contributions + * @param _beneficiary Token purchaser + * @param _weiAmount Amount of wei contributed + */ + function _updatePurchasingState(address _beneficiary, uint256 _weiAmount) internal { + super._updatePurchasingState(_beneficiary, _weiAmount); + contributions[_beneficiary] = contributions[_beneficiary].add(_weiAmount); + } + +} diff --git a/contracts/crowdsale/validation/TimedCrowdsale.sol b/contracts/crowdsale/validation/TimedCrowdsale.sol new file mode 100644 index 000000000..b015a1c74 --- /dev/null +++ b/contracts/crowdsale/validation/TimedCrowdsale.sol @@ -0,0 +1,55 @@ +pragma solidity ^0.4.18; + +import "../../math/SafeMath.sol"; +import "../Crowdsale.sol"; + + +/** + * @title TimedCrowdsale + * @dev Crowdsale accepting contributions only within a time frame. + */ +contract TimedCrowdsale is Crowdsale { + using SafeMath for uint256; + + uint256 public openingTime; + uint256 public closingTime; + + /** + * @dev Reverts if not in crowdsale time range. + */ + modifier onlyWhileOpen { + require(now >= openingTime && now <= closingTime); + _; + } + + /** + * @dev Constructor, takes crowdsale opening and closing times. + * @param _openingTime Crowdsale opening time + * @param _closingTime Crowdsale closing time + */ + function TimedCrowdsale(uint256 _openingTime, uint256 _closingTime) public { + require(_openingTime >= now); + require(_closingTime >= _openingTime); + + openingTime = _openingTime; + closingTime = _closingTime; + } + + /** + * @dev Checks whether the period in which the crowdsale is open has already elapsed. + * @return Whether crowdsale period has elapsed + */ + function hasClosed() public view returns (bool) { + return now > closingTime; + } + + /** + * @dev Extend parent behavior requiring to be within contributing period + * @param _beneficiary Token purchaser + * @param _weiAmount Amount of wei contributed + */ + function _preValidatePurchase(address _beneficiary, uint256 _weiAmount) internal onlyWhileOpen { + super._preValidatePurchase(_beneficiary, _weiAmount); + } + +} diff --git a/contracts/crowdsale/validation/WhitelistedCrowdsale.sol b/contracts/crowdsale/validation/WhitelistedCrowdsale.sol new file mode 100644 index 000000000..f46953ded --- /dev/null +++ b/contracts/crowdsale/validation/WhitelistedCrowdsale.sol @@ -0,0 +1,58 @@ +pragma solidity ^ 0.4.18; + +import "../Crowdsale.sol"; +import "../../ownership/Ownable.sol"; + + +/** + * @title WhitelistedCrowdsale + * @dev Crowdsale in which only whitelisted users can contribute. + */ +contract WhitelistedCrowdsale is Crowdsale, Ownable { + + mapping(address => bool) public whitelist; + + /** + * @dev Reverts if beneficiary is not whitelisted. Can be used when extending this contract. + */ + modifier isWhitelisted(address _beneficiary) { + require(whitelist[_beneficiary]); + _; + } + + /** + * @dev Adds single address to whitelist. + * @param _beneficiary Address to be added to the whitelist + */ + function addToWhitelist(address _beneficiary) external onlyOwner { + whitelist[_beneficiary] = true; + } + + /** + * @dev Adds list of addresses to whitelist. Not overloaded due to limitations with truffle testing. + * @param _beneficiaries Addresses to be added to the whitelist + */ + function addManyToWhitelist(address[] _beneficiaries) external onlyOwner { + for (uint256 i = 0; i < _beneficiaries.length; i++) { + whitelist[_beneficiaries[i]] = true; + } + } + + /** + * @dev Removes single address from whitelist. + * @param _beneficiary Address to be removed to the whitelist + */ + function removeFromWhitelist(address _beneficiary) external onlyOwner { + whitelist[_beneficiary] = false; + } + + /** + * @dev Extend parent behavior requiring beneficiary to be in whitelist. + * @param _beneficiary Token beneficiary + * @param _weiAmount Amount of wei contributed + */ + function _preValidatePurchase(address _beneficiary, uint256 _weiAmount) internal isWhitelisted(_beneficiary) { + super._preValidatePurchase(_beneficiary, _weiAmount); + } + +} diff --git a/contracts/examples/SampleCrowdsale.sol b/contracts/examples/SampleCrowdsale.sol index a9d111f8c..969ed460b 100644 --- a/contracts/examples/SampleCrowdsale.sol +++ b/contracts/examples/SampleCrowdsale.sol @@ -1,7 +1,8 @@ pragma solidity ^0.4.18; -import "../crowdsale/CappedCrowdsale.sol"; -import "../crowdsale/RefundableCrowdsale.sol"; +import "../crowdsale/validation/CappedCrowdsale.sol"; +import "../crowdsale/distribution/RefundableCrowdsale.sol"; +import "../crowdsale/emission/MintedCrowdsale.sol"; import "../token/ERC20/MintableToken.sol"; @@ -30,13 +31,13 @@ contract SampleCrowdsaleToken is MintableToken { * After adding multiple features it's good practice to run integration tests * to ensure that subcontracts works together as intended. */ -contract SampleCrowdsale is CappedCrowdsale, RefundableCrowdsale { +contract SampleCrowdsale is CappedCrowdsale, RefundableCrowdsale, MintedCrowdsale { - function SampleCrowdsale(uint256 _startTime, uint256 _endTime, uint256 _rate, uint256 _goal, uint256 _cap, address _wallet, MintableToken _token) public + function SampleCrowdsale(uint256 _openingTime, uint256 _closingTime, uint256 _rate, address _wallet, uint256 _cap, MintableToken _token, uint256 _goal) public + Crowdsale(_rate, _wallet, _token) CappedCrowdsale(_cap) - FinalizableCrowdsale() + TimedCrowdsale(_openingTime, _closingTime) RefundableCrowdsale(_goal) - Crowdsale(_startTime, _endTime, _rate, _wallet, _token) { //As goal needs to be met for a successful crowdsale //the value needs to less or equal than a cap which is limit for accepted funds diff --git a/contracts/mocks/AllowanceCrowdsaleImpl.sol b/contracts/mocks/AllowanceCrowdsaleImpl.sol new file mode 100644 index 000000000..896f75b3c --- /dev/null +++ b/contracts/mocks/AllowanceCrowdsaleImpl.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.4.18; + +import "../token/ERC20/ERC20.sol"; +import "../crowdsale/emission/AllowanceCrowdsale.sol"; + + +contract AllowanceCrowdsaleImpl is AllowanceCrowdsale { + + function AllowanceCrowdsaleImpl ( + uint256 _rate, + address _wallet, + ERC20 _token, + address _tokenWallet + ) + public + Crowdsale(_rate, _wallet, _token) + AllowanceCrowdsale(_tokenWallet) + { + } + +} diff --git a/contracts/mocks/CappedCrowdsaleImpl.sol b/contracts/mocks/CappedCrowdsaleImpl.sol index 39a3bc88e..45beec6a6 100644 --- a/contracts/mocks/CappedCrowdsaleImpl.sol +++ b/contracts/mocks/CappedCrowdsaleImpl.sol @@ -1,20 +1,19 @@ pragma solidity ^0.4.18; - -import "../crowdsale/CappedCrowdsale.sol"; +import "../token/ERC20/ERC20.sol"; +import "../crowdsale/validation/CappedCrowdsale.sol"; contract CappedCrowdsaleImpl is CappedCrowdsale { function CappedCrowdsaleImpl ( - uint256 _startTime, - uint256 _endTime, uint256 _rate, address _wallet, - uint256 _cap, - MintableToken _token - ) public - Crowdsale(_startTime, _endTime, _rate, _wallet, _token) + ERC20 _token, + uint256 _cap + ) + public + Crowdsale(_rate, _wallet, _token) CappedCrowdsale(_cap) { } diff --git a/contracts/mocks/FinalizableCrowdsaleImpl.sol b/contracts/mocks/FinalizableCrowdsaleImpl.sol index a03a0ad3d..2eb0a9278 100644 --- a/contracts/mocks/FinalizableCrowdsaleImpl.sol +++ b/contracts/mocks/FinalizableCrowdsaleImpl.sol @@ -1,19 +1,21 @@ pragma solidity ^0.4.18; - -import "../crowdsale/FinalizableCrowdsale.sol"; +import "../token/ERC20/MintableToken.sol"; +import "../crowdsale/distribution/FinalizableCrowdsale.sol"; contract FinalizableCrowdsaleImpl is FinalizableCrowdsale { function FinalizableCrowdsaleImpl ( - uint256 _startTime, - uint256 _endTime, + uint256 _openingTime, + uint256 _closingTime, uint256 _rate, address _wallet, MintableToken _token - ) public - Crowdsale(_startTime, _endTime, _rate, _wallet, _token) + ) + public + Crowdsale(_rate, _wallet, _token) + TimedCrowdsale(_openingTime, _closingTime) { } diff --git a/contracts/mocks/IncreasingPriceCrowdsaleImpl.sol b/contracts/mocks/IncreasingPriceCrowdsaleImpl.sol new file mode 100644 index 000000000..d9e7b7e3c --- /dev/null +++ b/contracts/mocks/IncreasingPriceCrowdsaleImpl.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.4.18; + +import "../crowdsale/price/IncreasingPriceCrowdsale.sol"; +import "../math/SafeMath.sol"; + + +contract IncreasingPriceCrowdsaleImpl is IncreasingPriceCrowdsale { + + function IncreasingPriceCrowdsaleImpl ( + uint256 _openingTime, + uint256 _closingTime, + address _wallet, + ERC20 _token, + uint256 _initialRate, + uint256 _finalRate + ) + public + Crowdsale(_initialRate, _wallet, _token) + TimedCrowdsale(_openingTime, _closingTime) + IncreasingPriceCrowdsale(_initialRate, _finalRate) + { + } + +} diff --git a/contracts/mocks/IndividuallyCappedCrowdsaleImpl.sol b/contracts/mocks/IndividuallyCappedCrowdsaleImpl.sol new file mode 100644 index 000000000..c8f58493a --- /dev/null +++ b/contracts/mocks/IndividuallyCappedCrowdsaleImpl.sol @@ -0,0 +1,19 @@ +pragma solidity ^0.4.18; + +import "../token/ERC20/ERC20.sol"; +import "../crowdsale/validation/IndividuallyCappedCrowdsale.sol"; + + +contract IndividuallyCappedCrowdsaleImpl is IndividuallyCappedCrowdsale { + + function IndividuallyCappedCrowdsaleImpl ( + uint256 _rate, + address _wallet, + ERC20 _token + ) + public + Crowdsale(_rate, _wallet, _token) + { + } + +} diff --git a/contracts/mocks/MintedCrowdsaleImpl.sol b/contracts/mocks/MintedCrowdsaleImpl.sol new file mode 100644 index 000000000..2406f08a0 --- /dev/null +++ b/contracts/mocks/MintedCrowdsaleImpl.sol @@ -0,0 +1,19 @@ +pragma solidity ^0.4.18; + +import "../token/ERC20/MintableToken.sol"; +import "../crowdsale/emission/MintedCrowdsale.sol"; + + +contract MintedCrowdsaleImpl is MintedCrowdsale { + + function MintedCrowdsaleImpl ( + uint256 _rate, + address _wallet, + MintableToken _token + ) + public + Crowdsale(_rate, _wallet, _token) + { + } + +} diff --git a/contracts/mocks/PostDeliveryCrowdsaleImpl.sol b/contracts/mocks/PostDeliveryCrowdsaleImpl.sol new file mode 100644 index 000000000..6ab1401f6 --- /dev/null +++ b/contracts/mocks/PostDeliveryCrowdsaleImpl.sol @@ -0,0 +1,22 @@ +pragma solidity ^0.4.18; + +import "../token/ERC20/ERC20.sol"; +import "../crowdsale/distribution/PostDeliveryCrowdsale.sol"; + + +contract PostDeliveryCrowdsaleImpl is PostDeliveryCrowdsale { + + function PostDeliveryCrowdsaleImpl ( + uint256 _openingTime, + uint256 _closingTime, + uint256 _rate, + address _wallet, + ERC20 _token + ) + public + TimedCrowdsale(_openingTime, _closingTime) + Crowdsale(_rate, _wallet, _token) + { + } + +} diff --git a/contracts/mocks/RefundableCrowdsaleImpl.sol b/contracts/mocks/RefundableCrowdsaleImpl.sol index d1744b6de..96ec7a72f 100644 --- a/contracts/mocks/RefundableCrowdsaleImpl.sol +++ b/contracts/mocks/RefundableCrowdsaleImpl.sol @@ -1,20 +1,21 @@ pragma solidity ^0.4.18; - -import "../crowdsale/RefundableCrowdsale.sol"; - +import "../token/ERC20/MintableToken.sol"; +import "../crowdsale/distribution/RefundableCrowdsale.sol"; contract RefundableCrowdsaleImpl is RefundableCrowdsale { function RefundableCrowdsaleImpl ( - uint256 _startTime, - uint256 _endTime, + uint256 _openingTime, + uint256 _closingTime, uint256 _rate, address _wallet, - uint256 _goal, - MintableToken _token - ) public - Crowdsale(_startTime, _endTime, _rate, _wallet, _token) + MintableToken _token, + uint256 _goal + ) + public + Crowdsale(_rate, _wallet, _token) + TimedCrowdsale(_openingTime, _closingTime) RefundableCrowdsale(_goal) { } diff --git a/contracts/mocks/TimedCrowdsaleImpl.sol b/contracts/mocks/TimedCrowdsaleImpl.sol new file mode 100644 index 000000000..ad2b3f324 --- /dev/null +++ b/contracts/mocks/TimedCrowdsaleImpl.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.4.18; + +import "../token/ERC20/ERC20.sol"; +import "../crowdsale/validation/TimedCrowdsale.sol"; + +contract TimedCrowdsaleImpl is TimedCrowdsale { + + function TimedCrowdsaleImpl ( + uint256 _openingTime, + uint256 _closingTime, + uint256 _rate, + address _wallet, + ERC20 _token + ) + public + Crowdsale(_rate, _wallet, _token) + TimedCrowdsale(_openingTime, _closingTime) + { + } + +} diff --git a/contracts/mocks/WhitelistedCrowdsaleImpl.sol b/contracts/mocks/WhitelistedCrowdsaleImpl.sol new file mode 100644 index 000000000..fc899bf39 --- /dev/null +++ b/contracts/mocks/WhitelistedCrowdsaleImpl.sol @@ -0,0 +1,19 @@ +pragma solidity ^0.4.18; + +import "../token/ERC20/ERC20.sol"; +import "../crowdsale/validation/WhitelistedCrowdsale.sol"; + + +contract WhitelistedCrowdsaleImpl is WhitelistedCrowdsale { + + function WhitelistedCrowdsaleImpl ( + uint256 _rate, + address _wallet, + ERC20 _token + ) + public + Crowdsale(_rate, _wallet, _token) + { + } + +} diff --git a/test/crowdsale/AllowanceCrowdsale.test.js b/test/crowdsale/AllowanceCrowdsale.test.js new file mode 100644 index 000000000..26b675607 --- /dev/null +++ b/test/crowdsale/AllowanceCrowdsale.test.js @@ -0,0 +1,68 @@ +import ether from '../helpers/ether'; + +const BigNumber = web3.BigNumber; + +const should = require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const AllowanceCrowdsale = artifacts.require('AllowanceCrowdsaleImpl'); +const SimpleToken = artifacts.require('SimpleToken'); + +contract('AllowanceCrowdsale', function ([_, investor, wallet, purchaser, tokenWallet]) { + const rate = new BigNumber(1); + const value = ether(0.42); + const expectedTokenAmount = rate.mul(value); + const tokenAllowance = new BigNumber('1e22'); + + beforeEach(async function () { + this.token = await SimpleToken.new({ from: tokenWallet }); + this.crowdsale = await AllowanceCrowdsale.new(rate, wallet, this.token.address, tokenWallet); + await this.token.approve(this.crowdsale.address, tokenAllowance, { from: tokenWallet }); + }); + + describe('accepting payments', function () { + it('should accept sends', async function () { + await this.crowdsale.send(value).should.be.fulfilled; + }); + + it('should accept payments', async function () { + await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }).should.be.fulfilled; + }); + }); + + describe('high-level purchase', function () { + it('should log purchase', async function () { + const { logs } = await this.crowdsale.sendTransaction({ value: value, from: investor }); + const event = logs.find(e => e.event === 'TokenPurchase'); + should.exist(event); + event.args.purchaser.should.equal(investor); + event.args.beneficiary.should.equal(investor); + event.args.value.should.be.bignumber.equal(value); + event.args.amount.should.be.bignumber.equal(expectedTokenAmount); + }); + + it('should assign tokens to sender', async function () { + await this.crowdsale.sendTransaction({ value: value, from: investor }); + let balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(expectedTokenAmount); + }); + + it('should forward funds to wallet', async function () { + const pre = web3.eth.getBalance(wallet); + await this.crowdsale.sendTransaction({ value, from: investor }); + const post = web3.eth.getBalance(wallet); + post.minus(pre).should.be.bignumber.equal(value); + }); + }); + + describe('check remaining allowance', function () { + it('should report correct allowace left', async function () { + let remainingAllowance = tokenAllowance - expectedTokenAmount; + await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }); + let tokensRemaining = await this.crowdsale.remainingTokens(); + tokensRemaining.should.be.bignumber.equal(remainingAllowance); + }); + }); +}); diff --git a/test/crowdsale/CappedCrowdsale.test.js b/test/crowdsale/CappedCrowdsale.test.js index 34bd42328..a2b0c504f 100644 --- a/test/crowdsale/CappedCrowdsale.test.js +++ b/test/crowdsale/CappedCrowdsale.test.js @@ -1,7 +1,4 @@ import ether from '../helpers/ether'; -import { advanceBlock } from '../helpers/advanceToBlock'; -import { increaseTimeTo, duration } from '../helpers/increaseTime'; -import latestTime from '../helpers/latestTime'; import EVMRevert from '../helpers/EVMRevert'; const BigNumber = web3.BigNumber; @@ -12,39 +9,27 @@ require('chai') .should(); const CappedCrowdsale = artifacts.require('CappedCrowdsaleImpl'); -const MintableToken = artifacts.require('MintableToken'); +const SimpleToken = artifacts.require('SimpleToken'); contract('CappedCrowdsale', function ([_, wallet]) { - const rate = new BigNumber(1000); - - const cap = ether(300); + const rate = new BigNumber(1); + const cap = ether(100); const lessThanCap = ether(60); - - before(async function () { - // Advance to the next block to correctly read time in the solidity "now" function interpreted by testrpc - await advanceBlock(); - }); + const tokenSupply = new BigNumber('1e22'); beforeEach(async function () { - this.startTime = latestTime() + duration.weeks(1); - this.endTime = this.startTime + duration.weeks(1); - - this.token = await MintableToken.new(); - this.crowdsale = await CappedCrowdsale.new(this.startTime, this.endTime, rate, wallet, cap, this.token.address); - await this.token.transferOwnership(this.crowdsale.address); + this.token = await SimpleToken.new(); + this.crowdsale = await CappedCrowdsale.new(rate, wallet, this.token.address, cap); + this.token.transfer(this.crowdsale.address, tokenSupply); }); describe('creating a valid crowdsale', function () { it('should fail with zero cap', async function () { - await CappedCrowdsale.new(this.startTime, this.endTime, rate, wallet, 0).should.be.rejectedWith(EVMRevert); + await CappedCrowdsale.new(rate, wallet, 0, this.token.address).should.be.rejectedWith(EVMRevert); }); }); describe('accepting payments', function () { - beforeEach(async function () { - await increaseTimeTo(this.startTime); - }); - it('should accept payments within cap', async function () { await this.crowdsale.send(cap.minus(lessThanCap)).should.be.fulfilled; await this.crowdsale.send(lessThanCap).should.be.fulfilled; @@ -61,28 +46,24 @@ contract('CappedCrowdsale', function ([_, wallet]) { }); describe('ending', function () { - beforeEach(async function () { - await increaseTimeTo(this.startTime); - }); - - it('should not be ended if under cap', async function () { - let hasEnded = await this.crowdsale.hasEnded(); - hasEnded.should.equal(false); + it('should not reach cap if sent under cap', async function () { + let capReached = await this.crowdsale.capReached(); + capReached.should.equal(false); await this.crowdsale.send(lessThanCap); - hasEnded = await this.crowdsale.hasEnded(); - hasEnded.should.equal(false); + capReached = await this.crowdsale.capReached(); + capReached.should.equal(false); }); - it('should not be ended if just under cap', async function () { + it('should not reach cap if sent just under cap', async function () { await this.crowdsale.send(cap.minus(1)); - let hasEnded = await this.crowdsale.hasEnded(); - hasEnded.should.equal(false); + let capReached = await this.crowdsale.capReached(); + capReached.should.equal(false); }); - it('should be ended if cap reached', async function () { + it('should reach cap if cap sent', async function () { await this.crowdsale.send(cap); - let hasEnded = await this.crowdsale.hasEnded(); - hasEnded.should.equal(true); + let capReached = await this.crowdsale.capReached(); + capReached.should.equal(true); }); }); }); diff --git a/test/crowdsale/Crowdsale.test.js b/test/crowdsale/Crowdsale.test.js index 340838061..4053dca37 100644 --- a/test/crowdsale/Crowdsale.test.js +++ b/test/crowdsale/Crowdsale.test.js @@ -1,8 +1,4 @@ import ether from '../helpers/ether'; -import { advanceBlock } from '../helpers/advanceToBlock'; -import { increaseTimeTo, duration } from '../helpers/increaseTime'; -import latestTime from '../helpers/latestTime'; -import EVMRevert from '../helpers/EVMRevert'; const BigNumber = web3.BigNumber; @@ -12,71 +8,31 @@ const should = require('chai') .should(); const Crowdsale = artifacts.require('Crowdsale'); -const MintableToken = artifacts.require('MintableToken'); +const SimpleToken = artifacts.require('SimpleToken'); contract('Crowdsale', function ([_, investor, wallet, purchaser]) { - const rate = new BigNumber(1000); + const rate = new BigNumber(1); const value = ether(42); - + const tokenSupply = new BigNumber('1e22'); const expectedTokenAmount = rate.mul(value); - before(async function () { - // Advance to the next block to correctly read time in the solidity "now" function interpreted by testrpc - await advanceBlock(); - }); - beforeEach(async function () { - this.startTime = latestTime() + duration.weeks(1); - this.endTime = this.startTime + duration.weeks(1); - this.afterEndTime = this.endTime + duration.seconds(1); - - this.token = await MintableToken.new(); - this.crowdsale = await Crowdsale.new(this.startTime, this.endTime, rate, wallet, this.token.address); - await this.token.transferOwnership(this.crowdsale.address); - }); - - it('should be token owner', async function () { - const owner = await this.token.owner(); - owner.should.equal(this.crowdsale.address); - }); - - it('should be ended only after end', async function () { - let ended = await this.crowdsale.hasEnded(); - ended.should.equal(false); - await increaseTimeTo(this.afterEndTime); - ended = await this.crowdsale.hasEnded(); - ended.should.equal(true); + this.token = await SimpleToken.new(); + this.crowdsale = await Crowdsale.new(rate, wallet, this.token.address); + await this.token.transfer(this.crowdsale.address, tokenSupply); }); describe('accepting payments', function () { - it('should reject payments before start', async function () { - await this.crowdsale.send(value).should.be.rejectedWith(EVMRevert); - await this.crowdsale.buyTokens(investor, { from: purchaser, value: value }).should.be.rejectedWith(EVMRevert); - }); - - it('should accept payments after start', async function () { - await increaseTimeTo(this.startTime); + it('should accept payments', async function () { await this.crowdsale.send(value).should.be.fulfilled; await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }).should.be.fulfilled; }); - - it('should reject payments after end', async function () { - await increaseTimeTo(this.afterEndTime); - await this.crowdsale.send(value).should.be.rejectedWith(EVMRevert); - await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }).should.be.rejectedWith(EVMRevert); - }); }); describe('high-level purchase', function () { - beforeEach(async function () { - await increaseTimeTo(this.startTime); - }); - it('should log purchase', async function () { const { logs } = await this.crowdsale.sendTransaction({ value: value, from: investor }); - const event = logs.find(e => e.event === 'TokenPurchase'); - should.exist(event); event.args.purchaser.should.equal(investor); event.args.beneficiary.should.equal(investor); @@ -84,12 +40,6 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) { event.args.amount.should.be.bignumber.equal(expectedTokenAmount); }); - it('should increase totalSupply', async function () { - await this.crowdsale.send(value); - const totalSupply = await this.token.totalSupply(); - totalSupply.should.be.bignumber.equal(expectedTokenAmount); - }); - it('should assign tokens to sender', async function () { await this.crowdsale.sendTransaction({ value: value, from: investor }); let balance = await this.token.balanceOf(investor); @@ -105,15 +55,9 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) { }); describe('low-level purchase', function () { - beforeEach(async function () { - await increaseTimeTo(this.startTime); - }); - it('should log purchase', async function () { const { logs } = await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }); - const event = logs.find(e => e.event === 'TokenPurchase'); - should.exist(event); event.args.purchaser.should.equal(purchaser); event.args.beneficiary.should.equal(investor); @@ -121,12 +65,6 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) { event.args.amount.should.be.bignumber.equal(expectedTokenAmount); }); - it('should increase totalSupply', async function () { - await this.crowdsale.buyTokens(investor, { value, from: purchaser }); - const totalSupply = await this.token.totalSupply(); - totalSupply.should.be.bignumber.equal(expectedTokenAmount); - }); - it('should assign tokens to beneficiary', async function () { await this.crowdsale.buyTokens(investor, { value, from: purchaser }); const balance = await this.token.balanceOf(investor); diff --git a/test/crowdsale/FinalizableCrowdsale.test.js b/test/crowdsale/FinalizableCrowdsale.test.js index ecba5085a..169a6043d 100644 --- a/test/crowdsale/FinalizableCrowdsale.test.js +++ b/test/crowdsale/FinalizableCrowdsale.test.js @@ -22,13 +22,13 @@ contract('FinalizableCrowdsale', function ([_, owner, wallet, thirdparty]) { }); beforeEach(async function () { - this.startTime = latestTime() + duration.weeks(1); - this.endTime = this.startTime + duration.weeks(1); - this.afterEndTime = this.endTime + duration.seconds(1); + this.openingTime = latestTime() + duration.weeks(1); + this.closingTime = this.openingTime + duration.weeks(1); + this.afterClosingTime = this.closingTime + duration.seconds(1); this.token = await MintableToken.new(); this.crowdsale = await FinalizableCrowdsale.new( - this.startTime, this.endTime, rate, wallet, this.token.address, { from: owner } + this.openingTime, this.closingTime, rate, wallet, this.token.address, { from: owner } ); await this.token.transferOwnership(this.crowdsale.address); }); @@ -38,23 +38,23 @@ contract('FinalizableCrowdsale', function ([_, owner, wallet, thirdparty]) { }); it('cannot be finalized by third party after ending', async function () { - await increaseTimeTo(this.afterEndTime); + await increaseTimeTo(this.afterClosingTime); await this.crowdsale.finalize({ from: thirdparty }).should.be.rejectedWith(EVMRevert); }); it('can be finalized by owner after ending', async function () { - await increaseTimeTo(this.afterEndTime); + await increaseTimeTo(this.afterClosingTime); await this.crowdsale.finalize({ from: owner }).should.be.fulfilled; }); it('cannot be finalized twice', async function () { - await increaseTimeTo(this.afterEndTime); + await increaseTimeTo(this.afterClosingTime); await this.crowdsale.finalize({ from: owner }); await this.crowdsale.finalize({ from: owner }).should.be.rejectedWith(EVMRevert); }); it('logs finalized', async function () { - await increaseTimeTo(this.afterEndTime); + await increaseTimeTo(this.afterClosingTime); const { logs } = await this.crowdsale.finalize({ from: owner }); const event = logs.find(e => e.event === 'Finalized'); should.exist(event); diff --git a/test/crowdsale/IncreasingPriceCrowdsale.test.js b/test/crowdsale/IncreasingPriceCrowdsale.test.js new file mode 100644 index 000000000..76f82ab02 --- /dev/null +++ b/test/crowdsale/IncreasingPriceCrowdsale.test.js @@ -0,0 +1,92 @@ +import ether from '../helpers/ether'; +import { advanceBlock } from '../helpers/advanceToBlock'; +import { increaseTimeTo, duration } from '../helpers/increaseTime'; +import latestTime from '../helpers/latestTime'; + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const IncreasingPriceCrowdsale = artifacts.require('IncreasingPriceCrowdsaleImpl'); +const SimpleToken = artifacts.require('SimpleToken'); + +contract('IncreasingPriceCrowdsale', function ([_, investor, wallet, purchaser]) { + const value = ether(1); + const tokenSupply = new BigNumber('1e22'); + + describe('rate during crowdsale should change at a fixed step every block', async function () { + let balance; + const initialRate = new BigNumber(9166); + const finalRate = new BigNumber(5500); + const rateAtTime150 = new BigNumber(9166); + const rateAtTime300 = new BigNumber(9165); + const rateAtTime1500 = new BigNumber(9157); + const rateAtTime30 = new BigNumber(9166); + const rateAtTime150000 = new BigNumber(8257); + const rateAtTime450000 = new BigNumber(6439); + + beforeEach(async function () { + await advanceBlock(); + this.startTime = latestTime() + duration.weeks(1); + this.closingTime = this.startTime + duration.weeks(1); + this.afterClosingTime = this.closingTime + duration.seconds(1); + this.token = await SimpleToken.new(); + this.crowdsale = await IncreasingPriceCrowdsale.new( + this.startTime, this.closingTime, wallet, this.token.address, initialRate, finalRate + ); + await this.token.transfer(this.crowdsale.address, tokenSupply); + }); + + it('at start', async function () { + await increaseTimeTo(this.startTime); + await this.crowdsale.buyTokens(investor, { value, from: purchaser }); + balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(value.mul(initialRate)); + }); + + it('at time 150', async function () { + await increaseTimeTo(this.startTime + 150); + await this.crowdsale.buyTokens(investor, { value, from: purchaser }); + balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(value.mul(rateAtTime150)); + }); + + it('at time 300', async function () { + await increaseTimeTo(this.startTime + 300); + await this.crowdsale.buyTokens(investor, { value, from: purchaser }); + balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(value.mul(rateAtTime300)); + }); + + it('at time 1500', async function () { + await increaseTimeTo(this.startTime + 1500); + await this.crowdsale.buyTokens(investor, { value, from: purchaser }); + balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(value.mul(rateAtTime1500)); + }); + + it('at time 30', async function () { + await increaseTimeTo(this.startTime + 30); + await this.crowdsale.buyTokens(investor, { value, from: purchaser }); + balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(value.mul(rateAtTime30)); + }); + + it('at time 150000', async function () { + await increaseTimeTo(this.startTime + 150000); + await this.crowdsale.buyTokens(investor, { value, from: purchaser }); + balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(value.mul(rateAtTime150000)); + }); + + it('at time 450000', async function () { + await increaseTimeTo(this.startTime + 450000); + await this.crowdsale.buyTokens(investor, { value, from: purchaser }); + balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(value.mul(rateAtTime450000)); + }); + }); +}); diff --git a/test/crowdsale/IndividuallyCappedCrowdsale.test.js b/test/crowdsale/IndividuallyCappedCrowdsale.test.js new file mode 100644 index 000000000..de4c61203 --- /dev/null +++ b/test/crowdsale/IndividuallyCappedCrowdsale.test.js @@ -0,0 +1,107 @@ +import ether from '../helpers/ether'; +import EVMRevert from '../helpers/EVMRevert'; + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const CappedCrowdsale = artifacts.require('IndividuallyCappedCrowdsaleImpl'); +const SimpleToken = artifacts.require('SimpleToken'); + +contract('IndividuallyCappedCrowdsale', function ([_, wallet, alice, bob, charlie]) { + const rate = new BigNumber(1); + const capAlice = ether(10); + const capBob = ether(2); + const lessThanCapAlice = ether(6); + const lessThanCapBoth = ether(1); + const tokenSupply = new BigNumber('1e22'); + + describe('individual capping', function () { + beforeEach(async function () { + this.token = await SimpleToken.new(); + this.crowdsale = await CappedCrowdsale.new(rate, wallet, this.token.address); + this.crowdsale.setUserCap(alice, capAlice); + this.crowdsale.setUserCap(bob, capBob); + this.token.transfer(this.crowdsale.address, tokenSupply); + }); + + describe('accepting payments', function () { + it('should accept payments within cap', async function () { + await this.crowdsale.buyTokens(alice, { value: lessThanCapAlice }).should.be.fulfilled; + await this.crowdsale.buyTokens(bob, { value: lessThanCapBoth }).should.be.fulfilled; + }); + + it('should reject payments outside cap', async function () { + await this.crowdsale.buyTokens(alice, { value: capAlice }); + await this.crowdsale.buyTokens(alice, { value: 1 }).should.be.rejectedWith(EVMRevert); + }); + + it('should reject payments that exceed cap', async function () { + await this.crowdsale.buyTokens(alice, { value: capAlice.plus(1) }).should.be.rejectedWith(EVMRevert); + await this.crowdsale.buyTokens(bob, { value: capBob.plus(1) }).should.be.rejectedWith(EVMRevert); + }); + + it('should manage independent caps', async function () { + await this.crowdsale.buyTokens(alice, { value: lessThanCapAlice }).should.be.fulfilled; + await this.crowdsale.buyTokens(bob, { value: lessThanCapAlice }).should.be.rejectedWith(EVMRevert); + }); + + it('should default to a cap of zero', async function () { + await this.crowdsale.buyTokens(charlie, { value: lessThanCapBoth }).should.be.rejectedWith(EVMRevert); + }); + }); + + describe('reporting state', function () { + it('should report correct cap', async function () { + let retrievedCap = await this.crowdsale.getUserCap(alice); + retrievedCap.should.be.bignumber.equal(capAlice); + }); + + it('should report actual contribution', async function () { + await this.crowdsale.buyTokens(alice, { value: lessThanCapAlice }); + let retrievedContribution = await this.crowdsale.getUserContribution(alice); + retrievedContribution.should.be.bignumber.equal(lessThanCapAlice); + }); + }); + }); + + describe('group capping', function () { + beforeEach(async function () { + this.token = await SimpleToken.new(); + this.crowdsale = await CappedCrowdsale.new(rate, wallet, this.token.address); + this.crowdsale.setGroupCap([bob, charlie], capBob); + this.token.transfer(this.crowdsale.address, tokenSupply); + }); + + describe('accepting payments', function () { + it('should accept payments within cap', async function () { + await this.crowdsale.buyTokens(bob, { value: lessThanCapBoth }).should.be.fulfilled; + await this.crowdsale.buyTokens(charlie, { value: lessThanCapBoth }).should.be.fulfilled; + }); + + it('should reject payments outside cap', async function () { + await this.crowdsale.buyTokens(bob, { value: capBob }); + await this.crowdsale.buyTokens(bob, { value: 1 }).should.be.rejectedWith(EVMRevert); + await this.crowdsale.buyTokens(charlie, { value: capBob }); + await this.crowdsale.buyTokens(charlie, { value: 1 }).should.be.rejectedWith(EVMRevert); + }); + + it('should reject payments that exceed cap', async function () { + await this.crowdsale.buyTokens(bob, { value: capBob.plus(1) }).should.be.rejectedWith(EVMRevert); + await this.crowdsale.buyTokens(charlie, { value: capBob.plus(1) }).should.be.rejectedWith(EVMRevert); + }); + }); + + describe('reporting state', function () { + it('should report correct cap', async function () { + let retrievedCapBob = await this.crowdsale.getUserCap(bob); + retrievedCapBob.should.be.bignumber.equal(capBob); + let retrievedCapCharlie = await this.crowdsale.getUserCap(charlie); + retrievedCapCharlie.should.be.bignumber.equal(capBob); + }); + }); + }); +}); diff --git a/test/crowdsale/MintedCrowdsale.test.js b/test/crowdsale/MintedCrowdsale.test.js new file mode 100644 index 000000000..362dc23af --- /dev/null +++ b/test/crowdsale/MintedCrowdsale.test.js @@ -0,0 +1,61 @@ +import ether from '../helpers/ether'; + +const BigNumber = web3.BigNumber; + +const should = require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const MintedCrowdsale = artifacts.require('MintedCrowdsaleImpl'); +const MintableToken = artifacts.require('MintableToken'); + +contract('MintedCrowdsale', function ([_, investor, wallet, purchaser]) { + const rate = new BigNumber(1000); + const value = ether(42); + + const expectedTokenAmount = rate.mul(value); + + beforeEach(async function () { + this.token = await MintableToken.new(); + this.crowdsale = await MintedCrowdsale.new(rate, wallet, this.token.address); + await this.token.transferOwnership(this.crowdsale.address); + }); + + describe('accepting payments', function () { + it('should be token owner', async function () { + const owner = await this.token.owner(); + owner.should.equal(this.crowdsale.address); + }); + + it('should accept payments', async function () { + await this.crowdsale.send(value).should.be.fulfilled; + await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }).should.be.fulfilled; + }); + }); + + describe('high-level purchase', function () { + it('should log purchase', async function () { + const { logs } = await this.crowdsale.sendTransaction({ value: value, from: investor }); + const event = logs.find(e => e.event === 'TokenPurchase'); + should.exist(event); + event.args.purchaser.should.equal(investor); + event.args.beneficiary.should.equal(investor); + event.args.value.should.be.bignumber.equal(value); + event.args.amount.should.be.bignumber.equal(expectedTokenAmount); + }); + + it('should assign tokens to sender', async function () { + await this.crowdsale.sendTransaction({ value: value, from: investor }); + let balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(expectedTokenAmount); + }); + + it('should forward funds to wallet', async function () { + const pre = web3.eth.getBalance(wallet); + await this.crowdsale.sendTransaction({ value, from: investor }); + const post = web3.eth.getBalance(wallet); + post.minus(pre).should.be.bignumber.equal(value); + }); + }); +}); diff --git a/test/crowdsale/PostDeliveryCrowdsale.test.js b/test/crowdsale/PostDeliveryCrowdsale.test.js new file mode 100644 index 000000000..478c8518e --- /dev/null +++ b/test/crowdsale/PostDeliveryCrowdsale.test.js @@ -0,0 +1,67 @@ +import { advanceBlock } from '../helpers/advanceToBlock'; +import { increaseTimeTo, duration } from '../helpers/increaseTime'; +import latestTime from '../helpers/latestTime'; +import EVMRevert from '../helpers/EVMRevert'; +import ether from '../helpers/ether'; + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const PostDeliveryCrowdsale = artifacts.require('PostDeliveryCrowdsaleImpl'); +const SimpleToken = artifacts.require('SimpleToken'); + +contract('PostDeliveryCrowdsale', function ([_, investor, wallet, purchaser]) { + const rate = new BigNumber(1); + const value = ether(42); + const tokenSupply = new BigNumber('1e22'); + + before(async function () { + // Advance to the next block to correctly read time in the solidity "now" function interpreted by testrpc + await advanceBlock(); + }); + + beforeEach(async function () { + this.openingTime = latestTime() + duration.weeks(1); + this.closingTime = this.openingTime + duration.weeks(1); + this.beforeEndTime = this.closingTime - duration.hours(1); + this.afterClosingTime = this.closingTime + duration.seconds(1); + this.token = await SimpleToken.new(); + this.crowdsale = await PostDeliveryCrowdsale.new( + this.openingTime, this.closingTime, rate, wallet, this.token.address + ); + await this.token.transfer(this.crowdsale.address, tokenSupply); + }); + + it('should not immediately assign tokens to beneficiary', async function () { + await increaseTimeTo(this.openingTime); + await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }); + const balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(0); + }); + + it('should not allow beneficiaries to withdraw tokens before crowdsale ends', async function () { + await increaseTimeTo(this.beforeEndTime); + await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }); + await this.crowdsale.withdrawTokens({ from: investor }).should.be.rejectedWith(EVMRevert); + }); + + it('should allow beneficiaries to withdraw tokens after crowdsale ends', async function () { + await increaseTimeTo(this.openingTime); + await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }); + await increaseTimeTo(this.afterClosingTime); + await this.crowdsale.withdrawTokens({ from: investor }).should.be.fulfilled; + }); + + it('should return the amount of tokens bought', async function () { + await increaseTimeTo(this.openingTime); + await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }); + await increaseTimeTo(this.afterClosingTime); + await this.crowdsale.withdrawTokens({ from: investor }); + const balance = await this.token.balanceOf(investor); + balance.should.be.bignumber.equal(value); + }); +}); diff --git a/test/crowdsale/RefundableCrowdsale.test.js b/test/crowdsale/RefundableCrowdsale.test.js index 63aedd2d6..a51380294 100644 --- a/test/crowdsale/RefundableCrowdsale.test.js +++ b/test/crowdsale/RefundableCrowdsale.test.js @@ -12,12 +12,13 @@ require('chai') .should(); const RefundableCrowdsale = artifacts.require('RefundableCrowdsaleImpl'); -const MintableToken = artifacts.require('MintableToken'); +const SimpleToken = artifacts.require('SimpleToken'); -contract('RefundableCrowdsale', function ([_, owner, wallet, investor]) { - const rate = new BigNumber(1000); - const goal = ether(800); - const lessThanGoal = ether(750); +contract('RefundableCrowdsale', function ([_, owner, wallet, investor, purchaser]) { + const rate = new BigNumber(1); + const goal = ether(50); + const lessThanGoal = ether(45); + const tokenSupply = new BigNumber('1e22'); before(async function () { // Advance to the next block to correctly read time in the solidity "now" function interpreted by testrpc @@ -25,61 +26,57 @@ contract('RefundableCrowdsale', function ([_, owner, wallet, investor]) { }); beforeEach(async function () { - this.startTime = latestTime() + duration.weeks(1); - this.endTime = this.startTime + duration.weeks(1); - this.afterEndTime = this.endTime + duration.seconds(1); + this.openingTime = latestTime() + duration.weeks(1); + this.closingTime = this.openingTime + duration.weeks(1); + this.afterClosingTime = this.closingTime + duration.seconds(1); - this.token = await MintableToken.new(); + this.token = await SimpleToken.new(); this.crowdsale = await RefundableCrowdsale.new( - this.startTime, this.endTime, rate, wallet, goal, this.token.address, { from: owner } + this.openingTime, this.closingTime, rate, wallet, this.token.address, goal, { from: owner } ); - await this.token.transferOwnership(this.crowdsale.address); + await this.token.transfer(this.crowdsale.address, tokenSupply); }); describe('creating a valid crowdsale', function () { it('should fail with zero goal', async function () { - await RefundableCrowdsale.new(this.startTime, this.endTime, rate, wallet, 0, { from: owner }) - .should.be.rejectedWith(EVMRevert); + await RefundableCrowdsale.new( + this.openingTime, this.closingTime, rate, wallet, this.token.address, 0, { from: owner } + ).should.be.rejectedWith(EVMRevert); }); }); it('should deny refunds before end', async function () { await this.crowdsale.claimRefund({ from: investor }).should.be.rejectedWith(EVMRevert); - await increaseTimeTo(this.startTime); + await increaseTimeTo(this.openingTime); await this.crowdsale.claimRefund({ from: investor }).should.be.rejectedWith(EVMRevert); }); it('should deny refunds after end if goal was reached', async function () { - await increaseTimeTo(this.startTime); + await increaseTimeTo(this.openingTime); await this.crowdsale.sendTransaction({ value: goal, from: investor }); - await increaseTimeTo(this.afterEndTime); + await increaseTimeTo(this.afterClosingTime); await this.crowdsale.claimRefund({ from: investor }).should.be.rejectedWith(EVMRevert); }); it('should allow refunds after end if goal was not reached', async function () { - await increaseTimeTo(this.startTime); + await increaseTimeTo(this.openingTime); await this.crowdsale.sendTransaction({ value: lessThanGoal, from: investor }); - await increaseTimeTo(this.afterEndTime); - + await increaseTimeTo(this.afterClosingTime); await this.crowdsale.finalize({ from: owner }); - const pre = web3.eth.getBalance(investor); await this.crowdsale.claimRefund({ from: investor, gasPrice: 0 }) .should.be.fulfilled; const post = web3.eth.getBalance(investor); - post.minus(pre).should.be.bignumber.equal(lessThanGoal); }); it('should forward funds to wallet after end if goal was reached', async function () { - await increaseTimeTo(this.startTime); + await increaseTimeTo(this.openingTime); await this.crowdsale.sendTransaction({ value: goal, from: investor }); - await increaseTimeTo(this.afterEndTime); - + await increaseTimeTo(this.afterClosingTime); const pre = web3.eth.getBalance(wallet); await this.crowdsale.finalize({ from: owner }); const post = web3.eth.getBalance(wallet); - post.minus(pre).should.be.bignumber.equal(goal); }); }); diff --git a/test/crowdsale/TimedCrowdsale.test.js b/test/crowdsale/TimedCrowdsale.test.js new file mode 100644 index 000000000..fcc5d5adf --- /dev/null +++ b/test/crowdsale/TimedCrowdsale.test.js @@ -0,0 +1,62 @@ +import ether from '../helpers/ether'; +import { advanceBlock } from '../helpers/advanceToBlock'; +import { increaseTimeTo, duration } from '../helpers/increaseTime'; +import latestTime from '../helpers/latestTime'; +import EVMRevert from '../helpers/EVMRevert'; + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-as-promised')) + .use(require('chai-bignumber')(BigNumber)) + .should(); + +const TimedCrowdsale = artifacts.require('TimedCrowdsaleImpl'); +const SimpleToken = artifacts.require('SimpleToken'); + +contract('TimedCrowdsale', function ([_, investor, wallet, purchaser]) { + const rate = new BigNumber(1); + const value = ether(42); + const tokenSupply = new BigNumber('1e22'); + + before(async function () { + // Advance to the next block to correctly read time in the solidity "now" function interpreted by testrpc + await advanceBlock(); + }); + + beforeEach(async function () { + this.openingTime = latestTime() + duration.weeks(1); + this.closingTime = this.openingTime + duration.weeks(1); + this.afterClosingTime = this.closingTime + duration.seconds(1); + this.token = await SimpleToken.new(); + this.crowdsale = await TimedCrowdsale.new(this.openingTime, this.closingTime, rate, wallet, this.token.address); + await this.token.transfer(this.crowdsale.address, tokenSupply); + }); + + it('should be ended only after end', async function () { + let ended = await this.crowdsale.hasClosed(); + ended.should.equal(false); + await increaseTimeTo(this.afterClosingTime); + ended = await this.crowdsale.hasClosed(); + ended.should.equal(true); + }); + + describe('accepting payments', function () { + it('should reject payments before start', async function () { + await this.crowdsale.send(value).should.be.rejectedWith(EVMRevert); + await this.crowdsale.buyTokens(investor, { from: purchaser, value: value }).should.be.rejectedWith(EVMRevert); + }); + + it('should accept payments after start', async function () { + await increaseTimeTo(this.openingTime); + await this.crowdsale.send(value).should.be.fulfilled; + await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }).should.be.fulfilled; + }); + + it('should reject payments after end', async function () { + await increaseTimeTo(this.afterClosingTime); + await this.crowdsale.send(value).should.be.rejectedWith(EVMRevert); + await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }).should.be.rejectedWith(EVMRevert); + }); + }); +}); diff --git a/test/crowdsale/WhitelistedCrowdsale.test.js b/test/crowdsale/WhitelistedCrowdsale.test.js new file mode 100644 index 000000000..e62016ace --- /dev/null +++ b/test/crowdsale/WhitelistedCrowdsale.test.js @@ -0,0 +1,93 @@ +import ether from '../helpers/ether'; + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-as-promised')) + .should(); + +const WhitelistedCrowdsale = artifacts.require('WhitelistedCrowdsaleImpl'); +const SimpleToken = artifacts.require('SimpleToken'); + +contract('WhitelistedCrowdsale', function ([_, wallet, authorized, unauthorized, anotherAuthorized]) { + const rate = 1; + const value = ether(42); + const tokenSupply = new BigNumber('1e22'); + + describe('single user whitelisting', function () { + beforeEach(async function () { + this.token = await SimpleToken.new(); + this.crowdsale = await WhitelistedCrowdsale.new(rate, wallet, this.token.address); + await this.token.transfer(this.crowdsale.address, tokenSupply); + await this.crowdsale.addToWhitelist(authorized); + }); + + describe('accepting payments', function () { + it('should accept payments to whitelisted (from whichever buyers)', async function () { + await this.crowdsale.buyTokens(authorized, { value: value, from: authorized }).should.be.fulfilled; + await this.crowdsale.buyTokens(authorized, { value: value, from: unauthorized }).should.be.fulfilled; + }); + + it('should reject payments to not whitelisted (from whichever buyers)', async function () { + await this.crowdsale.send(value).should.be.rejected; + await this.crowdsale.buyTokens(unauthorized, { value: value, from: unauthorized }).should.be.rejected; + await this.crowdsale.buyTokens(unauthorized, { value: value, from: authorized }).should.be.rejected; + }); + + it('should reject payments to addresses removed from whitelist', async function () { + await this.crowdsale.removeFromWhitelist(authorized); + await this.crowdsale.buyTokens(authorized, { value: value, from: authorized }).should.be.rejected; + }); + }); + + describe('reporting whitelisted', function () { + it('should correctly report whitelisted addresses', async function () { + let isAuthorized = await this.crowdsale.whitelist(authorized); + isAuthorized.should.equal(true); + let isntAuthorized = await this.crowdsale.whitelist(unauthorized); + isntAuthorized.should.equal(false); + }); + }); + }); + + describe('many user whitelisting', function () { + beforeEach(async function () { + this.token = await SimpleToken.new(); + this.crowdsale = await WhitelistedCrowdsale.new(rate, wallet, this.token.address); + await this.token.transfer(this.crowdsale.address, tokenSupply); + await this.crowdsale.addManyToWhitelist([authorized, anotherAuthorized]); + }); + + describe('accepting payments', function () { + it('should accept payments to whitelisted (from whichever buyers)', async function () { + await this.crowdsale.buyTokens(authorized, { value: value, from: authorized }).should.be.fulfilled; + await this.crowdsale.buyTokens(authorized, { value: value, from: unauthorized }).should.be.fulfilled; + await this.crowdsale.buyTokens(anotherAuthorized, { value: value, from: authorized }).should.be.fulfilled; + await this.crowdsale.buyTokens(anotherAuthorized, { value: value, from: unauthorized }).should.be.fulfilled; + }); + + it('should reject payments to not whitelisted (with whichever buyers)', async function () { + await this.crowdsale.send(value).should.be.rejected; + await this.crowdsale.buyTokens(unauthorized, { value: value, from: unauthorized }).should.be.rejected; + await this.crowdsale.buyTokens(unauthorized, { value: value, from: authorized }).should.be.rejected; + }); + + it('should reject payments to addresses removed from whitelist', async function () { + await this.crowdsale.removeFromWhitelist(anotherAuthorized); + await this.crowdsale.buyTokens(authorized, { value: value, from: authorized }).should.be.fulfilled; + await this.crowdsale.buyTokens(anotherAuthorized, { value: value, from: authorized }).should.be.rejected; + }); + }); + + describe('reporting whitelisted', function () { + it('should correctly report whitelisted addresses', async function () { + let isAuthorized = await this.crowdsale.whitelist(authorized); + isAuthorized.should.equal(true); + let isAnotherAuthorized = await this.crowdsale.whitelist(anotherAuthorized); + isAnotherAuthorized.should.equal(true); + let isntAuthorized = await this.crowdsale.whitelist(unauthorized); + isntAuthorized.should.equal(false); + }); + }); + }); +}); diff --git a/test/examples/SampleCrowdsale.test.js b/test/examples/SampleCrowdsale.test.js index d14807eac..132256abc 100644 --- a/test/examples/SampleCrowdsale.test.js +++ b/test/examples/SampleCrowdsale.test.js @@ -13,6 +13,7 @@ require('chai') const SampleCrowdsale = artifacts.require('SampleCrowdsale'); const SampleCrowdsaleToken = artifacts.require('SampleCrowdsaleToken'); +const RefundVault = artifacts.require('RefundVault'); contract('SampleCrowdsale', function ([owner, wallet, investor]) { const RATE = new BigNumber(10); @@ -25,30 +26,32 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) { }); beforeEach(async function () { - this.startTime = latestTime() + duration.weeks(1); - this.endTime = this.startTime + duration.weeks(1); - this.afterEndTime = this.endTime + duration.seconds(1); + this.openingTime = latestTime() + duration.weeks(1); + this.closingTime = this.openingTime + duration.weeks(1); + this.afterClosingTime = this.closingTime + duration.seconds(1); - this.token = await SampleCrowdsaleToken.new(); + this.token = await SampleCrowdsaleToken.new({ from: owner }); + this.vault = await RefundVault.new(wallet, { from: owner }); this.crowdsale = await SampleCrowdsale.new( - this.startTime, this.endTime, RATE, GOAL, CAP, wallet, this.token.address + this.openingTime, this.closingTime, RATE, wallet, CAP, this.token.address, GOAL ); await this.token.transferOwnership(this.crowdsale.address); + await this.vault.transferOwnership(this.crowdsale.address); }); it('should create crowdsale with correct parameters', async function () { this.crowdsale.should.exist; this.token.should.exist; - const startTime = await this.crowdsale.startTime(); - const endTime = await this.crowdsale.endTime(); + const openingTime = await this.crowdsale.openingTime(); + const closingTime = await this.crowdsale.closingTime(); const rate = await this.crowdsale.rate(); const walletAddress = await this.crowdsale.wallet(); const goal = await this.crowdsale.goal(); const cap = await this.crowdsale.cap(); - startTime.should.be.bignumber.equal(this.startTime); - endTime.should.be.bignumber.equal(this.endTime); + openingTime.should.be.bignumber.equal(this.openingTime); + closingTime.should.be.bignumber.equal(this.closingTime); rate.should.be.bignumber.equal(RATE); walletAddress.should.be.equal(wallet); goal.should.be.bignumber.equal(GOAL); @@ -64,7 +67,7 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) { const investmentAmount = ether(1); const expectedTokenAmount = RATE.mul(investmentAmount); - await increaseTimeTo(this.startTime); + await increaseTimeTo(this.openingTime); await this.crowdsale.buyTokens(investor, { value: investmentAmount, from: investor }).should.be.fulfilled; (await this.token.balanceOf(investor)).should.be.bignumber.equal(expectedTokenAmount); @@ -78,17 +81,17 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) { }); it('should reject payments over cap', async function () { - await increaseTimeTo(this.startTime); + await increaseTimeTo(this.openingTime); await this.crowdsale.send(CAP); await this.crowdsale.send(1).should.be.rejectedWith(EVMRevert); }); it('should allow finalization and transfer funds to wallet if the goal is reached', async function () { - await increaseTimeTo(this.startTime); + await increaseTimeTo(this.openingTime); await this.crowdsale.send(GOAL); const beforeFinalization = web3.eth.getBalance(wallet); - await increaseTimeTo(this.afterEndTime); + await increaseTimeTo(this.afterClosingTime); await this.crowdsale.finalize({ from: owner }); const afterFinalization = web3.eth.getBalance(wallet); @@ -98,9 +101,9 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) { it('should allow refunds if the goal is not reached', async function () { const balanceBeforeInvestment = web3.eth.getBalance(investor); - await increaseTimeTo(this.startTime); + await increaseTimeTo(this.openingTime); await this.crowdsale.sendTransaction({ value: ether(1), from: investor, gasPrice: 0 }); - await increaseTimeTo(this.afterEndTime); + await increaseTimeTo(this.afterClosingTime); await this.crowdsale.finalize({ from: owner }); await this.crowdsale.claimRefund({ from: investor, gasPrice: 0 }).should.be.fulfilled;