Sfoglia il codice sorgente

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
Alejo Salles 7 anni fa
parent
commit
c05918c3cc
36 ha cambiato i file con 1319 aggiunte e 283 eliminazioni
  1. 0 35
      contracts/crowdsale/CappedCrowdsale.sol
  2. 99 48
      contracts/crowdsale/Crowdsale.sol
  3. 5 6
      contracts/crowdsale/distribution/FinalizableCrowdsale.sol
  4. 35 0
      contracts/crowdsale/distribution/PostDeliveryCrowdsale.sol
  5. 20 8
      contracts/crowdsale/distribution/RefundableCrowdsale.sol
  6. 11 2
      contracts/crowdsale/distribution/utils/RefundVault.sol
  7. 42 0
      contracts/crowdsale/emission/AllowanceCrowdsale.sol
  8. 22 0
      contracts/crowdsale/emission/MintedCrowdsale.sol
  9. 52 0
      contracts/crowdsale/price/IncreasingPriceCrowdsale.sol
  10. 43 0
      contracts/crowdsale/validation/CappedCrowdsale.sol
  11. 76 0
      contracts/crowdsale/validation/IndividuallyCappedCrowdsale.sol
  12. 55 0
      contracts/crowdsale/validation/TimedCrowdsale.sol
  13. 58 0
      contracts/crowdsale/validation/WhitelistedCrowdsale.sol
  14. 7 6
      contracts/examples/SampleCrowdsale.sol
  15. 21 0
      contracts/mocks/AllowanceCrowdsaleImpl.sol
  16. 7 8
      contracts/mocks/CappedCrowdsaleImpl.sol
  17. 8 6
      contracts/mocks/FinalizableCrowdsaleImpl.sol
  18. 24 0
      contracts/mocks/IncreasingPriceCrowdsaleImpl.sol
  19. 19 0
      contracts/mocks/IndividuallyCappedCrowdsaleImpl.sol
  20. 19 0
      contracts/mocks/MintedCrowdsaleImpl.sol
  21. 22 0
      contracts/mocks/PostDeliveryCrowdsaleImpl.sol
  22. 10 9
      contracts/mocks/RefundableCrowdsaleImpl.sol
  23. 21 0
      contracts/mocks/TimedCrowdsaleImpl.sol
  24. 19 0
      contracts/mocks/WhitelistedCrowdsaleImpl.sol
  25. 68 0
      test/crowdsale/AllowanceCrowdsale.test.js
  26. 19 38
      test/crowdsale/CappedCrowdsale.test.js
  27. 7 69
      test/crowdsale/Crowdsale.test.js
  28. 8 8
      test/crowdsale/FinalizableCrowdsale.test.js
  29. 92 0
      test/crowdsale/IncreasingPriceCrowdsale.test.js
  30. 107 0
      test/crowdsale/IndividuallyCappedCrowdsale.test.js
  31. 61 0
      test/crowdsale/MintedCrowdsale.test.js
  32. 67 0
      test/crowdsale/PostDeliveryCrowdsale.test.js
  33. 22 25
      test/crowdsale/RefundableCrowdsale.test.js
  34. 62 0
      test/crowdsale/TimedCrowdsale.test.js
  35. 93 0
      test/crowdsale/WhitelistedCrowdsale.test.js
  36. 18 15
      test/examples/SampleCrowdsale.test.js

+ 0 - 35
contracts/crowdsale/CappedCrowdsale.sol

@@ -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();
-  }
-
-}

+ 99 - 48
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;
-
-  // start and end timestamps where investments are allowed (both inclusive)
-  uint256 public startTime;
-  uint256 public endTime;
+  ERC20 public token;
 
-  // 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);
+
+    _updatePurchasingState(_beneficiary, weiAmount);
+
+    _forwardFunds();
+    _postValidatePurchase(_beneficiary, weiAmount);
+  }
+
+  // -----------------------------------------
+  // 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);
+  }
 
-    forwardFunds();
+  /**
+   * @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
   }
 
-  // @return true if crowdsale event has ended
-  function hasEnded() public view returns (bool) {
-    return now > endTime;
+  /**
+   * @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);
   }
 
-  // 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 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);
   }
 
-  // send ether to the fund collection wallet
-  // override to create custom fund forwarding mechanisms
-  function forwardFunds() internal {
-    wallet.transfer(msg.value);
+  /**
+   * @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
   }
 
-  // @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;
+  /**
+   * @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);
+  }
 }

+ 5 - 6
contracts/crowdsale/FinalizableCrowdsale.sol → 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();

+ 35 - 0
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);
+  }
+}

+ 20 - 8
contracts/crowdsale/RefundableCrowdsale.sol → 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);
   }
 

+ 11 - 2
contracts/crowdsale/RefundVault.sol → 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];

+ 42 - 0
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);
+  }
+}

+ 22 - 0
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));
+  }
+}

+ 52 - 0
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);
+  }
+
+}

+ 43 - 0
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);
+  }
+
+}

+ 76 - 0
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);
+  }
+
+}

+ 55 - 0
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);
+  }
+
+}

+ 58 - 0
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);
+  }
+
+}

+ 7 - 6
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

+ 21 - 0
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)
+  {
+  }
+
+}

+ 7 - 8
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)
   {
   }

+ 8 - 6
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)
   {
   }
 

+ 24 - 0
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)
+  {
+  }
+
+}

+ 19 - 0
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)
+  {
+  }
+
+}

+ 19 - 0
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)
+  {
+  }
+
+}

+ 22 - 0
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)
+  {
+  }
+
+}

+ 10 - 9
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)
   {
   }

+ 21 - 0
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)
+  {
+  }
+
+}

+ 19 - 0
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)
+  {
+  }
+
+}

+ 68 - 0
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);
+    });
+  });
+});

+ 19 - 38
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);
     });
   });
 });

+ 7 - 69
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);

+ 8 - 8
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);

+ 92 - 0
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));
+    });
+  });
+});

+ 107 - 0
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);
+      });
+    });
+  });
+});

+ 61 - 0
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);
+    });
+  });
+});

+ 67 - 0
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);
+  });
+});

+ 22 - 25
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);
   });
 });

+ 62 - 0
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);
+    });
+  });
+});

+ 93 - 0
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);
+      });
+    });
+  });
+});

+ 18 - 15
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;