diff --git a/contracts/mocks/WhitelistMock.sol b/contracts/mocks/WhitelistMock.sol new file mode 100644 index 00000000000..4c286927ab7 --- /dev/null +++ b/contracts/mocks/WhitelistMock.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.4.18; + +import "../ownership/Whitelist.sol"; + + +contract WhitelistMock is Whitelist { + + function onlyWhitelistedCanDoThis() + onlyWhitelisted + view + external + { + } +} diff --git a/contracts/ownership/Whitelist.sol b/contracts/ownership/Whitelist.sol new file mode 100644 index 00000000000..bc90c162098 --- /dev/null +++ b/contracts/ownership/Whitelist.sol @@ -0,0 +1,81 @@ +pragma solidity ^0.4.18; + + +import "./Ownable.sol"; + + +/** + * @title Whitelist + * @dev The Whitelist contract has a whitelist of addresses, and provides basic authorization control functions. + * @dev This simplifies the implementation of "user permissions". + */ +contract Whitelist is Ownable { + mapping(address => bool) public whitelist; + + event WhitelistedAddressAdded(address addr); + event WhitelistedAddressRemoved(address addr); + + /** + * @dev Throws if called by any account that's not whitelisted. + */ + modifier onlyWhitelisted() { + require(whitelist[msg.sender]); + _; + } + + /** + * @dev add an address to the whitelist + * @param addr address + * @return true if the address was added to the whitelist, false if the address was already in the whitelist + */ + function addAddressToWhitelist(address addr) onlyOwner public returns(bool success) { + if (!whitelist[addr]) { + whitelist[addr] = true; + WhitelistedAddressAdded(addr); + success = true; + } + } + + /** + * @dev add addresses to the whitelist + * @param addrs addresses + * @return true if at least one address was added to the whitelist, + * false if all addresses were already in the whitelist + */ + function addAddressesToWhitelist(address[] addrs) onlyOwner public returns(bool success) { + for (uint256 i = 0; i < addrs.length; i++) { + if (addAddressToWhitelist(addrs[i])) { + success = true; + } + } + } + + /** + * @dev remove an address from the whitelist + * @param addr address + * @return true if the address was removed from the whitelist, + * false if the address wasn't in the whitelist in the first place + */ + function removeAddressFromWhitelist(address addr) onlyOwner public returns(bool success) { + if (whitelist[addr]) { + whitelist[addr] = false; + WhitelistedAddressRemoved(addr); + success = true; + } + } + + /** + * @dev remove addresses from the whitelist + * @param addrs addresses + * @return true if at least one address was removed from the whitelist, + * false if all addresses weren't in the whitelist in the first place + */ + function removeAddressesFromWhitelist(address[] addrs) onlyOwner public returns(bool success) { + for (uint256 i = 0; i < addrs.length; i++) { + if (removeAddressFromWhitelist(addrs[i])) { + success = true; + } + } + } + +} diff --git a/test/ownership/Whitelist.test.js b/test/ownership/Whitelist.test.js new file mode 100644 index 00000000000..e0ae29f96a2 --- /dev/null +++ b/test/ownership/Whitelist.test.js @@ -0,0 +1,103 @@ +import expectThrow from '../helpers/expectThrow'; +import expectEvent from '../helpers/expectEvent'; + +const WhitelistMock = artifacts.require('WhitelistMock'); + +require('chai') + .use(require('chai-as-promised')) + .should(); + +contract('Whitelist', function (accounts) { + let mock; + + const [ + owner, + whitelistedAddress1, + whitelistedAddress2, + anyone, + ] = accounts; + + const whitelistedAddresses = [whitelistedAddress1, whitelistedAddress2]; + + before(async function () { + mock = await WhitelistMock.new(); + }); + + context('in normal conditions', () => { + it('should add address to the whitelist', async function () { + await expectEvent.inTransaction( + mock.addAddressToWhitelist(whitelistedAddress1, { from: owner }), + 'WhitelistedAddressAdded' + ); + const isWhitelisted = await mock.whitelist(whitelistedAddress1); + isWhitelisted.should.be.equal(true); + }); + + it('should add addresses to the whitelist', async function () { + await expectEvent.inTransaction( + mock.addAddressesToWhitelist(whitelistedAddresses, { from: owner }), + 'WhitelistedAddressAdded' + ); + for (let addr of whitelistedAddresses) { + const isWhitelisted = await mock.whitelist(addr); + isWhitelisted.should.be.equal(true); + } + }); + + it('should not announce WhitelistedAddressAdded event if address is already in the whitelist', async function () { + const { logs } = await mock.addAddressToWhitelist(whitelistedAddress1, { from: owner }); + logs.should.be.empty; + }); + + it('should remove address from the whitelist', async function () { + await expectEvent.inTransaction( + mock.removeAddressFromWhitelist(whitelistedAddress1, { from: owner }), + 'WhitelistedAddressRemoved' + ); + let isWhitelisted = await mock.whitelist(whitelistedAddress1); + isWhitelisted.should.be.equal(false); + }); + + it('should remove addresses from the the whitelist', async function () { + await expectEvent.inTransaction( + mock.removeAddressesFromWhitelist(whitelistedAddresses, { from: owner }), + 'WhitelistedAddressRemoved' + ); + for (let addr of whitelistedAddresses) { + const isWhitelisted = await mock.whitelist(addr); + isWhitelisted.should.be.equal(false); + } + }); + + it('should not announce WhitelistedAddressRemoved event if address is not in the whitelist', async function () { + const { logs } = await mock.removeAddressFromWhitelist(whitelistedAddress1, { from: owner }); + logs.should.be.empty; + }); + + it('should allow whitelisted address to call #onlyWhitelistedCanDoThis', async () => { + await mock.addAddressToWhitelist(whitelistedAddress1, { from: owner }); + await mock.onlyWhitelistedCanDoThis({ from: whitelistedAddress1 }) + .should.be.fulfilled; + }); + }); + + context('in adversarial conditions', () => { + it('should not allow "anyone" to add to the whitelist', async () => { + await expectThrow( + mock.addAddressToWhitelist(whitelistedAddress1, { from: anyone }) + ); + }); + + it('should not allow "anyone" to remove from the whitelist', async () => { + await expectThrow( + mock.removeAddressFromWhitelist(whitelistedAddress1, { from: anyone }) + ); + }); + + it('should not allow "anyone" to call #onlyWhitelistedCanDoThis', async () => { + await expectThrow( + mock.onlyWhitelistedCanDoThis({ from: anyone }) + ); + }); + }); +});