Przeglądaj źródła

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.
Nicolás Venturo 6 lat temu
rodzic
commit
7142e25e78

+ 50 - 0
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 - 0
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 - 0
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 {
+    /**
+    * @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 - 0
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 - 0
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 - 0
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
+    function _removeWhitelister(address account) internal {
+        super._removeWhitelister(account);
+    }
+}

+ 43 - 32
test/access/roles/PublicRole.behavior.js

@@ -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;
+
+      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 });
+        });
       });
     });
 

+ 12 - 0
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 - 0
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 - 0
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);
+    });
+  });
+});