Commit c05918c3 by Alejo Salles Committed by Manuel Aráoz

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
parent 108d5f3b
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();
}
}
pragma solidity ^0.4.18; pragma solidity ^0.4.18;
import "../token/ERC20/MintableToken.sol"; import "../token/ERC20/ERC20.sol";
import "../math/SafeMath.sol"; import "../math/SafeMath.sol";
/** /**
* @title Crowdsale * @title Crowdsale
* @dev Crowdsale is a base contract for managing a token crowdsale. * @dev Crowdsale is a base contract for managing a token crowdsale,
* Crowdsales have a start and end timestamps, where investors can make * allowing investors to purchase tokens with ether. This contract implements
* token purchases and the crowdsale will assign them tokens based * such functionality in its most fundamental form and can be extended to provide additional
* on a token per ETH rate. Funds collected are forwarded to a wallet * functionality and/or custom behavior.
* as they arrive. The contract requires a MintableToken that will be * The external interface represents the basic interface for purchasing tokens, and conform
* minted as contributions arrive, note that the crowdsale contract * the base architecture for crowdsales. They are *not* intended to be modified / overriden.
* must be owner of the token in order to be able to mint it. * 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 { contract Crowdsale {
using SafeMath for uint256; using SafeMath for uint256;
// The token being sold // 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; address public wallet;
// how many token units a buyer gets per wei // How many token units a buyer gets per wei
uint256 public rate; uint256 public rate;
// amount of raised money in wei // Amount of wei raised
uint256 public weiRaised; uint256 public weiRaised;
/** /**
* event for token purchase logging * Event for token purchase logging
* @param purchaser who paid for the tokens * @param purchaser who paid for the tokens
* @param beneficiary who got the tokens * @param beneficiary who got the tokens
* @param value weis paid for purchase * @param value weis paid for purchase
...@@ -42,66 +40,119 @@ contract Crowdsale { ...@@ -42,66 +40,119 @@ contract Crowdsale {
*/ */
event TokenPurchase(address indexed purchaser, address indexed beneficiary, uint256 value, uint256 amount); 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 { * @param _rate Number of token units a buyer gets per wei
require(_startTime >= now); * @param _wallet Address where collected funds will be forwarded to
require(_endTime >= _startTime); * @param _token Address of the token being sold
*/
function Crowdsale(uint256 _rate, address _wallet, ERC20 _token) public {
require(_rate > 0); require(_rate > 0);
require(_wallet != address(0)); require(_wallet != address(0));
require(_token != address(0)); require(_token != address(0));
startTime = _startTime;
endTime = _endTime;
rate = _rate; rate = _rate;
wallet = _wallet; wallet = _wallet;
token = _token; token = _token;
} }
// fallback function can be used to buy tokens // -----------------------------------------
// Crowdsale external interface
// -----------------------------------------
/**
* @dev fallback function ***DO NOT OVERRIDE***
*/
function () external payable { function () external payable {
buyTokens(msg.sender); buyTokens(msg.sender);
} }
// low level token purchase function /**
function buyTokens(address beneficiary) public payable { * @dev low level token purchase ***DO NOT OVERRIDE***
require(beneficiary != address(0)); * @param _beneficiary Address performing the token purchase
require(validPurchase()); */
function buyTokens(address _beneficiary) public payable {
uint256 weiAmount = msg.value; uint256 weiAmount = msg.value;
_preValidatePurchase(_beneficiary, weiAmount);
// calculate token amount to be created // calculate token amount to be created
uint256 tokens = getTokenAmount(weiAmount); uint256 tokens = _getTokenAmount(weiAmount);
// update state // update state
weiRaised = weiRaised.add(weiAmount); weiRaised = weiRaised.add(weiAmount);
token.mint(beneficiary, tokens); _processPurchase(_beneficiary, tokens);
TokenPurchase(msg.sender, beneficiary, weiAmount, 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) { * @dev Source of tokens. Override this method to modify the way in which the crowdsale ultimately gets and sends its tokens.
return now > endTime; * @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) { * @dev Executed when a purchase has been validated and is ready to be executed. Not necessarily emits/sends tokens.
return weiAmount.mul(rate); * @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 * @dev Override for extensions that require an internal state to check for validity (current user contributions, etc.)
function forwardFunds() internal { * @param _beneficiary Address receiving the tokens
wallet.transfer(msg.value); * @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) { * @dev Override to extend the way in which ether is converted to tokens.
bool withinPeriod = now >= startTime && now <= endTime; * @param _weiAmount Value in wei to be converted into tokens
bool nonZeroPurchase = msg.value != 0; * @return Number of tokens that can be purchased with the specified _weiAmount
return withinPeriod && nonZeroPurchase; */
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);
}
} }
pragma solidity ^0.4.18; pragma solidity ^0.4.18;
import "../math/SafeMath.sol"; import "../../math/SafeMath.sol";
import "../ownership/Ownable.sol"; import "../../ownership/Ownable.sol";
import "./Crowdsale.sol"; import "../validation/TimedCrowdsale.sol";
/** /**
* @title FinalizableCrowdsale * @title FinalizableCrowdsale
* @dev Extension of Crowdsale where an owner can do extra work * @dev Extension of Crowdsale where an owner can do extra work
* after finishing. * after finishing.
*/ */
contract FinalizableCrowdsale is Crowdsale, Ownable { contract FinalizableCrowdsale is TimedCrowdsale, Ownable {
using SafeMath for uint256; using SafeMath for uint256;
bool public isFinalized = false; bool public isFinalized = false;
...@@ -23,7 +22,7 @@ contract FinalizableCrowdsale is Crowdsale, Ownable { ...@@ -23,7 +22,7 @@ contract FinalizableCrowdsale is Crowdsale, Ownable {
*/ */
function finalize() onlyOwner public { function finalize() onlyOwner public {
require(!isFinalized); require(!isFinalized);
require(hasEnded()); require(hasClosed());
finalization(); finalization();
Finalized(); Finalized();
......
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);
}
}
pragma solidity ^0.4.18; pragma solidity ^0.4.18;
import "../math/SafeMath.sol"; import "../../math/SafeMath.sol";
import "./FinalizableCrowdsale.sol"; import "./FinalizableCrowdsale.sol";
import "./RefundVault.sol"; import "./utils/RefundVault.sol";
/** /**
...@@ -21,13 +21,19 @@ contract RefundableCrowdsale is FinalizableCrowdsale { ...@@ -21,13 +21,19 @@ contract RefundableCrowdsale is FinalizableCrowdsale {
// refund vault used to hold funds while crowdsale is running // refund vault used to hold funds while crowdsale is running
RefundVault public vault; RefundVault public vault;
/**
* @dev Constructor, creates RefundVault.
* @param _goal Funding goal
*/
function RefundableCrowdsale(uint256 _goal) public { function RefundableCrowdsale(uint256 _goal) public {
require(_goal > 0); require(_goal > 0);
vault = new RefundVault(wallet); vault = new RefundVault(wallet);
goal = _goal; goal = _goal;
} }
// if crowdsale is unsuccessful, investors can claim refunds here /**
* @dev Investors can claim refunds here if crowdsale is unsuccessful
*/
function claimRefund() public { function claimRefund() public {
require(isFinalized); require(isFinalized);
require(!goalReached()); require(!goalReached());
...@@ -35,11 +41,17 @@ contract RefundableCrowdsale is FinalizableCrowdsale { ...@@ -35,11 +41,17 @@ contract RefundableCrowdsale is FinalizableCrowdsale {
vault.refund(msg.sender); vault.refund(msg.sender);
} }
/**
* @dev Checks whether funding goal was reached.
* @return Whether funding goal was reached
*/
function goalReached() public view returns (bool) { function goalReached() public view returns (bool) {
return weiRaised >= goal; return weiRaised >= goal;
} }
// vault finalization task, called when owner calls finalize() /**
* @dev vault finalization task, called when owner calls finalize()
*/
function finalization() internal { function finalization() internal {
if (goalReached()) { if (goalReached()) {
vault.close(); vault.close();
...@@ -50,10 +62,10 @@ contract RefundableCrowdsale is FinalizableCrowdsale { ...@@ -50,10 +62,10 @@ contract RefundableCrowdsale is FinalizableCrowdsale {
super.finalization(); super.finalization();
} }
// We're overriding the fund forwarding from Crowdsale. /**
// In addition to sending the funds, we want to call * @dev Overrides Crowdsale fund forwarding, sending funds to vault.
// the RefundVault deposit function */
function forwardFunds() internal { function _forwardFunds() internal {
vault.deposit.value(msg.value)(msg.sender); vault.deposit.value(msg.value)(msg.sender);
} }
......
pragma solidity ^0.4.18; pragma solidity ^0.4.18;
import "../math/SafeMath.sol"; import "../../../math/SafeMath.sol";
import "../ownership/Ownable.sol"; import "../../../ownership/Ownable.sol";
/** /**
...@@ -23,12 +23,18 @@ contract RefundVault is Ownable { ...@@ -23,12 +23,18 @@ contract RefundVault is Ownable {
event RefundsEnabled(); event RefundsEnabled();
event Refunded(address indexed beneficiary, uint256 weiAmount); event Refunded(address indexed beneficiary, uint256 weiAmount);
/**
* @param _wallet Vault address
*/
function RefundVault(address _wallet) public { function RefundVault(address _wallet) public {
require(_wallet != address(0)); require(_wallet != address(0));
wallet = _wallet; wallet = _wallet;
state = State.Active; state = State.Active;
} }
/**
* @param investor Investor address
*/
function deposit(address investor) onlyOwner public payable { function deposit(address investor) onlyOwner public payable {
require(state == State.Active); require(state == State.Active);
deposited[investor] = deposited[investor].add(msg.value); deposited[investor] = deposited[investor].add(msg.value);
...@@ -47,6 +53,9 @@ contract RefundVault is Ownable { ...@@ -47,6 +53,9 @@ contract RefundVault is Ownable {
RefundsEnabled(); RefundsEnabled();
} }
/**
* @param investor Investor address
*/
function refund(address investor) public { function refund(address investor) public {
require(state == State.Refunding); require(state == State.Refunding);
uint256 depositedValue = deposited[investor]; uint256 depositedValue = deposited[investor];
......
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);
}
}
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));
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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);
}
}
pragma solidity ^0.4.18; pragma solidity ^0.4.18;
import "../crowdsale/CappedCrowdsale.sol"; import "../crowdsale/validation/CappedCrowdsale.sol";
import "../crowdsale/RefundableCrowdsale.sol"; import "../crowdsale/distribution/RefundableCrowdsale.sol";
import "../crowdsale/emission/MintedCrowdsale.sol";
import "../token/ERC20/MintableToken.sol"; import "../token/ERC20/MintableToken.sol";
...@@ -30,13 +31,13 @@ contract SampleCrowdsaleToken is MintableToken { ...@@ -30,13 +31,13 @@ contract SampleCrowdsaleToken is MintableToken {
* After adding multiple features it's good practice to run integration tests * After adding multiple features it's good practice to run integration tests
* to ensure that subcontracts works together as intended. * 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) CappedCrowdsale(_cap)
FinalizableCrowdsale() TimedCrowdsale(_openingTime, _closingTime)
RefundableCrowdsale(_goal) RefundableCrowdsale(_goal)
Crowdsale(_startTime, _endTime, _rate, _wallet, _token)
{ {
//As goal needs to be met for a successful crowdsale //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 //the value needs to less or equal than a cap which is limit for accepted funds
......
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)
{
}
}
pragma solidity ^0.4.18; pragma solidity ^0.4.18;
import "../token/ERC20/ERC20.sol";
import "../crowdsale/CappedCrowdsale.sol"; import "../crowdsale/validation/CappedCrowdsale.sol";
contract CappedCrowdsaleImpl is CappedCrowdsale { contract CappedCrowdsaleImpl is CappedCrowdsale {
function CappedCrowdsaleImpl ( function CappedCrowdsaleImpl (
uint256 _startTime,
uint256 _endTime,
uint256 _rate, uint256 _rate,
address _wallet, address _wallet,
uint256 _cap, ERC20 _token,
MintableToken _token uint256 _cap
) public )
Crowdsale(_startTime, _endTime, _rate, _wallet, _token) public
Crowdsale(_rate, _wallet, _token)
CappedCrowdsale(_cap) CappedCrowdsale(_cap)
{ {
} }
......
pragma solidity ^0.4.18; pragma solidity ^0.4.18;
import "../token/ERC20/MintableToken.sol";
import "../crowdsale/FinalizableCrowdsale.sol"; import "../crowdsale/distribution/FinalizableCrowdsale.sol";
contract FinalizableCrowdsaleImpl is FinalizableCrowdsale { contract FinalizableCrowdsaleImpl is FinalizableCrowdsale {
function FinalizableCrowdsaleImpl ( function FinalizableCrowdsaleImpl (
uint256 _startTime, uint256 _openingTime,
uint256 _endTime, uint256 _closingTime,
uint256 _rate, uint256 _rate,
address _wallet, address _wallet,
MintableToken _token MintableToken _token
) public )
Crowdsale(_startTime, _endTime, _rate, _wallet, _token) public
Crowdsale(_rate, _wallet, _token)
TimedCrowdsale(_openingTime, _closingTime)
{ {
} }
......
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)
{
}
}
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)
{
}
}
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)
{
}
}
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)
{
}
}
pragma solidity ^0.4.18; pragma solidity ^0.4.18;
import "../token/ERC20/MintableToken.sol";
import "../crowdsale/RefundableCrowdsale.sol"; import "../crowdsale/distribution/RefundableCrowdsale.sol";
contract RefundableCrowdsaleImpl is RefundableCrowdsale { contract RefundableCrowdsaleImpl is RefundableCrowdsale {
function RefundableCrowdsaleImpl ( function RefundableCrowdsaleImpl (
uint256 _startTime, uint256 _openingTime,
uint256 _endTime, uint256 _closingTime,
uint256 _rate, uint256 _rate,
address _wallet, address _wallet,
uint256 _goal, MintableToken _token,
MintableToken _token uint256 _goal
) public )
Crowdsale(_startTime, _endTime, _rate, _wallet, _token) public
Crowdsale(_rate, _wallet, _token)
TimedCrowdsale(_openingTime, _closingTime)
RefundableCrowdsale(_goal) RefundableCrowdsale(_goal)
{ {
} }
......
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)
{
}
}
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)
{
}
}
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);
});
});
});
import ether from '../helpers/ether'; 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'; import EVMRevert from '../helpers/EVMRevert';
const BigNumber = web3.BigNumber; const BigNumber = web3.BigNumber;
...@@ -12,39 +9,27 @@ require('chai') ...@@ -12,39 +9,27 @@ require('chai')
.should(); .should();
const CappedCrowdsale = artifacts.require('CappedCrowdsaleImpl'); const CappedCrowdsale = artifacts.require('CappedCrowdsaleImpl');
const MintableToken = artifacts.require('MintableToken'); const SimpleToken = artifacts.require('SimpleToken');
contract('CappedCrowdsale', function ([_, wallet]) { contract('CappedCrowdsale', function ([_, wallet]) {
const rate = new BigNumber(1000); const rate = new BigNumber(1);
const cap = ether(100);
const cap = ether(300);
const lessThanCap = ether(60); const lessThanCap = ether(60);
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 () { beforeEach(async function () {
this.startTime = latestTime() + duration.weeks(1); this.token = await SimpleToken.new();
this.endTime = this.startTime + duration.weeks(1); this.crowdsale = await CappedCrowdsale.new(rate, wallet, this.token.address, cap);
this.token.transfer(this.crowdsale.address, tokenSupply);
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);
}); });
describe('creating a valid crowdsale', function () { describe('creating a valid crowdsale', function () {
it('should fail with zero cap', async 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 () { describe('accepting payments', function () {
beforeEach(async function () {
await increaseTimeTo(this.startTime);
});
it('should accept payments within cap', async function () { it('should accept payments within cap', async function () {
await this.crowdsale.send(cap.minus(lessThanCap)).should.be.fulfilled; await this.crowdsale.send(cap.minus(lessThanCap)).should.be.fulfilled;
await this.crowdsale.send(lessThanCap).should.be.fulfilled; await this.crowdsale.send(lessThanCap).should.be.fulfilled;
...@@ -61,28 +46,24 @@ contract('CappedCrowdsale', function ([_, wallet]) { ...@@ -61,28 +46,24 @@ contract('CappedCrowdsale', function ([_, wallet]) {
}); });
describe('ending', function () { describe('ending', function () {
beforeEach(async function () { it('should not reach cap if sent under cap', async function () {
await increaseTimeTo(this.startTime); let capReached = await this.crowdsale.capReached();
}); capReached.should.equal(false);
it('should not be ended if under cap', async function () {
let hasEnded = await this.crowdsale.hasEnded();
hasEnded.should.equal(false);
await this.crowdsale.send(lessThanCap); await this.crowdsale.send(lessThanCap);
hasEnded = await this.crowdsale.hasEnded(); capReached = await this.crowdsale.capReached();
hasEnded.should.equal(false); 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)); await this.crowdsale.send(cap.minus(1));
let hasEnded = await this.crowdsale.hasEnded(); let capReached = await this.crowdsale.capReached();
hasEnded.should.equal(false); 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); await this.crowdsale.send(cap);
let hasEnded = await this.crowdsale.hasEnded(); let capReached = await this.crowdsale.capReached();
hasEnded.should.equal(true); capReached.should.equal(true);
}); });
}); });
}); });
import ether from '../helpers/ether'; 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; const BigNumber = web3.BigNumber;
...@@ -12,71 +8,31 @@ const should = require('chai') ...@@ -12,71 +8,31 @@ const should = require('chai')
.should(); .should();
const Crowdsale = artifacts.require('Crowdsale'); const Crowdsale = artifacts.require('Crowdsale');
const MintableToken = artifacts.require('MintableToken'); const SimpleToken = artifacts.require('SimpleToken');
contract('Crowdsale', function ([_, investor, wallet, purchaser]) { contract('Crowdsale', function ([_, investor, wallet, purchaser]) {
const rate = new BigNumber(1000); const rate = new BigNumber(1);
const value = ether(42); const value = ether(42);
const tokenSupply = new BigNumber('1e22');
const expectedTokenAmount = rate.mul(value); 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 () { beforeEach(async function () {
this.startTime = latestTime() + duration.weeks(1); this.token = await SimpleToken.new();
this.endTime = this.startTime + duration.weeks(1); this.crowdsale = await Crowdsale.new(rate, wallet, this.token.address);
this.afterEndTime = this.endTime + duration.seconds(1); await this.token.transfer(this.crowdsale.address, tokenSupply);
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);
}); });
describe('accepting payments', function () { describe('accepting payments', function () {
it('should reject payments before start', async function () { it('should accept payments', 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);
await this.crowdsale.send(value).should.be.fulfilled; await this.crowdsale.send(value).should.be.fulfilled;
await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }).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 () { describe('high-level purchase', function () {
beforeEach(async function () {
await increaseTimeTo(this.startTime);
});
it('should log purchase', async function () { it('should log purchase', async function () {
const { logs } = await this.crowdsale.sendTransaction({ value: value, from: investor }); const { logs } = await this.crowdsale.sendTransaction({ value: value, from: investor });
const event = logs.find(e => e.event === 'TokenPurchase'); const event = logs.find(e => e.event === 'TokenPurchase');
should.exist(event); should.exist(event);
event.args.purchaser.should.equal(investor); event.args.purchaser.should.equal(investor);
event.args.beneficiary.should.equal(investor); event.args.beneficiary.should.equal(investor);
...@@ -84,12 +40,6 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) { ...@@ -84,12 +40,6 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) {
event.args.amount.should.be.bignumber.equal(expectedTokenAmount); 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 () { it('should assign tokens to sender', async function () {
await this.crowdsale.sendTransaction({ value: value, from: investor }); await this.crowdsale.sendTransaction({ value: value, from: investor });
let balance = await this.token.balanceOf(investor); let balance = await this.token.balanceOf(investor);
...@@ -105,15 +55,9 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) { ...@@ -105,15 +55,9 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) {
}); });
describe('low-level purchase', function () { describe('low-level purchase', function () {
beforeEach(async function () {
await increaseTimeTo(this.startTime);
});
it('should log purchase', async function () { it('should log purchase', async function () {
const { logs } = await this.crowdsale.buyTokens(investor, { value: value, from: purchaser }); const { logs } = await this.crowdsale.buyTokens(investor, { value: value, from: purchaser });
const event = logs.find(e => e.event === 'TokenPurchase'); const event = logs.find(e => e.event === 'TokenPurchase');
should.exist(event); should.exist(event);
event.args.purchaser.should.equal(purchaser); event.args.purchaser.should.equal(purchaser);
event.args.beneficiary.should.equal(investor); event.args.beneficiary.should.equal(investor);
...@@ -121,12 +65,6 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) { ...@@ -121,12 +65,6 @@ contract('Crowdsale', function ([_, investor, wallet, purchaser]) {
event.args.amount.should.be.bignumber.equal(expectedTokenAmount); 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 () { it('should assign tokens to beneficiary', async function () {
await this.crowdsale.buyTokens(investor, { value, from: purchaser }); await this.crowdsale.buyTokens(investor, { value, from: purchaser });
const balance = await this.token.balanceOf(investor); const balance = await this.token.balanceOf(investor);
......
...@@ -22,13 +22,13 @@ contract('FinalizableCrowdsale', function ([_, owner, wallet, thirdparty]) { ...@@ -22,13 +22,13 @@ contract('FinalizableCrowdsale', function ([_, owner, wallet, thirdparty]) {
}); });
beforeEach(async function () { beforeEach(async function () {
this.startTime = latestTime() + duration.weeks(1); this.openingTime = latestTime() + duration.weeks(1);
this.endTime = this.startTime + duration.weeks(1); this.closingTime = this.openingTime + duration.weeks(1);
this.afterEndTime = this.endTime + duration.seconds(1); this.afterClosingTime = this.closingTime + duration.seconds(1);
this.token = await MintableToken.new(); this.token = await MintableToken.new();
this.crowdsale = await FinalizableCrowdsale.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); await this.token.transferOwnership(this.crowdsale.address);
}); });
...@@ -38,23 +38,23 @@ contract('FinalizableCrowdsale', function ([_, owner, wallet, thirdparty]) { ...@@ -38,23 +38,23 @@ contract('FinalizableCrowdsale', function ([_, owner, wallet, thirdparty]) {
}); });
it('cannot be finalized by third party after ending', async function () { 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); await this.crowdsale.finalize({ from: thirdparty }).should.be.rejectedWith(EVMRevert);
}); });
it('can be finalized by owner after ending', async function () { 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; await this.crowdsale.finalize({ from: owner }).should.be.fulfilled;
}); });
it('cannot be finalized twice', async function () { 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 });
await this.crowdsale.finalize({ from: owner }).should.be.rejectedWith(EVMRevert); await this.crowdsale.finalize({ from: owner }).should.be.rejectedWith(EVMRevert);
}); });
it('logs finalized', async function () { it('logs finalized', async function () {
await increaseTimeTo(this.afterEndTime); await increaseTimeTo(this.afterClosingTime);
const { logs } = await this.crowdsale.finalize({ from: owner }); const { logs } = await this.crowdsale.finalize({ from: owner });
const event = logs.find(e => e.event === 'Finalized'); const event = logs.find(e => e.event === 'Finalized');
should.exist(event); should.exist(event);
......
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));
});
});
});
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);
});
});
});
});
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);
});
});
});
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);
});
});
...@@ -12,12 +12,13 @@ require('chai') ...@@ -12,12 +12,13 @@ require('chai')
.should(); .should();
const RefundableCrowdsale = artifacts.require('RefundableCrowdsaleImpl'); const RefundableCrowdsale = artifacts.require('RefundableCrowdsaleImpl');
const MintableToken = artifacts.require('MintableToken'); const SimpleToken = artifacts.require('SimpleToken');
contract('RefundableCrowdsale', function ([_, owner, wallet, investor]) { contract('RefundableCrowdsale', function ([_, owner, wallet, investor, purchaser]) {
const rate = new BigNumber(1000); const rate = new BigNumber(1);
const goal = ether(800); const goal = ether(50);
const lessThanGoal = ether(750); const lessThanGoal = ether(45);
const tokenSupply = new BigNumber('1e22');
before(async function () { before(async function () {
// Advance to the next block to correctly read time in the solidity "now" function interpreted by testrpc // 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]) { ...@@ -25,61 +26,57 @@ contract('RefundableCrowdsale', function ([_, owner, wallet, investor]) {
}); });
beforeEach(async function () { beforeEach(async function () {
this.startTime = latestTime() + duration.weeks(1); this.openingTime = latestTime() + duration.weeks(1);
this.endTime = this.startTime + duration.weeks(1); this.closingTime = this.openingTime + duration.weeks(1);
this.afterEndTime = this.endTime + duration.seconds(1); this.afterClosingTime = this.closingTime + duration.seconds(1);
this.token = await MintableToken.new(); this.token = await SimpleToken.new();
this.crowdsale = await RefundableCrowdsale.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 () { describe('creating a valid crowdsale', function () {
it('should fail with zero goal', async function () { it('should fail with zero goal', async function () {
await RefundableCrowdsale.new(this.startTime, this.endTime, rate, wallet, 0, { from: owner }) await RefundableCrowdsale.new(
.should.be.rejectedWith(EVMRevert); this.openingTime, this.closingTime, rate, wallet, this.token.address, 0, { from: owner }
).should.be.rejectedWith(EVMRevert);
}); });
}); });
it('should deny refunds before end', async function () { it('should deny refunds before end', async function () {
await this.crowdsale.claimRefund({ from: investor }).should.be.rejectedWith(EVMRevert); 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); await this.crowdsale.claimRefund({ from: investor }).should.be.rejectedWith(EVMRevert);
}); });
it('should deny refunds after end if goal was reached', async function () { 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 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); await this.crowdsale.claimRefund({ from: investor }).should.be.rejectedWith(EVMRevert);
}); });
it('should allow refunds after end if goal was not reached', async function () { 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 this.crowdsale.sendTransaction({ value: lessThanGoal, from: investor });
await increaseTimeTo(this.afterEndTime); await increaseTimeTo(this.afterClosingTime);
await this.crowdsale.finalize({ from: owner }); await this.crowdsale.finalize({ from: owner });
const pre = web3.eth.getBalance(investor); const pre = web3.eth.getBalance(investor);
await this.crowdsale.claimRefund({ from: investor, gasPrice: 0 }) await this.crowdsale.claimRefund({ from: investor, gasPrice: 0 })
.should.be.fulfilled; .should.be.fulfilled;
const post = web3.eth.getBalance(investor); const post = web3.eth.getBalance(investor);
post.minus(pre).should.be.bignumber.equal(lessThanGoal); post.minus(pre).should.be.bignumber.equal(lessThanGoal);
}); });
it('should forward funds to wallet after end if goal was reached', async function () { 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 this.crowdsale.sendTransaction({ value: goal, from: investor });
await increaseTimeTo(this.afterEndTime); await increaseTimeTo(this.afterClosingTime);
const pre = web3.eth.getBalance(wallet); const pre = web3.eth.getBalance(wallet);
await this.crowdsale.finalize({ from: owner }); await this.crowdsale.finalize({ from: owner });
const post = web3.eth.getBalance(wallet); const post = web3.eth.getBalance(wallet);
post.minus(pre).should.be.bignumber.equal(goal); post.minus(pre).should.be.bignumber.equal(goal);
}); });
}); });
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);
});
});
});
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);
});
});
});
});
...@@ -13,6 +13,7 @@ require('chai') ...@@ -13,6 +13,7 @@ require('chai')
const SampleCrowdsale = artifacts.require('SampleCrowdsale'); const SampleCrowdsale = artifacts.require('SampleCrowdsale');
const SampleCrowdsaleToken = artifacts.require('SampleCrowdsaleToken'); const SampleCrowdsaleToken = artifacts.require('SampleCrowdsaleToken');
const RefundVault = artifacts.require('RefundVault');
contract('SampleCrowdsale', function ([owner, wallet, investor]) { contract('SampleCrowdsale', function ([owner, wallet, investor]) {
const RATE = new BigNumber(10); const RATE = new BigNumber(10);
...@@ -25,30 +26,32 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) { ...@@ -25,30 +26,32 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) {
}); });
beforeEach(async function () { beforeEach(async function () {
this.startTime = latestTime() + duration.weeks(1); this.openingTime = latestTime() + duration.weeks(1);
this.endTime = this.startTime + duration.weeks(1); this.closingTime = this.openingTime + duration.weeks(1);
this.afterEndTime = this.endTime + duration.seconds(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.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.token.transferOwnership(this.crowdsale.address);
await this.vault.transferOwnership(this.crowdsale.address);
}); });
it('should create crowdsale with correct parameters', async function () { it('should create crowdsale with correct parameters', async function () {
this.crowdsale.should.exist; this.crowdsale.should.exist;
this.token.should.exist; this.token.should.exist;
const startTime = await this.crowdsale.startTime(); const openingTime = await this.crowdsale.openingTime();
const endTime = await this.crowdsale.endTime(); const closingTime = await this.crowdsale.closingTime();
const rate = await this.crowdsale.rate(); const rate = await this.crowdsale.rate();
const walletAddress = await this.crowdsale.wallet(); const walletAddress = await this.crowdsale.wallet();
const goal = await this.crowdsale.goal(); const goal = await this.crowdsale.goal();
const cap = await this.crowdsale.cap(); const cap = await this.crowdsale.cap();
startTime.should.be.bignumber.equal(this.startTime); openingTime.should.be.bignumber.equal(this.openingTime);
endTime.should.be.bignumber.equal(this.endTime); closingTime.should.be.bignumber.equal(this.closingTime);
rate.should.be.bignumber.equal(RATE); rate.should.be.bignumber.equal(RATE);
walletAddress.should.be.equal(wallet); walletAddress.should.be.equal(wallet);
goal.should.be.bignumber.equal(GOAL); goal.should.be.bignumber.equal(GOAL);
...@@ -64,7 +67,7 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) { ...@@ -64,7 +67,7 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) {
const investmentAmount = ether(1); const investmentAmount = ether(1);
const expectedTokenAmount = RATE.mul(investmentAmount); 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.crowdsale.buyTokens(investor, { value: investmentAmount, from: investor }).should.be.fulfilled;
(await this.token.balanceOf(investor)).should.be.bignumber.equal(expectedTokenAmount); (await this.token.balanceOf(investor)).should.be.bignumber.equal(expectedTokenAmount);
...@@ -78,17 +81,17 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) { ...@@ -78,17 +81,17 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) {
}); });
it('should reject payments over cap', async function () { 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(CAP);
await this.crowdsale.send(1).should.be.rejectedWith(EVMRevert); 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 () { 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); await this.crowdsale.send(GOAL);
const beforeFinalization = web3.eth.getBalance(wallet); const beforeFinalization = web3.eth.getBalance(wallet);
await increaseTimeTo(this.afterEndTime); await increaseTimeTo(this.afterClosingTime);
await this.crowdsale.finalize({ from: owner }); await this.crowdsale.finalize({ from: owner });
const afterFinalization = web3.eth.getBalance(wallet); const afterFinalization = web3.eth.getBalance(wallet);
...@@ -98,9 +101,9 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) { ...@@ -98,9 +101,9 @@ contract('SampleCrowdsale', function ([owner, wallet, investor]) {
it('should allow refunds if the goal is not reached', async function () { it('should allow refunds if the goal is not reached', async function () {
const balanceBeforeInvestment = web3.eth.getBalance(investor); 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 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.finalize({ from: owner });
await this.crowdsale.claimRefund({ from: investor, gasPrice: 0 }).should.be.fulfilled; await this.crowdsale.claimRefund({ from: investor, gasPrice: 0 }).should.be.fulfilled;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment