Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/crowdsale refactor #744

Merged
merged 66 commits into from Feb 20, 2018

Conversation

fiiiu
Copy link
Contributor

@fiiiu fiiiu commented Feb 15, 2018

Fixes #739

πŸš€ Description

Refactor of crowdsale models. Still based on the inheritance architecture used in OZ (a model based on composition is still to expensive in terms of gas). Base Crowdsale contract is made as abstract as possible but functional nonetheless. Simple extension classes add features and customized behavior. Intended as a foundation to be expanded, where many more features can be implemented in future pull requests.

  • πŸ“˜ I've reviewed the OpenZeppelin Contributor Guidelines
  • βœ… I've added tests where applicable to test my new functionality.
  • πŸ“– I've made sure that my contracts are well-documented.
  • 🎨 I've run the JS/Solidity linters and fixed any issues (npm run lint:all:fix).

@fiiiu fiiiu added the next label Feb 15, 2018
ERC20 _token
) public
Crowdsale(_rate, _wallet, _token)
//Ownable() //Do I need to call it anyway?! --apparently not
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Base constructors only need to be referenced when you need to pass parameters to them, so no, Ownable is usually not needed to be referenced.
Can we remove the comment here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, left this behind, removed.

questions.md Outdated
@@ -0,0 +1,135 @@
# Questions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd remove this file from the PR.
These kind of TODO files are more a personal thing and there is no need to keep it in the repo's history for everyone to see.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, sorry, residual file, didn't mean to add it in the first place, removed.

@@ -0,0 +1,67 @@
import ether from '../helpers/ether';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the name "PremintedCrowdsale" here...
In a way, the base Crowdsale is also preminted, because it assumes that it has a positive balance for the associated token. In this case, we also assume that the token has been pre-minted, AND assume that the crowdsale has approval/allowance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had disscussed this with @facuspagnuolo, since ApprovedCrowdsale wasn't too explanatory.. But I fully agree with your comment that the base is also preminted. Any other proposal?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some ideas:
DelegateCrowdsale, ProxyCrowdsale, ForwardingCrowdsale, AuthorizedCrowdsale?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AllowanceCrowdsale?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

}

// TODO: consider querying approval left and end crowdsale if depleted
// But approval could be increased..
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this, my point in that reply was that maybe the wallet is depleted, but then the allowance could be increased by increaseApproval() and the crowdsale continue, right? Do we not want to allow this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function I have in mind simply returns the current allowance.

