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
Feature/crowdsale refactor #744
Conversation
ERC20 _token | ||
) public | ||
Crowdsale(_rate, _wallet, _token) | ||
//Ownable() //Do I need to call it anyway?! --apparently not |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AllowanceCrowdsale?
There was a problem hiding this comment.
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.. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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);
}
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using
require(!hasExpired());
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. My bad =P
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()
.
There was a problem hiding this comment.
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..
There was a problem hiding this 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)); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
β¦to solve Crowdsale selection in tests, better way?
β¦acing hasEnded())
β¦rited from Crowdsale
β¦med, mocks changed. Capped tests passing
β¦emoved unused dependencies in tests
β¦ed IndividuallyCappedCrowdsaleImpl.sol and corresponding tests, passed.
β¦dsales. checked that it IS indeed inherited, but leaving it there as per Frans suggestion.
There was a problem hiding this 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); | ||
_; | ||
} |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]); | ||
_; | ||
} |
There was a problem hiding this comment.
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++) { |
There was a problem hiding this comment.
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++) { |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to addManyToWhitelist
pls
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(discussed offline)
* 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
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.
npm run lint:all:fix
).