Add back WhitelistedCrowdsale (#1525)
* Added WhitelisterRole. * Added WhitelisteeRole and WhitelistedCrowdsale. * Added WhitelistedCrowdsale tests. * Whitelisters can now remove Whitelistees. * PublicRole.behavior now supports a manager account, added Whitelistee tests. * Rephrased tests, added test for whitelistees doing invalid purchases. * Fixed linter error. * Fixed typos Co-Authored-By: nventuro <nicolas.venturo@gmail.com> * Working around JS quirks Co-Authored-By: nventuro <nicolas.venturo@gmail.com> * Update PublicRole.behavior.js * Renamed WhitelisteeRole to WhitelistedRole. * Renamed WhitelistedCrowdsale to WhitelistCrowdsale. * Now using the new test helper. * Added basic documentation.
This commit is contained in:
50
contracts/access/roles/WhitelistedRole.sol
Normal file
50
contracts/access/roles/WhitelistedRole.sol
Normal file
@ -0,0 +1,50 @@
|
||||
pragma solidity ^0.4.24;
|
||||
|
||||
import "../Roles.sol";
|
||||
import "./WhitelisterRole.sol";
|
||||
|
||||
/**
|
||||
* @title WhitelistedRole
|
||||
* @dev Whitelisted accounts have been approved by a Whitelister to perform certain actions (e.g. participate in a
|
||||
* crowdsale). This role is special in that the only accounts that can add it are Whitelisters (who can also remove it),
|
||||
* and not Whitelisteds themselves.
|
||||
*/
|
||||
contract WhitelistedRole is WhitelisterRole {
|
||||
using Roles for Roles.Role;
|
||||
|
||||
event WhitelistedAdded(address indexed account);
|
||||
event WhitelistedRemoved(address indexed account);
|
||||
|
||||
Roles.Role private _whitelisteds;
|
||||
|
||||
modifier onlyWhitelisted() {
|
||||
require(isWhitelisted(msg.sender));
|
||||
_;
|
||||
}
|
||||
|
||||
function isWhitelisted(address account) public view returns (bool) {
|
||||
return _whitelisteds.has(account);
|
||||
}
|
||||
|
||||
function addWhitelisted(address account) public onlyWhitelister {
|
||||
_addWhitelisted(account);
|
||||
}
|
||||
|
||||
function removeWhitelisted(address account) public onlyWhitelister {
|
||||
_removeWhitelisted(account);
|
||||
}
|
||||
|
||||
function renounceWhitelisted() public {
|
||||
_removeWhitelisted(msg.sender);
|
||||
}
|
||||
|
||||
function _addWhitelisted(address account) internal {
|
||||
_whitelisteds.add(account);
|
||||
emit WhitelistedAdded(account);
|
||||
}
|
||||
|
||||
function _removeWhitelisted(address account) internal {
|
||||
_whitelisteds.remove(account);
|
||||
emit WhitelistedRemoved(account);
|
||||
}
|
||||
}
|
||||
47
contracts/access/roles/WhitelisterRole.sol
Normal file
47
contracts/access/roles/WhitelisterRole.sol
Normal file
@ -0,0 +1,47 @@
|
||||
pragma solidity ^0.4.24;
|
||||
|
||||
import "../Roles.sol";
|
||||
|
||||
/**
|
||||
* @title WhitelisterRole
|
||||
* @dev Whitelisters are responsible for assigning and removing Whitelisted accounts.
|
||||
*/
|
||||
contract WhitelisterRole {
|
||||
using Roles for Roles.Role;
|
||||
|
||||
event WhitelisterAdded(address indexed account);
|
||||
event WhitelisterRemoved(address indexed account);
|
||||
|
||||
Roles.Role private _whitelisters;
|
||||
|
||||
constructor () internal {
|
||||
_addWhitelister(msg.sender);
|
||||
}
|
||||
|
||||
modifier onlyWhitelister() {
|
||||
require(isWhitelister(msg.sender));
|
||||
_;
|
||||
}
|
||||
|
||||
function isWhitelister(address account) public view returns (bool) {
|
||||
return _whitelisters.has(account);
|
||||
}
|
||||
|
||||
function addWhitelister(address account) public onlyWhitelister {
|
||||
_addWhitelister(account);
|
||||
}
|
||||
|
||||
function renounceWhitelister() public {
|
||||
_removeWhitelister(msg.sender);
|
||||
}
|
||||
|
||||
function _addWhitelister(address account) internal {
|
||||
_whitelisters.add(account);
|
||||
emit WhitelisterAdded(account);
|
||||
}
|
||||
|
||||
function _removeWhitelister(address account) internal {
|
||||
_whitelisters.remove(account);
|
||||
emit WhitelisterRemoved(account);
|
||||
}
|
||||
}
|
||||
21
contracts/crowdsale/validation/WhitelistCrowdsale.sol
Normal file
21
contracts/crowdsale/validation/WhitelistCrowdsale.sol
Normal file
@ -0,0 +1,21 @@
|
||||
pragma solidity ^0.4.24;
|
||||
import "../Crowdsale.sol";
|
||||
import "../../access/roles/WhitelistedRole.sol";
|
||||
|
||||
|
||||
/**
|
||||
* @title WhitelistCrowdsale
|
||||
* @dev Crowdsale in which only whitelisted users can contribute.
|
||||
*/
|
||||
contract WhitelistCrowdsale is WhitelistedRole, Crowdsale {
|
||||
/**
|
||||
* @dev Extend parent behavior requiring beneficiary to be whitelisted. Note that no
|
||||
* restriction is imposed on the account sending the transaction.
|
||||
* @param _beneficiary Token beneficiary
|
||||
* @param _weiAmount Amount of wei contributed
|
||||
*/
|
||||
function _preValidatePurchase(address _beneficiary, uint256 _weiAmount) internal view {
|
||||
require(isWhitelisted(_beneficiary));
|
||||
super._preValidatePurchase(_beneficiary, _weiAmount);
|
||||
}
|
||||
}
|
||||
10
contracts/mocks/WhitelistCrowdsaleImpl.sol
Normal file
10
contracts/mocks/WhitelistCrowdsaleImpl.sol
Normal file
@ -0,0 +1,10 @@
|
||||
pragma solidity ^0.4.24;
|
||||
|
||||
import "../token/ERC20/IERC20.sol";
|
||||
import "../crowdsale/validation/WhitelistCrowdsale.sol";
|
||||
import "../crowdsale/Crowdsale.sol";
|
||||
|
||||
|
||||
contract WhitelistCrowdsaleImpl is Crowdsale, WhitelistCrowdsale {
|
||||
constructor (uint256 _rate, address _wallet, IERC20 _token) Crowdsale(_rate, _wallet, _token) public {}
|
||||
}
|
||||
8
contracts/mocks/WhitelistedRoleMock.sol
Normal file
8
contracts/mocks/WhitelistedRoleMock.sol
Normal file
@ -0,0 +1,8 @@
|
||||
pragma solidity ^0.4.24;
|
||||
|
||||
import "../access/roles/WhitelistedRole.sol";
|
||||
|
||||
contract WhitelistedRoleMock is WhitelistedRole {
|
||||
function onlyWhitelistedMock() public view onlyWhitelisted {
|
||||
}
|
||||
}
|
||||
17
contracts/mocks/WhitelisterRoleMock.sol
Normal file
17
contracts/mocks/WhitelisterRoleMock.sol
Normal file
@ -0,0 +1,17 @@
|
||||
pragma solidity ^0.4.24;
|
||||
|
||||
import "../access/roles/WhitelisterRole.sol";
|
||||
|
||||
contract WhitelisterRoleMock is WhitelisterRole {
|
||||
function removeWhitelister(address account) public {
|
||||
_removeWhitelister(account);
|
||||
}
|
||||
|
||||
function onlyWhitelisterMock() public view onlyWhitelister {
|
||||
}
|
||||
|
||||
// Causes a compilation error if super._removeWhitelister is not internal
|
||||
function _removeWhitelister(address account) internal {
|
||||
super._removeWhitelister(account);
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@ function capitalize (str) {
|
||||
return str.replace(/\b\w/g, l => l.toUpperCase());
|
||||
}
|
||||
|
||||
function shouldBehaveLikePublicRole (authorized, otherAuthorized, [anyone], rolename) {
|
||||
function shouldBehaveLikePublicRole (authorized, otherAuthorized, [anyone], rolename, manager) {
|
||||
rolename = capitalize(rolename);
|
||||
|
||||
describe('should behave like public role', function () {
|
||||
@ -18,11 +18,13 @@ function shouldBehaveLikePublicRole (authorized, otherAuthorized, [anyone], role
|
||||
(await this.contract[`is${rolename}`](anyone)).should.equal(false);
|
||||
});
|
||||
|
||||
it('emits events during construction', async function () {
|
||||
await expectEvent.inConstruction(this.contract, `${rolename}Added`, {
|
||||
account: authorized,
|
||||
if (manager === undefined) { // Managed roles are only assigned by the manager, and none are set at construction
|
||||
it('emits events during construction', async function () {
|
||||
await expectEvent.inConstruction(this.contract, `${rolename}Added`, {
|
||||
account: authorized,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('reverts when querying roles for the null account', async function () {
|
||||
await shouldFail.reverting(this.contract[`is${rolename}`](ZERO_ADDRESS));
|
||||
@ -47,43 +49,52 @@ function shouldBehaveLikePublicRole (authorized, otherAuthorized, [anyone], role
|
||||
});
|
||||
|
||||
describe('add', function () {
|
||||
it('adds role to a new account', async function () {
|
||||
await this.contract[`add${rolename}`](anyone, { from: authorized });
|
||||
(await this.contract[`is${rolename}`](anyone)).should.equal(true);
|
||||
});
|
||||
const from = manager === undefined ? authorized : manager;
|
||||
|
||||
it(`emits a ${rolename}Added event`, async function () {
|
||||
const { logs } = await this.contract[`add${rolename}`](anyone, { from: authorized });
|
||||
expectEvent.inLogs(logs, `${rolename}Added`, { account: anyone });
|
||||
});
|
||||
context(`from ${manager ? 'the manager' : 'a role-haver'} account`, function () {
|
||||
it('adds role to a new account', async function () {
|
||||
await this.contract[`add${rolename}`](anyone, { from });
|
||||
(await this.contract[`is${rolename}`](anyone)).should.equal(true);
|
||||
});
|
||||
|
||||
it('reverts when adding role to an already assigned account', async function () {
|
||||
await shouldFail.reverting(this.contract[`add${rolename}`](authorized, { from: authorized }));
|
||||
});
|
||||
it(`emits a ${rolename}Added event`, async function () {
|
||||
const { logs } = await this.contract[`add${rolename}`](anyone, { from });
|
||||
expectEvent.inLogs(logs, `${rolename}Added`, { account: anyone });
|
||||
});
|
||||
|
||||
it('reverts when adding role to the null account', async function () {
|
||||
await shouldFail.reverting(this.contract[`add${rolename}`](ZERO_ADDRESS, { from: authorized }));
|
||||
it('reverts when adding role to an already assigned account', async function () {
|
||||
await shouldFail.reverting(this.contract[`add${rolename}`](authorized, { from }));
|
||||
});
|
||||
|
||||
it('reverts when adding role to the null account', async function () {
|
||||
await shouldFail.reverting(this.contract[`add${rolename}`](ZERO_ADDRESS, { from }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', function () {
|
||||
it('removes role from an already assigned account', async function () {
|
||||
await this.contract[`remove${rolename}`](authorized);
|
||||
(await this.contract[`is${rolename}`](authorized)).should.equal(false);
|
||||
(await this.contract[`is${rolename}`](otherAuthorized)).should.equal(true);
|
||||
});
|
||||
// Non-managed roles have no restrictions on the mocked '_remove' function (exposed via 'remove').
|
||||
const from = manager || anyone;
|
||||
|
||||
it(`emits a ${rolename}Removed event`, async function () {
|
||||
const { logs } = await this.contract[`remove${rolename}`](authorized);
|
||||
expectEvent.inLogs(logs, `${rolename}Removed`, { account: authorized });
|
||||
});
|
||||
context(`from ${manager ? 'the manager' : 'any'} account`, function () {
|
||||
it('removes role from an already assigned account', async function () {
|
||||
await this.contract[`remove${rolename}`](authorized, { from });
|
||||
(await this.contract[`is${rolename}`](authorized)).should.equal(false);
|
||||
(await this.contract[`is${rolename}`](otherAuthorized)).should.equal(true);
|
||||
});
|
||||
|
||||
it('reverts when removing from an unassigned account', async function () {
|
||||
await shouldFail.reverting(this.contract[`remove${rolename}`](anyone));
|
||||
});
|
||||
it(`emits a ${rolename}Removed event`, async function () {
|
||||
const { logs } = await this.contract[`remove${rolename}`](authorized, { from });
|
||||
expectEvent.inLogs(logs, `${rolename}Removed`, { account: authorized });
|
||||
});
|
||||
|
||||
it('reverts when removing role from the null account', async function () {
|
||||
await shouldFail.reverting(this.contract[`remove${rolename}`](ZERO_ADDRESS));
|
||||
it('reverts when removing from an unassigned account', async function () {
|
||||
await shouldFail.reverting(this.contract[`remove${rolename}`](anyone), { from });
|
||||
});
|
||||
|
||||
it('reverts when removing role from the null account', async function () {
|
||||
await shouldFail.reverting(this.contract[`remove${rolename}`](ZERO_ADDRESS), { from });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
12
test/access/roles/WhitelistedRole.test.js
Normal file
12
test/access/roles/WhitelistedRole.test.js
Normal file
@ -0,0 +1,12 @@
|
||||
const { shouldBehaveLikePublicRole } = require('../../access/roles/PublicRole.behavior');
|
||||
const WhitelistedRoleMock = artifacts.require('WhitelistedRoleMock');
|
||||
|
||||
contract('WhitelistedRole', function ([_, whitelisted, otherWhitelisted, whitelister, ...otherAccounts]) {
|
||||
beforeEach(async function () {
|
||||
this.contract = await WhitelistedRoleMock.new({ from: whitelister });
|
||||
await this.contract.addWhitelisted(whitelisted, { from: whitelister });
|
||||
await this.contract.addWhitelisted(otherWhitelisted, { from: whitelister });
|
||||
});
|
||||
|
||||
shouldBehaveLikePublicRole(whitelisted, otherWhitelisted, otherAccounts, 'whitelisted', whitelister);
|
||||
});
|
||||
11
test/access/roles/WhitelisterRole.test.js
Normal file
11
test/access/roles/WhitelisterRole.test.js
Normal file
@ -0,0 +1,11 @@
|
||||
const { shouldBehaveLikePublicRole } = require('../../access/roles/PublicRole.behavior');
|
||||
const WhitelisterRoleMock = artifacts.require('WhitelisterRoleMock');
|
||||
|
||||
contract('WhitelisterRole', function ([_, whitelister, otherWhitelister, ...otherAccounts]) {
|
||||
beforeEach(async function () {
|
||||
this.contract = await WhitelisterRoleMock.new({ from: whitelister });
|
||||
await this.contract.addWhitelister(otherWhitelister, { from: whitelister });
|
||||
});
|
||||
|
||||
shouldBehaveLikePublicRole(whitelister, otherWhitelister, otherAccounts, 'whitelister');
|
||||
});
|
||||
57
test/crowdsale/WhitelistCrowdsale.test.js
Normal file
57
test/crowdsale/WhitelistCrowdsale.test.js
Normal file
@ -0,0 +1,57 @@
|
||||
require('../helpers/setup');
|
||||
const { ether } = require('../helpers/ether');
|
||||
const shouldFail = require('../helpers/shouldFail');
|
||||
|
||||
const BigNumber = web3.BigNumber;
|
||||
|
||||
const WhitelistCrowdsale = artifacts.require('WhitelistCrowdsaleImpl');
|
||||
const SimpleToken = artifacts.require('SimpleToken');
|
||||
|
||||
contract('WhitelistCrowdsale', function ([_, wallet, whitelister, whitelisted, otherWhitelisted, anyone]) {
|
||||
const rate = 1;
|
||||
const value = ether(42);
|
||||
const tokenSupply = new BigNumber('1e22');
|
||||
|
||||
beforeEach(async function () {
|
||||
this.token = await SimpleToken.new({ from: whitelister });
|
||||
this.crowdsale = await WhitelistCrowdsale.new(rate, wallet, this.token.address, { from: whitelister });
|
||||
await this.token.transfer(this.crowdsale.address, tokenSupply, { from: whitelister });
|
||||
});
|
||||
|
||||
async function purchaseShouldSucceed (crowdsale, beneficiary, value) {
|
||||
await crowdsale.buyTokens(beneficiary, { from: beneficiary, value });
|
||||
await crowdsale.sendTransaction({ from: beneficiary, value });
|
||||
}
|
||||
|
||||
async function purchaseShouldFail (crowdsale, beneficiary, value) {
|
||||
await shouldFail.reverting(crowdsale.buyTokens(beneficiary, { from: beneficiary, value }));
|
||||
await shouldFail.reverting(crowdsale.sendTransaction({ from: beneficiary, value }));
|
||||
}
|
||||
|
||||
context('with no whitelisted addresses', function () {
|
||||
it('rejects all purchases', async function () {
|
||||
await purchaseShouldFail(this.crowdsale, anyone, value);
|
||||
await purchaseShouldFail(this.crowdsale, whitelisted, value);
|
||||
});
|
||||
});
|
||||
|
||||
context('with whitelisted addresses', function () {
|
||||
beforeEach(async function () {
|
||||
await this.crowdsale.addWhitelisted(whitelisted, { from: whitelister });
|
||||
await this.crowdsale.addWhitelisted(otherWhitelisted, { from: whitelister });
|
||||
});
|
||||
|
||||
it('accepts purchases with whitelisted beneficiaries', async function () {
|
||||
await purchaseShouldSucceed(this.crowdsale, whitelisted, value);
|
||||
await purchaseShouldSucceed(this.crowdsale, otherWhitelisted, value);
|
||||
});
|
||||
|
||||
it('rejects purchases from whitelisted addresses with non-whitelisted beneficiaries', async function () {
|
||||
await shouldFail(this.crowdsale.buyTokens(anyone, { from: whitelisted, value }));
|
||||
});
|
||||
|
||||
it('rejects purchases with non-whitelisted beneficiaries', async function () {
|
||||
await purchaseShouldFail(this.crowdsale, anyone, value);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user