function remainingTokens() public returns (uint256) {
  return token.allowance(_tokenWallet, this);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, I'll put that in.


function _preValidatePurchase(address _beneficiary, uint256 _weiAmount) internal {
super._preValidatePurchase(_beneficiary, _weiAmount);
require(now >= startTime && now <= endTime);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using
require(!hasExpired());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean substitute
require(now >= startTime && now <= endTime);
for:
require(now >= startTime);
require(!hasExpired());
?
We still need to check we are after startTime, so I'm not sure what the advantage of this would be...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. My bad =P

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would abstract that require into another inTimeRange modifier, to make the code easier to read and because it may come handy when working with a TimedCrowdsale. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe withinBuyingPeriod


function _postValidatePurchase(address _beneficiary, uint256 _weiAmount) internal {
super._postValidatePurchase(_beneficiary, _weiAmount);
contributions[_beneficiary] = contributions[_beneficiary].add(_weiAmount);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the fact that we are not only validating here, but also updating the user's contribution.
Perhaps we should add a new abstract method on the base Crowdsale to do this sort of thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but the user contribution is only tracked for validation purposes.. We would need something like a separate _updateValidationState().. Maybe too much, since we already have _pre and _post validation schemes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this new state member is only tracked for the sake of validation.
I also don't like _updateValidationState(), since as you say, seems a little to bloated.
Now, consider a crowdsale extension that simply keeps track of additional user data and allows users to query it individually or statistically. This function processPurchase() would be the entry point and the extension would not be a validation in itself. However, IndividuallyCappedCrowdsale as a validation would use updatePurchase().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Please check whether we want it to be like this, as Crowdsale is a bit more bloated now..

Copy link
Contributor Author

@fiiiu fiiiu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test review

uint256 elapsedTime = now - startTime;
uint256 timeRange = endTime - startTime;
uint256 rateRange = initialRate - finalRate;
return initialRate.sub(elapsedTime.mul(rateRange).div(timeRange));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using
rateRange = finalRate - initialRate for consistency and easier readability. (.sub is replaced by .add)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would make rateRangeΒ negative, we require(_initialRate >= _finalRate); in order to work with unsigned ints (hence IncreasingPrice). We could add a DecreasingPriceCrowdsale, but I'm skeptical it would be of much use.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, slope of the rate curve is negative. I was thinking about the slope of the price curve.
My bad again then.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm with @ajsantander in here, this is confusing. We should leave a clarification to avoid silly millionaire mistakes.

theethernaut and others added 21 commits February 15, 2018 15:59
…to solve Crowdsale selection in tests, better way?
…ed IndividuallyCappedCrowdsaleImpl.sol and corresponding tests, passed.
…dsales. checked that it IS indeed inherited, but leaving it there as per Frans suggestion.
Copy link
Contributor

@facuspagnuolo facuspagnuolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! some minor comments

modifier inTimeRange {
require(now >= startTime && now <= endTime);
_;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, but we may want to place modifiers after the state variables :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoops, I took ordering from solidity docs, will fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it is between constructor and other methods in https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/ownership/Ownable.sol for instance..

Which should it be, before or after constructor?

modifier isWhitelisted(address _beneficiary) {
require(whitelist[_beneficiary]);
_;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

* @param _beneficiaries Addresses to be added to the whitelist
*/
function addCrowdToWhitelist(address[] _beneficiaries) external onlyOwner {
for (uint i = 0; i < _beneficiaries.length; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use uin256 explicitly

* @param _cap Wei limit for individual contribution
*/
function setGroupCap(address[] _beneficiaries, uint256 _cap) external onlyOwner {
for (uint i = 0; i < _beneficiaries.length; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use uin256 explicitly

@@ -1,20 +1,19 @@
pragma solidity ^0.4.18;


import "../crowdsale/CappedCrowdsale.sol";
//import "../examples/SimpleToken.sol";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we remove this line?


import "../token/ERC20/MintableToken.sol";
import "../crowdsale/distribution/RefundableCrowdsale.sol";
import "../crowdsale/distribution/utils/RefundVault.sol";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this one?

@@ -0,0 +1,19 @@
pragma solidity ^0.4.18;

//import "../examples/SimpleToken.sol";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

@@ -0,0 +1,19 @@
pragma solidity ^0.4.18;

//import "../examples/SimpleToken.sol";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️

* 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
Copy link
Contributor

@shrugs shrugs Feb 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've traditionally seen multiline @dev comments either prefixed with @dev all the way down

/**
 * @dev like
 * @dev this
 */

The docs don't specify what happens for multiline, though, so I'm not sure what the standard is. Off the top of my head, though, I think OZ primarily uses the @dev prefix per-line, but I could be wrong and I'm not particularly partial

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @shrugs. Throughout the OZ codebase, a single @dev at the beginning of a multi-sentence description seems to be the norm (examples: Bounty.sol, DayLimit.sol). Also, the automatically-generated docs parse NatSpec following this convention.

The NatSpec specification is not at all clear about how to handle this case, but there are two examples within the document which use a muti-line @notice tag (link).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the heads up!

* @param _beneficiary Address performing the token purchase
* @param _weiAmount Value in wei involved in the purchase
*/
function _preValidatePurchase(address _beneficiary, uint256 _weiAmount) internal {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm down to do that, now that we're making sweeping changes. Obviously updated across the file(s) where appropriate

* @param _beneficiary Address performing the token purchase
* @param _weiAmount Value in wei involved in the purchase
*/
function _postValidatePurchase(address _beneficiary, uint256 _weiAmount) internal {
Copy link
Contributor

@shrugs shrugs Feb 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are some example use-cases for this, especially since we don't use it in these contracts? If the answer is just "why not allow it" that's fine too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, if you are performing a purchase that involves a call to another contract, you can check that the call worked the way you were expecting here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, gotcha, for invariants and such. Nice.

*/
function TimedCrowdsale(uint256 _startTime, uint256 _endTime) public {
require(_startTime >= now);
require(_endTime >= _startTime);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: should endTime just be > _startTime? having a crowdsale open for exactly one second in time doesn't seem useful.

(could also ignore this comment because it's unnecessarily restrictive)

* @dev Checks whether the period in which the crowdsale is open has already elapsed.
* @return Whether crowdsale period has elapsed
*/
function hasExpired() public view returns (bool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason we're not using the existing naming of hasEnded? I don't mind either way, but it's good to know why we changed the api.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasEnded was the prior method that encompassed ending either by reaching endTime or by reaching the cap in a CappedCrowdsale. Now the purchase validation is done in separate places so we need to split this (hasExpired and capReached). While we could call this one hasEnded, a crowdsale could also end "prematurely" by reaching its cap, thus, be ended but not expired..


/**
* @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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to enforce that the linear function here must make the tokens increase in price? I expect a crowdsale that gets cheaper over time could be desired as well. I'm not sure there's a benefit to requiring an increase. We should let the creator make a linear function of their own slope. (and in that case this contract could be LinearlyScaledRateCrowdsale

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of restricting the price to be increasing was to be able to use unsigned ints throughout.. Plus I didn't see much use for a decreasing price crowdsale, but we could add this more general crowdsale flavor later if you think it might be useful!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, I'm happy with that logic πŸ‘

@@ -0,0 +1,58 @@
pragma solidity ^ 0.4.18;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excited for this contract, people keep asking for it :D

CappedCrowdsale(_cap)
FinalizableCrowdsale()
RefundableCrowdsale(_goal)
Crowdsale(_startTime, _endTime, _rate, _wallet, _token)
TimedCrowdsale(_startTime, _endTime)
Crowdsale(_rate, _wallet, _token)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that we should probably be following the "most basic class first" rules here, like we do in the other *Impl.sol files. Is there a reason Crowdsale() is last? or should it be the first, since it's the most basic class?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it's perhaps that this might not actually work, since the Crowdsale is inherited from last and doesn't call super.*

@@ -13,7 +13,8 @@ contract FinalizableCrowdsaleImpl is FinalizableCrowdsale {
address _wallet,
MintableToken _token
) public
Crowdsale(_startTime, _endTime, _rate, _wallet, _token)
TimedCrowdsale(_startTime, _endTime)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likewise here for "most base-like"

ERC20 _token,
uint256 _initialRate,
uint256 _finalRate
) public
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we follow the sytnax we use across the project for this, here and across this PR? It would look like:

  function IncreasingPriceCrowdsaleImpl (
    uint256 _startTime,
    uint256 _endTime,
    address _wallet,
    ERC20 _token,
    uint256 _initialRate,
    uint256 _finalRate
  )
    public
    Crowdsale(_initialRate, _wallet, _token)
    TimedCrowdsale(_startTime, _endTime)
    IncreasingPriceCrowdsale(_initialRate, _finalRate)
  {
  }

(where the visibility modifier is on an equal indentation with the rest of the modifiers)

facuspagnuolo
facuspagnuolo previously approved these changes Feb 20, 2018
Copy link
Contributor

@facuspagnuolo facuspagnuolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Contributor

@maraoz maraoz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great PR! Please see minor corrections on naming pls

* @param _beneficiary Address performing the token purchase
* @param _weiAmount Value in wei involved in the purchase
*/
function _preValidatePurchase(address _beneficiary, uint256 _weiAmount) internal {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NACK

* @param _beneficiary Token purchaser
* @param _tokenAmount Amount of tokens purchased
*/
function _emitTokens(address _beneficiary, uint256 _tokenAmount) internal {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super happy with this name

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed offline with @fiiiu and @facuspagnuolo and decided to go with _deliverTokens πŸ•

* @title UserCappedCrowdsale
* @dev Crowdsale with per-user caps.
*/
contract UserCappedCrowdsale is Crowdsale, Ownable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can think of a better name?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decided offline with @facuspagnuolo and @fiiiu to go with IndividuallyCappedCrowdsale

* @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 addCrowdToWhitelist(address[] _beneficiaries) external onlyOwner {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to addManyToWhitelist pls

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(discussed offline)

@maraoz maraoz merged commit c05918c into OpenZeppelin:master Feb 20, 2018
@maraoz maraoz removed next labels Feb 20, 2018
ProphetDaniel pushed a commit to classicdelta/Smart-Contracts that referenced this pull request Mar 9, 2018
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Crowdsale Refactor
7 participants