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

Add back WhitelistedCrowdsale #1525

Merged
merged 15 commits into from Dec 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 50 additions & 0 deletions contracts/access/roles/WhitelistedRole.sol
@@ -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 changes: 47 additions & 0 deletions contracts/access/roles/WhitelisterRole.sol
@@ -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 changes: 21 additions & 0 deletions contracts/crowdsale/validation/WhitelistCrowdsale.sol
@@ -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 {
frangio marked this conversation as resolved.
Show resolved Hide resolved
/**
* @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 changes: 10 additions & 0 deletions contracts/mocks/WhitelistCrowdsaleImpl.sol
@@ -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 changes: 8 additions & 0 deletions contracts/mocks/WhitelistedRoleMock.sol
@@ -0,0 +1,8 @@
pragma solidity ^0.4.24;

import "../access/roles/WhitelistedRole.sol";

contract WhitelistedRoleMock is WhitelistedRole {
function onlyWhitelistedMock() public view onlyWhitelisted {
}
}
17 changes: 17 additions & 0 deletions contracts/mocks/WhitelisterRoleMock.sol
@@ -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
frangio marked this conversation as resolved.
Show resolved Hide resolved
function _removeWhitelister(address account) internal {
super._removeWhitelister(account);
}
}
75 changes: 43 additions & 32 deletions test/access/roles/PublicRole.behavior.js
Expand Up @@ -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 () {
Expand All @@ -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));
Expand All @@ -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;

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(`emits a ${rolename}Removed event`, async function () {
const { logs } = await this.contract[`remove${rolename}`](authorized);
expectEvent.inLogs(logs, `${rolename}Removed`, { account: authorized });
});
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 from an unassigned account', async function () {
await shouldFail.reverting(this.contract[`remove${rolename}`](anyone));
});
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));
it('reverts when removing role from the null account', async function () {
await shouldFail.reverting(this.contract[`remove${rolename}`](ZERO_ADDRESS), { from });
});
});
});

Expand Down
12 changes: 12 additions & 0 deletions test/access/roles/WhitelistedRole.test.js
@@ -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 changes: 11 additions & 0 deletions test/access/roles/WhitelisterRole.test.js
@@ -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 changes: 57 additions & 0 deletions test/crowdsale/WhitelistCrowdsale.test.js
@@ -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);
});
});
});